Every API here is available over the APIs.io API and to AI agents over MCP.
{
"openapi": "3.1.0",
"info": {
"title": "NoRekey API",
"version": "1.0.0",
"summary": "Convert PDF bank statements to CSV, Excel, OFX, QFX and JSON — every conversion verified against the statement's own balances.",
"description": "Four endpoints: upload a statement, poll its status, download the result in any supported format, and check the team's page balance. Authentication is a bearer API key (created under Settings → Team → API keys; keys are shown once and start with `nrk_`). API conversions draw from the same monthly page allowance as the web converter, on every plan including Free. Rate limit: 120 requests per minute per API key. Source PDFs are deleted the moment processing finishes; statement passwords are used once to open the file and never stored.",
"termsOfService": "https://norekey.com/terms",
"contact": {
"name": "NoRekey support",
"email": "support@norekey.com",
"url": "https://norekey.com/help/using-the-api"
}
},
"servers": [{ "url": "https://norekey.com" }],
"security": [{ "apiKey": [] }],
"paths": {
"/api/v1/conversions": {
"post": {
"operationId": "createConversion",
"summary": "Upload a statement for conversion",
"description": "Accepts the statement either as a multipart file upload or as base64 inside a JSON body (for callers where multipart is awkward). PDFs only, up to 20 MB. Password-protected PDFs convert in one step when the password is supplied; scanned statements convert through OCR automatically and are charged like any other page (`pages_charged` reports the cost). Returns the conversion document immediately — poll it until `status` is `completed`.",
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"required": ["statement"],
"properties": {
"statement": {
"type": "string",
"format": "binary",
"description": "The statement PDF, up to 20 MB."
},
"password": {
"type": "string",
"maxLength": 255,
"description": "Password for a protected PDF. Used once to open the file, never stored."
}
}
}
},
"application/json": {
"schema": {
"type": "object",
"required": ["statement", "filename"],
"properties": {
"statement": {
"type": "string",
"contentEncoding": "base64",
"description": "The statement PDF, base64-encoded. Decoded size limit is the same 20 MB."
},
"filename": {
"type": "string",
"maxLength": 255,
"description": "Original filename, used for display and exports."
},
"password": {
"type": "string",
"maxLength": 255,
"description": "Password for a protected PDF. Used once to open the file, never stored."
}
}
}
}
}
},
"responses": {
"201": {
"description": "Statement accepted; conversion queued or processing.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Conversion"
}
}
}
},
"401": { "$ref": "#/components/responses/Unauthenticated" },
"422": {
"description": "The statement was rejected. `code` identifies why — `password_required` (protected PDF, no password given), `quota_exceeded` (the statement would take the team over its monthly page allowance), `too_many_pages` (over the per-file page limit), or an `invalid_statement`/unreadable-PDF code with a human-readable `message`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": { "$ref": "#/components/responses/RateLimited" }
}
}
},
"/api/v1/conversions/{conversion}": {
"get": {
"operationId": "getConversion",
"summary": "Poll a conversion",
"description": "Returns the conversion document. Poll until `status` is `completed` (or `failed`); `expected_seconds` is a live estimate of total processing time for a statement of this size. Completed conversions report the detected `bank` and `currency`, whether the extraction `balanced` against the statement's own opening and closing figures, and the formats available for export.",
"parameters": [
{ "$ref": "#/components/parameters/conversionId" }
],
"responses": {
"200": {
"description": "The conversion document.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Conversion"
}
}
}
},
"401": { "$ref": "#/components/responses/Unauthenticated" },
"404": { "$ref": "#/components/responses/NotFound" },
"429": { "$ref": "#/components/responses/RateLimited" }
}
}
},
"/api/v1/conversions/{conversion}/export/{format}": {
"get": {
"operationId": "downloadExport",
"summary": "Download a completed conversion",
"description": "Streams the converted statement in the requested format as a file download. Available once `status` is `completed`, until the conversion expires on the team's retention schedule (expired conversions return 410).",
"parameters": [
{ "$ref": "#/components/parameters/conversionId" },
{
"name": "format",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": ["csv", "xlsx", "ofx", "qfx", "json"]
},
"description": "The export format."
}
],
"responses": {
"200": {
"description": "The converted statement as a file download.",
"content": {
"text/csv": { "schema": { "type": "string" } },
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/x-ofx": {
"schema": { "type": "string" }
},
"application/json": {
"schema": { "type": "object" }
}
}
},
"401": { "$ref": "#/components/responses/Unauthenticated" },
"404": {
"description": "Unknown conversion, a conversion belonging to another team, or an unknown export format."
},
"410": {
"description": "The conversion has expired on the team's retention schedule and its data has been deleted."
},
"429": { "$ref": "#/components/responses/RateLimited" }
}
}
},
"/api/v1/usage": {
"get": {
"operationId": "getUsage",
"summary": "Check the team's page balance",
"description": "Returns the page balance of the team the API key belongs to: the plan's allowance for the current usage cycle, the pages charged against it so far, the credit balance on top, and `available`, the total an upload is checked against. A statement uploads when its `pages_charged` (its page count, scanned or not) is no more than `available`; otherwise the upload is refused with `quota_exceeded`. Pages are charged when a conversion is accepted and refunded if it fails. Allowance figures are `null` on unlimited plans. The cycle resets monthly at `cycle.resets_at`; credit pages do not reset. Calling this endpoint costs no pages.",
"responses": {
"200": {
"description": "The team's page balance.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Usage"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthenticated"
},
"429": {
"$ref": "#/components/responses/RateLimited"
}
}
}
}
},
"components": {
"securitySchemes": {
"apiKey": {
"type": "http",
"scheme": "bearer",
"description": "Team API key, sent as `Authorization: Bearer nrk_…`. Create keys under Settings → Team → API keys."
}
},
"parameters": {
"conversionId": {
"name": "conversion",
"in": "path",
"required": true,
"schema": { "type": "string" },
"description": "The conversion id (ULID) returned when the statement was uploaded."
}
},
"responses": {
"Unauthenticated": {
"description": "Missing, malformed or revoked API key."
},
"NotFound": {
"description": "No such conversion visible to this API key — keys see exactly their own team's conversions."
},
"RateLimited": {
"description": "Rate limit exceeded — 120 requests per minute per API key."
}
},
"schemas": {
"Conversion": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ULID identifying the conversion."
},
"statement": {
"type": "string",
"description": "Original filename of the uploaded PDF."
},
"status": {
"type": "string",
"enum": [
"pending",
"processing",
"completed",
"failed",
"expired"
]
},
"pages": {
"type": ["integer", "null"],
"description": "Page count of the statement."
},
"scanned": {
"type": "boolean",
"description": "Whether the statement was detected as scanned and converted through OCR."
},
"pages_charged": {
"type": ["integer", "null"],
"description": "Pages drawn from the monthly allowance — the page count, scanned or not."
},
"expected_seconds": {
"type": "integer",
"description": "Live estimate of total processing time for a statement of this size, in seconds."
},
"bank": {
"type": ["string", "null"],
"description": "Bank or issuer detected from the statement, once completed."
},
"currency": {
"type": ["string", "null"],
"description": "Currency detected from the statement, once completed."
},
"balanced": {
"type": ["boolean", "null"],
"description": "Whether the extracted transactions reconciled against the statement's own opening and closing balances. False means the conversion is flagged for review — inspect before trusting."
},
"opening_balance": {
"type": ["number", "string", "null"],
"description": "The statement's declared opening balance."
},
"closing_balance": {
"type": ["number", "string", "null"],
"description": "The statement's declared closing balance."
},
"error": {
"type": ["string", "null"],
"description": "Human-readable failure reason when status is failed."
},
"formats": {
"type": "array",
"items": { "type": "string" },
"description": "Export formats available from the export endpoint."
},
"created_at": {
"type": ["string", "null"],
"format": "date-time"
},
"completed_at": {
"type": ["string", "null"],
"format": "date-time"
}
}
},
"Usage": {
"type": "object",
"required": ["team", "plan", "cycle", "pages", "limits"],
"properties": {
"team": {
"type": "object",
"required": ["name", "slug"],
"description": "The team the API key belongs to.",
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
},
"plan": {
"type": "string",
"description": "The team's plan, e.g. `free`, `practice` or `firm`."
},
"cycle": {
"type": "object",
"required": ["started_at", "resets_at"],
"description": "The current monthly usage cycle. Paid teams' cycles follow their billing date.",
"properties": {
"started_at": {
"type": "string",
"format": "date-time"
},
"resets_at": {
"type": "string",
"format": "date-time",
"description": "When `used` returns to zero."
}
}
},
"pages": {
"type": "object",
"required": [
"allowance",
"used",
"remaining",
"credit",
"available"
],
"properties": {
"allowance": {
"type": ["integer", "null"],
"minimum": 0,
"description": "Pages the plan includes per cycle; null when unlimited."
},
"used": {
"type": "integer",
"minimum": 0,
"description": "Pages charged this cycle: accepted, in-progress and completed conversions. Failed conversions are refunded."
},
"remaining": {
"type": ["integer", "null"],
"minimum": 0,
"description": "What's left of the plan allowance this cycle (never negative); null when unlimited."
},
"credit": {
"type": "integer",
"minimum": 0,
"description": "Extra pages from page packs the team has bought or pages we've added. Spent only once the plan allowance runs out; they never expire."
},
"available": {
"type": ["integer", "null"],
"minimum": 0,
"description": "`remaining` plus `credit`: the most pages the next upload may be charged. Null when unlimited."
}
}
},
"limits": {
"type": "object",
"required": [
"max_pages_per_file",
"max_file_mb",
"scanned_page_multiplier"
],
"properties": {
"max_pages_per_file": {
"type": "integer",
"minimum": 0,
"description": "Largest statement accepted, in pages."
},
"max_file_mb": {
"type": "integer",
"minimum": 0,
"description": "Largest file accepted, in megabytes."
},
"scanned_page_multiplier": {
"type": "integer",
"minimum": 0,
"description": "Always 1 \u2014 scanned pages are charged like any other. Kept so existing clients don't break."
}
}
}
}
},
"Error": {
"type": "object",
"required": ["code", "message"],
"properties": {
"code": {
"type": "string",
"description": "Machine-readable error identifier, e.g. password_required, quota_exceeded, too_many_pages, invalid_statement."
},
"message": {
"type": "string",
"description": "Human-readable explanation."
}
}
}
}
}
}