Everything the converter does is available over HTTPS: upload a statement, poll until it's parsed and balance-checked, then download the result in any format. Three endpoints, no SDK required.
Authentication
Owners and admins create keys in Settings → Team → API keys. The key is shown once — store it like a password, and send it as a bearer token on every request:
Authorization: Bearer nrk_vZxd79iwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Keys belong to the team: conversions they create count against the team's page allowance and appear in the team's history, attributed to the API.
1. Upload a statement
curl https://norekey.com/api/v1/conversions \
-H "Authorization: Bearer $NOREKEY_KEY" \
-F "statement=@january.pdf"
Add -F "password=..." for a password-protected PDF — the password is used to open the file and never stored. You'll get 201 Created back immediately, before parsing starts:
{
"id": "01kx90e4hpjjxhkrzb6fz1kja1",
"statement": "january.pdf",
"status": "pending",
"pages": 1,
"bank": null,
"currency": null,
"balanced": null,
"opening_balance": null,
"closing_balance": null,
"error": null,
"formats": ["csv", "xlsx", "ofx", "qfx", "json"],
"created_at": "2026-07-14T09:12:03+00:00",
"completed_at": null
}
Prefer JSON to multipart? Send the file base64-encoded — handy from serverless functions and no-code tools:
curl https://norekey.com/api/v1/conversions \
-H "Authorization: Bearer $NOREKEY_KEY" \
-H "Content-Type: application/json" \
-d "{\"filename\": \"january.pdf\", \"statement\": \"$(base64 -i january.pdf)\"}"
Base64 inflates payloads by about a third, so multipart is the better choice for very large statements.
2. Poll until it's done
curl https://norekey.com/api/v1/conversions/01kx90e4hpjjxhkrzb6fz1kja1 \
-H "Authorization: Bearer $NOREKEY_KEY"
Most statements finish in 10–30 seconds; polling every 2–3 seconds is plenty. When status is completed, the metadata is filled in — including balanced, the same verification the web converter runs:
{
"id": "01kx90e4hpjjxhkrzb6fz1kja1",
"statement": "january.pdf",
"status": "completed",
"pages": 1,
"bank": "HSBC",
"currency": "GBP",
"balanced": true,
"opening_balance": "3425.00",
"closing_balance": "6320.09",
"error": null,
"formats": ["csv", "xlsx", "ofx", "qfx", "json"],
"created_at": "2026-07-14T09:12:03+00:00",
"completed_at": "2026-07-14T09:12:21+00:00"
}
| Status | Meaning |
|---|---|
pending |
Accepted, waiting for a worker. |
processing |
Being parsed and balance-checked right now. |
completed |
Done — exports are ready. Check balanced. |
failed |
Couldn't convert; error says why in plain English. |
expired |
Rows passed your retention window; exports are gone. |
A balanced of true means the rows reconcile exactly against the statement's own opening and closing balances. false means they don't — the data is still downloadable, but check it before importing. null means the statement declared no totals to verify against.
3. Download the export
The final path segment is the format — anything in the formats array works, and new formats join automatically:
curl -O -J https://norekey.com/api/v1/conversions/01kx90e4hpjjxhkrzb6fz1kja1/export/csv \
-H "Authorization: Bearer $NOREKEY_KEY"
The json export is the richest: metadata, declared totals, the balance verdict and every row, with amounts as decimal strings so nothing is lost to float precision:
{
"statement": "january.pdf",
"bank": "HSBC",
"currency": "GBP",
"opening_balance": "3425.00",
"closing_balance": "6320.09",
"balanced": true,
"transactions": [
{
"date": "2026-01-04",
"description": "SAINSBURYS S/MKT",
"amount": "-57.56",
"balance": "3367.44"
},
{
"date": "2026-01-24",
"description": "BACS SALARY ACME LTD",
"amount": "3597.17",
"balance": "6964.61"
}
]
}
A complete script
Python:
import requests, time
API = "https://norekey.com/api/v1"
HEADERS = {"Authorization": "Bearer nrk_..."}
with open("january.pdf", "rb") as f:
conversion = requests.post(
f"{API}/conversions", headers=HEADERS, files={"statement": f}
).json()
while conversion["status"] in ("pending", "processing"):
time.sleep(2)
conversion = requests.get(
f"{API}/conversions/{conversion['id']}", headers=HEADERS
).json()
if conversion["status"] == "completed" and conversion["balanced"]:
csv = requests.get(
f"{API}/conversions/{conversion['id']}/export/csv", headers=HEADERS
)
open("january.csv", "wb").write(csv.content)
else:
print("needs attention:", conversion["error"] or "totals need review")
Node:
const API = 'https://norekey.com/api/v1';
const headers = { Authorization: 'Bearer nrk_...' };
const form = new FormData();
form.append(
'statement',
new Blob([await fs.readFile('january.pdf')]),
'january.pdf',
);
let conversion = await (
await fetch(`${API}/conversions`, { method: 'POST', headers, body: form })
).json();
while (['pending', 'processing'].includes(conversion.status)) {
await new Promise((r) => setTimeout(r, 2000));
conversion = await (
await fetch(`${API}/conversions/${conversion.id}`, { headers })
).json();
}
const rows = await (
await fetch(`${API}/conversions/${conversion.id}/export/json`, { headers })
).json();
console.log(
`${rows.transactions.length} transactions, balanced: ${rows.balanced}`,
);
Errors
Every error is JSON with a code and a human-readable message:
{
"code": "quota_exceeded",
"message": "This 3-page statement would take your team over its monthly page allowance."
}
| Code | Status | When |
|---|---|---|
unauthenticated |
401 | Missing, wrong or revoked key. |
password_required |
422 | Protected PDF uploaded without its password. |
invalid_statement |
422 | Base64 payload that isn't valid base64 or isn't a PDF. |
too_many_pages |
422 | Over the per-file page cap. |
quota_exceeded |
422 | The team's monthly page allowance is spent. |
scanned_statement |
422 | A scanned PDF — detected and declined rather than guessed at. |
| — | 404 | Conversion doesn't exist or belongs to another team. |
| — | 410 | Conversion expired under your retention settings. |
Limits, on every plan
API access isn't gated behind a paid tier — every team, Free included, can create keys and convert programmatically. Requests are limited to 120 per minute per key, and conversions consume your plan's page allowance exactly as web uploads do. Something missing for your integration? Tell us — support@norekey.com.