API reference

Convert PDF bank statements to Excel, CSV or JSON from your own code. One request in, a verified spreadsheet out — the same engine and the same page balance as the web app.

Base URL https://rowzx.com/api/v1. Every request needs an API key, and pages come from the same balance as the website.

Authentication

Send your key in the X-API-Key header, or as Authorization: Bearer if your client prefers it. Find and rotate it under Settings in your account.

Rotating a key invalidates the previous one immediately. That is the point — if a key leaks, rotating is the fix.

Keys are stored hashed, so an existing key can never be shown again — only replaced. Keep it server-side: anything in a browser bundle is public.
curl https://rowzx.com/api/v1/api_jobs \
  -H "X-API-Key: $ROWZX_API_KEY"

# Authorization: Bearer works too, if that suits your HTTP client better.
curl https://rowzx.com/api/v1/api_jobs \
  -H "Authorization: Bearer $ROWZX_API_KEY"

Response format

Every response — success or failure — uses the same envelope. The payload lives under data; on failure data is null, error carries a stable machine-readable code, and humanError holds a message you can show a user as-is.

{ "data": { ... }, "error": null, "humanError": null }

{ "data": null,
  "error": "insufficient_balance",
  "humanError": "Insufficient pages. Available: 3, required: 12" }

Errors

Codes are stable; messages are not. Branch on error, never on the text.

StatusCodeWhat to do
401invalid_api_keyThe key is wrong or was rotated. Issue a new one in Settings.
400insufficient_balanceNot enough pages left. The message says how many are needed.
400decryption_failedThe PDF needs a password, or the one supplied is wrong. Send it as password.
400invalid_fileThe base64 payload could not be decoded.
413file_too_largeOver 48 MB. This endpoint carries the file as base64 in the request body; larger statements go through the presigned upload flow (/upload/init → PUT → /upload/complete /convert), which sends the bytes straight to storage.
400conversion_errorExtraction failed. Retry once; if it persists the file is likely damaged or not a statement.
404job_not_foundUnknown job, or it belongs to another account.
429Rate limited. Back off and retry.

Rate limits

Conversion is limited to 100 requests per hour per key. Billing is by page: a 12-page statement costs 12 pages, drawn from the same balance the web app uses. A failed conversion is refunded automatically.

Convert a PDF

POST/api/v1/api_convert

Send a base64-encoded PDF and get back a job id. Works for both document types: bank_statement returns transactions with an accuracy check, invoice returns header fields plus every line item.

The call returns 202 Accepted straight away and the conversion runs in the background. Poll retrieve a job until status is completed, then follow its download_url. Seconds for a recognised layout, up to a couple of minutes for a long document that goes through the AI engine.

Pages are reserved when the job is queued and charged when it finishes. A failed conversion gives them back.

The file goes in the request body as base64, so this endpoint takes PDFs up to 48 MB. Send anything larger through the presigned upload flow — /upload/init → PUT → /upload/complete /convert — which uploads straight to storage.

Parameters

filestringrequired
The PDF, base64-encoded. No data URI prefix.
result_file_typestring
One of csv, xlsx or json. Defaults to csv.
job_typestring
bank_statement (default) or invoice. Invoices return header fields plus every line item; statements return transactions and an accuracy check.
categorizeboolean
Add a Category column to every row, read off the bank's own rules where we know the bank and a bank-agnostic table where we do not. Bank statements only. No extra pages, no extra wait. See the categories reference.
passwordstring
Password for a protected PDF. Used to decrypt, then discarded — never stored.
filenamestring
Your name for this document. Every output row carries it in the filename column, so rows stay traceable once you combine results. Defaults to upload.pdf.

Returns

The job id, the pages reserved and your remaining balance. Watch it with retrieve a job, then fetch the file with download.

curl -X POST https://rowzx.com/api/v1/api_convert \
  -H "X-API-Key: $ROWZX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": "'"$(base64 -i statement.pdf)"'",
    "result_file_type": "xlsx",
    "categorize": true,
    "job_type": "bank_statement"
  }'
Response
202 Accepted

{
  "data": {
    "success": true,
    "message": "Conversion queued. Poll /api_job/{job_id} for status and the download link.",
    "job_id": "3f9a1c22-0b7e-4d51-9a3c-77e2b4d81f60",
    "status": "queued",
    "pages_used": 12,
    "pages_remaining": 988,
    "job_type": "bank_statement"
  },
  "error": null,
  "humanError": null
}

Retrieve a job

GET/api/v1/api_job/{job_id}

Status, the download URL, the detected layout and the accuracy check. Use it to confirm a conversion is trustworthy before importing — see the accuracy check.

