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.
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.
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.
| Status | Code | What to do |
|---|---|---|
| 401 | invalid_api_key | The key is wrong or was rotated. Issue a new one in Settings. |
| 400 | insufficient_balance | Not enough pages left. The message says how many are needed. |
| 400 | decryption_failed | The PDF needs a password, or the one supplied is wrong. Send it as password. |
| 400 | invalid_file | The base64 payload could not be decoded. |
| 413 | file_too_large | Over 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. |
| 400 | conversion_error | Extraction failed. Retry once; if it persists the file is likely damaged or not a statement. |
| 404 | job_not_found | Unknown job, or it belongs to another account. |
| 429 | — | Rate 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
/api/v1/api_convertSend 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"
}'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
/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"{
"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
/api/v1/api_jobsEvery 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"{
"data": {
"3f9a1c22-0b7e-4d51-9a3c-77e2b4d81f60": "completed",
"8c1d4e77-2a90-4b6f-8e15-3d90ab77c401": "processing"
},
"error": null,
"humanError": null
}Download the result
/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.xlsxThe 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.
| status | Meaning |
|---|---|
ok | Every verifiable row passed. Safe to import unattended. |
partial | Some rows did not pass. unreconciled lists the affected row numbers, matching the exported file. |
unavailable | The 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.
| Group | Categories |
|---|---|
| Banking | BANK 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 |
| Spend | UTILITIES, TELECOM, GOVERNMENT FEES, TRANSPORT, FUEL, INSURANCE, RENT, GROCERIES, DINING, HEALTHCARE, EDUCATION, TRAVEL, SUBSCRIPTIONS, SHOPPING, DONATION |
| Residual | OTHER 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.