Formats & exportsUpdated 10 October 2026

Using the API

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. One endpoint for each of those, plus one that reports your page balance: four in all, no SDK required — and a machine-readable OpenAPI 3.1 specification if you'd rather generate a client or import the API into Postman.

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,
    "scanned": false,
    "pages_charged": 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,
    "scanned": false,
    "pages_charged": 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"
        }
    ]
}

Check your page balance

Before sending a batch, ask how many pages the team has left. The call costs no pages:

curl https://norekey.com/api/v1/usage \
  -H "Authorization: Bearer $NOREKEY_KEY"
{
    "team": { "name": "Acme Bookkeeping", "slug": "acme-bookkeeping" },
    "plan": "practice",
    "cycle": {
        "started_at": "2026-09-14T09:30:00+00:00",
        "resets_at": "2026-10-14T09:30:00+00:00"
    },
    "pages": {
        "allowance": 2500,
        "used": 1812,
        "remaining": 688,
        "credit": 50,
        "available": 738
    },
    "limits": {
        "max_pages_per_file": 30,
        "max_file_mb": 20,
        "scanned_page_multiplier": 1
    }
}
Field Meaning
allowance Pages your plan includes each cycle.
used Pages charged this cycle, including conversions still in progress. Failed conversions are refunded.
remaining What's left of the allowance. Never negative.
credit Extra pages granted to your team. Spent only after the allowance runs out, and kept from cycle to cycle.
available remaining plus credit: the most pages your next upload can be charged.
cycle.resets_at When used goes back to zero. Paid teams' cycles follow their billing date.

An upload is accepted when its pages_charged (its page count) is no more than available. scanned_page_multiplier is always 1 now that scans cost the same as any other page; it stays in the response so existing clients keep working. Otherwise it's refused with quota_exceeded. On an unlimited plan, allowance, remaining and available are null.

The balance moves the moment a conversion is accepted, so read it just before a batch rather than caching it for long:

usage = requests.get(f"{API}/usage", headers=HEADERS).json()
available = usage["pages"]["available"]

if available is not None and available < pages_needed:
    print(f"Only {available} pages left until {usage['cycle']['resets_at']}")

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 Not enough pages left: check GET /api/v1/usage.
— 404 Conversion doesn't exist or belongs to another team.
— 410 Conversion expired under your retention settings.

Scanned statements

Scanned or photographed PDFs convert through OCR automatically — the API never prompts. The response marks them with "scanned": true, and they're charged like any other upload: one page of allowance per page.

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. GET /api/v1/usage shows where you stand at any time. Something missing for your integration? Tell us — support@norekey.com.

Was this guide helpful?
Got a statement to convert?
Free to try — no sign-up, balance-checked, deleted within the hour.
Convert a statement