Returns

status is one of processing, completed or error. download_url is null until the file exists.

curl https://rowzx.com/api/v1/api_job/$JOB_ID \
  -H "X-API-Key: $ROWZX_API_KEY"
Response
{
  "data": {
    "status": "completed",
    "download_url": "https://rowzx.com/api/v1/download/3f9a1c22-...",
    "detected_bank": "ENBD",
    "pages_used": 12,
    "job_type": "bank_statement",
    "reconciliation": {
      "status": "ok",
      "rows": 248,
      "checked": 247,
      "ok": 247,
      "unreconciled": []
    },
    "categorisation": {
      "status": "ok",
      "rows": 248,
      "categorised": 246,
      "coverage": 0.9919,
      "by_source": { "bank": 231, "generic": 17 },
      "by_category": { "TRANSFER": 88, "BANK CHARGES": 61, "SALARY": 40 }
    }
  },
  "error": null,
  "humanError": null
}

List jobs

GET/api/v1/api_jobs

Every job on the account as a flat map of id to status, newest first. Expired jobs are omitted.

curl https://rowzx.com/api/v1/api_jobs \
  -H "X-API-Key: $ROWZX_API_KEY"
Response
{
  "data": {
    "3f9a1c22-0b7e-4d51-9a3c-77e2b4d81f60": "completed",
    "8c1d4e77-2a90-4b6f-8e15-3d90ab77c401": "processing"
  },
  "error": null,
  "humanError": null
}

Download the result

GET/api/v1/download/{job_id}

Returns a 302 to a short-lived storage URL rather than the bytes themselves, so large files never proxy through the API. Your HTTP client must follow redirects — most do by default, but curl needs -L.

The link expires in five minutes, so fetch it when you are ready to store the file rather than ahead of time.

Returns

The converted file, named after the original upload with the requested extension. 425 means the conversion has not finished yet.

# -L matters: the endpoint redirects to a short-lived storage URL.
curl -L https://rowzx.com/api/v1/download/$JOB_ID \
  -H "X-API-Key: $ROWZX_API_KEY" \
  -o statement.xlsx

The accuracy check

Every conversion is put through an AI accuracy check before it is handed back, and the result comes back on the job.

This is what separates a complete extraction from a plausible-looking one. If a row was dropped, the check fails and we tell you where.

statusMeaning
okEvery verifiable row passed. Safe to import unattended.
partialSome rows did not pass. unreconciled lists the affected row numbers, matching the exported file.
unavailableThe document does not carry enough information to validate against. No claim is made either way.
unavailable is not a failure. We extract exactly what the document contains and never synthesise a balance column — a computed one would be our arithmetic presented as the bank's figures.

Categories

Send categorize: true and every row comes back with a Category column. Two rule sets run in order: the bank's own, written by accountants against that bank's wording, and a bank-agnostic one distilled from all of them. The second is what categorises a scan, or a bank whose layout we have never seen.

What no rule claims becomes OTHER CREDIT or OTHER DEBIT, by the side of the ledger it moved. A merchant we do not recognise stays there rather than being filed under a plausible-looking name.

GroupCategories
BankingBANK CHARGES, TAX, INTEREST, TRANSFER, INTERNAL, CASH, CHEQUE PAYMENT, CHEQUE DEPOSIT, CHEQUE RETURNS - INWARD, CHEQUE RETURNS - OUTWARD, SALARY, EMI, LOAN SETTLEMENT, LOAN DISBURSEMENT, TRADE FINANCE, MARGIN, INVESTMENT, CORPORATE CARD, POS, BILL PAYMENT, REVERSAL
SpendUTILITIES, TELECOM, GOVERNMENT FEES, TRANSPORT, FUEL, INSURANCE, RENT, GROCERIES, DINING, HEALTHCARE, EDUCATION, TRAVEL, SUBSCRIPTIONS, SHOPPING, DONATION
ResidualOTHER CREDIT, OTHER DEBIT

The job carries a categorisation summary next to the accuracy check: rows, categorised (rows a named category claimed), coverage, by_source and by_category. Rows in the residual buckets are counted in by_category but not in categorised, so coverage never flatters itself.

A bank's own table may return categories outside this list — that is its vocabulary, and we pass it through unchanged. A second column, Category 2, appears only where a bank table defines a sub-category.

Cookies on RowzX

Necessary cookies keep you signed in. Analytics only if you agree. What we store

We need a few to sign you in and keep your conversions working. We would also like to record how the site is used and which advert brought you, so we can see where it gets in your way. That part is yours to decide. What we store