Skip to content

API reference

One endpoint to upload, one to fetch or delete, and flexible endpoints to stream or export. Everything returns JSON except the file endpoints, which stream the converted document.

Not currently offered as a hosted service. This documents the engine's HTTP contract, which the command-line tool and self-hosted builds implement. No API keys are issued today, and conversion on this site runs in your browser. If you need programmatic access, get in touch — it helps us prioritise it.

https://statement2sheets.com/api/v1 Stable — v1

Quickstart

Post one or more PDFs as multipart form data. Files are parsed and reconciled synchronously in memory, returning 200 with the completed Job object (or 422 if all files fail).

Authentication: API access is available for paid accounts. Pass your API key via the Authorization: Bearer <key> or X-API-Key: <key> header on all requests.
curl
curl -X POST https://statement2sheets.com/api/v1/convert \
  -H "Authorization: Bearer s2s_your_api_key" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "password=hunter2"

Endpoints

POST /convert

Upload one or more PDF statements.

Multipart request. Uploaded files are parsed and reconciled synchronously in memory. Requires a paid API key. Returns 200 with the completed Job when at least one document converted, 422 when all failed, or 429 when the allowance is exhausted. Maximum 20 files per request, 25 MB each.

FieldTypeRequiredDescription
filesfile[]requiredRepeatable PDF part. One entry per statement.
passwordstringoptionalPassword for encrypted PDFs. Applies to every file in the request.
GET /jobs/{job_id}

Fetch job state.

Retrieves a conversion job by its id while held in memory. Returns 200 with the Job object, or 404 once the retention window has expired (1 hour by default) or if it does not exist.

FieldTypeRequiredDescription
job_idstringrequiredIdentifier returned by POST /convert.
DELETE /jobs/{job_id}

Erase conversion immediately.

Erases the conversion from memory before its retention window ends. Returns 204 on success, or 404 if it does not exist.

FieldTypeRequiredDescription
job_idstringrequiredIdentifier returned by POST /convert.
POST /jobs/{job_id}/unlock

Supply the password for an encrypted document.

Retries the locked documents of an existing job in place. The job id and every document id are preserved, so edits already made to other files in the batch survive. 200 if anything unlocked, 401 if the password was wrong (the document stays needs_password and is retryable), 409 if nothing is locked.

FieldTypeRequiredDescription
passwordstringrequiredThe PDF password to try.
document_idstringoptionalUnlock one document. Omit to try the password on every locked document — a batch may carry files with different passwords.
GET /jobs/{job_id}/documents/{doc_id}/download

Stream the converted file as parsed.

Responds with the file body and Content-Disposition: attachment. Use this when the client has made no edits.

FieldTypeRequiredDescription
formatenumrequiredOne of csv, xlsx, json, ofx. Query parameter.
columnsstringoptionalComma-separated field list, in order — for example date,description,debit,credit,balance. Omit for the statement’s own layout.
GET /jobs/{job_id}/download

Download every statement in the job at once.

mode=combined merges the batch into one file carrying Source and Account columns — and, for XLSX, a Summary sheet with each statement’s totals and balance check. mode=zip returns an archive of the individual files instead.

FieldTypeRequiredDescription
formatenumrequiredOne of csv, xlsx, json, ofx.
modeenumoptionalcombined (default) or zip.
columnsstringoptionalComma-separated field list, as on the single download.
POST /jobs/{job_id}/documents/{doc_id}/export

Re-export using client-edited rows.

Send the corrected transaction array back and the file is rendered from those rows instead of the original extraction. Streams the file the same way download does.

FieldTypeRequiredDescription
formatenumrequiredOne of csv, xlsx, json, ofx.
transactionsTransaction[]optionalThe full, edited row set to render. Omit to re-export the stored rows — which is how a pure column re-map avoids resending every row.
columnsColumn[]optionalColumn layout as { field, header? } objects, in order. Unlike the query-string form, this carries renamed headings.
GET /quota

Remaining allowance for the caller.

Anonymous callers are identified by IP. Metered in pages, not files, on a rolling window. Returns used, limit, remaining and resets_at — an ISO timestamp. A 429 also carries a Retry-After header in seconds.

GET /banks

List recognised banking institutions.

The institutions recognised by name for automatic date order and decimal convention handling.

GET /health

Liveness probe.

Returns status and the deployed version string. Available at /health (root) and /api/v1/health.

Conversion workflow

Conversions execute synchronously in memory. The returned job_id can be used immediately to download converted files, unlock password-protected documents, or re-export custom layouts.

JavaScript
// 1. Upload statements with your paid API key
const form = new FormData();
form.append('files', pdfFile, 'january.pdf');

const job = await fetch(`${BASE}/convert`, {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer s2s_your_api_key'
  },
  body: form
}).then((r) => r.json());

// 2. Download the first converted statement
const doc = job.documents[0];
const file = await fetch(
  `${BASE}/jobs/${job.job_id}/documents/${doc.id}/download?format=csv`,
  { headers: { 'Authorization': 'Bearer s2s_your_api_key' } }
).then((r) => r.blob());

// 3. Or stream all statements combined into a single workbook
const workbook = await fetch(
  `${BASE}/jobs/${job.job_id}/download?format=xlsx&mode=combined`,
  { headers: { 'Authorization': 'Bearer s2s_your_api_key' } }
).then((r) => r.blob());

Column layouts

A statement has one natural shape, but the file you need depends on what you are importing into. Rather than guess, name the columns you want. The most consequential choice is between one signed amount column and a debit/credit pair, which splits that one signed figure into two positive-magnitude columns.

FieldMeaning
dateTransaction date, YYYY-MM-DD.
value_dateValue date, where the statement prints one.
descriptionNarrative as printed.
amountSigned: negative is money out.
debitMoney out, as a positive figure. Blank on credits.
creditMoney in, as a positive figure. Blank on debits.
balanceRunning balance, where the statement prints one.
currencyISO code for the account.
accountMasked account number.
sourceThe uploaded file the row came from.
pagePage of the PDF the row was read from.
confidenceExtraction confidence, 0–1.
cURL
# Two positive columns, what accounting imports expect
curl "${BASE}/jobs/${jobId}/documents/${docId}/download\
?format=csv&columns=date,description,debit,credit,balance"

# Renamed headings need the POST form
curl -X POST "${BASE}/jobs/${jobId}/documents/${docId}/export" \
  -H 'Content-Type: application/json' \
  -d '{"format":"csv","columns":[{"field":"date","header":"Posted"},{"field":"debit"}]}'

# The whole batch, merged into one workbook
curl "${BASE}/jobs/${jobId}/download?format=xlsx&mode=combined"

Objects

Job

status is one of queued, processing, completed, failed or needs_password.

Job
{
  "job_id": "j_9f2a4c81",
  "status": "completed",
  "created_at": "2026-09-05T10:00:00Z",
  "documents": [ /* Document objects */ ]
}

Document

amount is signed — negative is money out. balance is null when the statement has no balance column. summary.balance_check is passed, failed or unavailable.

Document
{
  "id": "d_1a2b",
  "filename": "chase-jan.pdf",
  "status": "completed",
  "error": null,
  "pages": 4,
  "bank": { "id": "chase", "name": "Chase", "confidence": 0.97 },
  "account": {
    "number": "****1234",
    "holder": "JANE DOE",
    "type": "checking",
    "currency": "USD",
    "period_start": "2024-01-01",
    "period_end": "2024-01-31",
    "opening_balance": 1204.11,
    "closing_balance": 2093.56
  },
  "columns": ["date","description","debit","credit","balance"],
  "transactions": [
    {
      "index": 0,
      "date": "2024-01-03",
      "description": "AMAZON MKTPL AMZN.COM/BILL",
      "amount": -42.10,
      "balance": 1162.01,
      "page": 1,
      "confidence": 0.99
    }
  ],
  "summary": {
    "count": 83,
    "total_in": 5100.00,
    "total_out": -4210.55,
    "net": 889.45,
    "balance_check": "passed",
    "confidence": 0.98
  },
  "warnings": ["Page 3: 2 rows had no balance column"]
}

Errors

Every 4xx and 5xx response carries the same envelope.

Error response
{
  "error": {
    "code": "encrypted_pdf",
    "message": "Password required to open this document."
  }
}
CodeHTTPMeaning
unauthorized401Missing or invalid paid API key. Obtain one at statement2sheets.com/pricing.
invalid_request400Malformed upload or request body.
invalid_format400Requested export or download format is not supported.
invalid_pdf400The file is not a readable PDF, or has no text layer.
encrypted_pdf401The PDF is encrypted and the password was missing or wrong.
too_large413A file exceeded the 25 MB per-file limit.
too_many_files400More than 20 files in a single request.
quota_exceeded429The caller has used their allowance for the period.
scanned_pdf422The pages are images with no text behind them. Needs OCR.
no_transactions_found422Parsing succeeded but produced no transaction rows.
nothing_locked409Unlock was called on a job with no document awaiting a password.
not_converted409Download was called on a document or job where conversion failed.
not_found404The job expired or never existed.
internal500Unexpected server-side failure. Safe to retry.

Limits

  • 20 files per request
  • 25 MB per file
  • 50 pages per 24 hours for anonymous callers (FREE_PAGES_PER_DAY)
  • 5,000 API calls per month on Pro
  • Converted data is deleted 1 hour after upload

Need higher volume or a dedicated instance? See Business pricing or get in touch.