API Reference

Convert bank statement PDFs to structured data via REST API. Upload a file, poll for completion, and export as CSV, Excel, JSON, or accounting formats.

Quick start

curl -X POST https://api.bankstatemently.com/v1/documents \
  -H "X-API-Key: bsk_live_..." \
  -H "Content-Type: application/pdf" \
  --data-binary @statement.pdf

Summary

REST API for uploading bank statements, polling processing status, and exporting converted data. Base URL: https://api.bankstatemently.com/v1.

All API responses return JSON with camelCase keys (except error.request_id and error.doc_url). Webhook payloads use snake_case for envelope keys (event_id, event_type, created_at) and camelCase for the nested data object. All timestamps are ISO 8601 UTC. All export URLs are absolute.

Authentication

Pass your API key via the X-API-Key header:

curl -H "X-API-Key: bsk_live_..." https://api.bankstatemently.com/v1/documents

Keys are org-scoped and created from the Developer Dashboard. Each key inherits the org's rate limits and credit balance.

For frontend integrations, a Clerk JWT in the Authorization: Bearer <token> header is also accepted.

Endpoints

POST /v1/documents

Upload a PDF bank statement for conversion.

Request: Send the raw PDF as the request body with Content-Type: application/pdf.

Headers:

HeaderRequiredDescription
X-API-KeyYesYour API key
Content-TypeYesMust be application/pdf
X-FilenameNoOriginal filename (for display in dashboard). Defaults to document.pdf.
X-Webhook-URLNoHTTPS URL to receive document.completed / document.failed events for this upload. Overrides the default webhook URL configured on the API key.
X-PasswordNoDecryption password for encrypted PDFs. When provided, the PDF is decrypted and processed in a single request — no need for the two-step unlock flow. The password is used in memory only and never stored or logged.
Idempotency-KeyNoDeduplication key — 24-hour TTL. Replayed responses include X-Idempotency-Replayed: true.

Query parameters:

ParameterRequiredDescription
retryNoSet to true to reprocess a previously failed upload of the same file. Without this, re-uploading a failed file returns already_processing (409).

Rate limit: 10 req/min

curl -X POST https://api.bankstatemently.com/v1/documents \
  -H "X-API-Key: bsk_live_..." \
  -H "Content-Type: application/pdf" \
  -H "X-Filename: statement-jan-2026.pdf" \
  --data-binary @statement.pdf

Response (202 Accepted):

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "processing",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "statements": [],
  "links": {
    "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851"
  }
}

Response (202 Accepted) -- password-protected PDF:

When the uploaded PDF is password-protected, the document is stored but not processed. The response includes an unlock link:

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "password_required",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "statements": [],
  "links": {
    "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851",
    "unlock": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851/unlock"
  }
}

To proceed, call the unlock endpoint with the password. No credits are deducted until processing begins.


GET /v1/documents/:id

Poll document processing status.

Rate limit: 120 req/min

curl https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851 \
  -H "X-API-Key: bsk_live_..."

Response (200 OK) -- processing:

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "processing",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "updatedAt": "2026-02-27T10:15:00Z",
  "statements": [],
  "links": {
    "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851"
  }
}

Response (200 OK) -- completed:

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "completed",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "updatedAt": "2026-02-27T10:15:25Z",
  "document": {
    "bank": "DBS Bank",
    "country": "SG",
    "currency": "SGD",
    "documentType": "bank-statement",
    "pageCount": 3,
    "transactionCount": 47,
    "accountCount": 1
  },
  "statements": [
    {
      "index": 0,
      "statementPeriod": {
        "from": "2026-01-01",
        "to": "2026-01-31"
      },
      "transactionCount": 47
    }
  ],
  "exports": {
    "json": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/json",
    "csv": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/csv",
    "xlsx": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xlsx",
    "qbo": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/qbo",
    "xero": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xero"
  },
  "links": {
    "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-...",
    "data": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../data"
  }
}

Response (200 OK) -- failed:

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "failed",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "updatedAt": "2026-02-27T10:15:12Z",
  "statements": [],
  "error": "Could not extract transactions from this PDF",
  "links": {
    "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851"
  }
}

Status values: processing, completed, failed, password_required.


POST /v1/documents/:id/unlock

Unlock a password-protected document by providing the decryption password. On success, processing begins automatically and the document transitions to processing status.

The password is used in memory only -- it is never stored, logged, or persisted.

Request: application/json

FieldRequiredDescription
passwordYesThe PDF decryption password (max 1000 chars)

Rate limit: 5 req/min

curl -X POST https://api.bankstatemently.com/v1/documents/d290f1ee-.../unlock \
  -H "X-API-Key: bsk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"password": "my-pdf-password"}'

Response (200 OK):

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "processing",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "statements": [],
  "links": {
    "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851"
  }
}

After unlocking, poll GET /v1/documents/:id as usual until status is completed.

Errors:

CodeHTTPDescription
wrong_password422Incorrect password. meta includes { attempts, max_attempts }.
unlock_attempts_exceeded429Too many wrong attempts (max 10). Re-upload the file to reset.
invalid_parameters400Document is not in password_required status
document_not_found404Document does not exist or does not belong to you

GET /v1/documents/:id/data

Retrieve structured document data including accounts and transactions. Only available when document status is completed.

Query parameters:

ParamDefaultDescription
rawfalseSet to true to include originalData (raw pre-normalization cell values) on each transaction
limit500Max transactions per page (max 2000, or 500 when raw=true)
offset0Pagination offset for transactions

Rate limit: 60 req/min

curl https://api.bankstatemently.com/v1/documents/d290f1ee-.../data \
  -H "X-API-Key: bsk_live_..."

# With original data and pagination
curl "https://api.bankstatemently.com/v1/documents/d290f1ee-.../data?raw=true&limit=100&offset=0" \
  -H "X-API-Key: bsk_live_..."

Response (200 OK):

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "contentHash": "a1b2c3d4e5",
  "createdAt": "2026-02-27T10:15:00Z",
  "updatedAt": "2026-02-27T10:15:25Z",
  "processedAt": "2026-02-27T10:15:25Z",
  "document": {
    "documentType": "bank-statement",
    "bank": "DBS Bank",
    "bankId": "dbs-bank-ltd",
    "country": "SG",
    "currency": "SGD",
    "languages": ["en"],
    "statementDate": "2026-01-31",
    "statementPeriod": { "from": "2026-01-01", "to": "2026-01-31" },
    "pageCount": 3
  },
  "accounts": [
    {
      "accountId": 0,
      "accountNumber": "XXX-X-XX1234",
      "accountHolderName": "John Doe",
      "currency": "SGD",
      "isPrimary": true,
      "totals": {
        "openingBalance": 5000.00,
        "closingBalance": 4250.75,
        "totalCredits": 1200.00,
        "totalDebits": 1949.25
      }
    }
  ],
  "transactions": [
    {
      "sequence": 0,
      "date": "2026-01-02",
      "description": "SALARY CREDIT",
      "amount": 1200,
      "direction": "credit",
      "currency": "SGD",
      "balance": 6200.00,
      "accountId": 0,
      "accountNumber": "XXX-X-XX1234"
    }
  ],
  "pagination": {
    "total": 47,
    "limit": 500,
    "offset": 0,
    "hasMore": false
  },
  "exports": {
    "json": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/json",
    "csv": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/csv",
    "xlsx": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xlsx",
    "qbo": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/qbo",
    "xero": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xero"
  }
}

Note on sequence: The sequence field is a positional index within the current processing result. It is not a permanent identifier and may change if the document is reprocessed. Use processedAt to detect when the underlying data has changed.

Note on date: The date field is either a date (YYYY-MM-DD) or a datetime (YYYY-MM-DDTHH:mm:ss) depending on the precision available in the source document. Always parse with a library that handles both formats.

Error (409 Conflict) -- document not ready:

{
  "error": {
    "type": "invalid_request_error",
    "code": "document_not_ready",
    "message": "Document is not ready (status: processing). Poll GET /v1/documents/{id} until status is \"completed\".",
    "doc_url": "https://bankstatemently.com/developers/docs/errors/document_not_ready",
    "meta": {
      "status": "processing"
    },
    "request_id": "req_7yNm3kPqW2a4bZ1c"
  }
}

GET /v1/documents/:id/export/:format

Download the converted document in a specific format.

Formats: json, csv, xlsx, qbo, xero

Query parameters:

ParamDefaultDescription
flavorenhancedoriginal, enhanced, or normalized
dateFormat--Date format string (Xero only, e.g. DD/MM/YYYY)

Rate limit: 30 req/min

Exports are deterministic for a given contentHash + flavor + format combination.

# Download as CSV with normalized flavor
curl -OJ https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/csv?flavor=normalized \
  -H "X-API-Key: bsk_live_..."

# Download Xero-compatible CSV with AU date format
curl -OJ https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xero?dateFormat=DD/MM/YYYY \
  -H "X-API-Key: bsk_live_..."

Response: The file is returned as a download with Content-Disposition: attachment and a generated filename based on the original upload name.

FormatContent-Type
jsonapplication/json
csvtext/csv
xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
qboapplication/x-ofx
xerotext/csv; charset=utf-8

Use -OJ with curl to save the file with the server-provided filename.


GET /v1/documents

List recent documents with pagination.

Query parameters:

ParamDefaultDescription
limit20Max items per page (max 100)
offset0Pagination offset
status--Filter by status: processing, completed, failed, password_required

Rate limit: 120 req/min

curl "https://api.bankstatemently.com/v1/documents?limit=10&status=completed" \
  -H "X-API-Key: bsk_live_..."

Response (200 OK):

{
  "data": [
    {
      "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
      "status": "completed",
      "contentHash": "a1b2c3d4e5",
      "filename": "statement-jan-2026.pdf",
      "pageCount": 3,
      "source": "api",
      "createdAt": "2026-02-27T10:15:00Z",
      "updatedAt": "2026-02-27T10:15:25Z",
      "exports": {
        "json": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/json",
        "csv": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/csv",
        "xlsx": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xlsx",
        "qbo": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/qbo",
        "xero": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xero"
      }
    }
  ],
  "pagination": {
    "total": 42,
    "limit": 10,
    "offset": 0,
    "hasMore": true
  }
}

The exports field is only present when status is completed. The source field indicates the upload origin ("api" or "dashboard").


GET /v1/credits

Check your current credit balance. Recommended: call this before large uploads to ensure sufficient credits.

Rate limit: 120 req/min

curl https://api.bankstatemently.com/v1/credits \
  -H "X-API-Key: bsk_live_..."

Response (200 OK):

{
  "balance": 47,
  "limit": 100,
  "planId": "pro",
  "creditsExpireAt": "2026-04-01T00:00:00.000Z"
}
FieldTypeDescription
balanceintegerCurrent remaining credits. 1 credit = 1 page.
limitintegerPlan allowance (10 for free, plan-specific for paid).
planIdstringCurrent plan: free, pro, or business.
creditsExpireAtstring | nullWhen current plan credits reset, or null for plans without expiry.

Response is cached for 5 seconds (Cache-Control: private, max-age=5).


POST /v1/benchmark/evaluate

Evaluate parsed bank statement transactions against the Bank Statement Parsing Benchmark ground truth. Free to use — no credits consumed.

The contentHash is the SHA-256 hex digest of the benchmark PDF you parsed. This ties the evaluation to the exact file, supports dataset versioning, and prevents ID-based guessing.

Request: application/json

FieldRequiredTypeDescription
contentHashYesstringSHA-256 hex digest of the benchmark PDF (64 lowercase hex characters)
transactionsYesarrayParsed transactions, 1-2000 (see schema below)

Transaction object:

FieldRequiredTypeDescription
dateYesstringISO 8601 date (YYYY-MM-DD)
descriptionYesstringTransaction description
amountYesnumberTransaction amount. Positive absolute value, or negative to indicate debit.
directionNostring"credit" or "debit". If omitted, inferred from amount sign (negative = debit, positive/zero = credit). Negative amount with "credit" is rejected as contradictory.
balanceNonumberRunning balance (used in parsed scoring only — normalized scoring does not evaluate balance)
currencyNostringISO 4217 currency code
originalDataYesobjectRaw column values as on the PDF (see below)

originalData format: A flat Record<string, string> where each key is the column header exactly as it appears in the PDF, and each value is the cell text exactly as printed — no normalization, no type conversion. All values are strings, even amounts and dates.

{
  "Date": "02/06/2025",
  "Description": "NTUC FAIRPRICE",
  "Withdrawal (-)": "12.20",
  "Deposit (+)": "",
  "Balance": "15,438.55"
}

Rate limit: 50 req/hour (per API key)

HASH=$(shasum -a 256 bsb-001-statement.pdf | cut -d' ' -f1)

curl -X POST https://api.bankstatemently.com/v1/benchmark/evaluate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: bsk_live_..." \
  -d "{
    \"contentHash\": \"$HASH\",
    \"transactions\": [
      {
        \"date\": \"2025-06-02\",
        \"description\": \"NTUC FAIRPRICE\",
        \"amount\": 12.20,
        \"direction\": \"debit\",
        \"balance\": 15438.55,
        \"originalData\": {
          \"Date\": \"02/06/2025\",
          \"Description\": \"NTUC FAIRPRICE\",
          \"Withdrawal (-)\": \"12.20\",
          \"Balance\": \"15,438.55\"
        }
      }
    ]
  }"

Response (200 OK):

{
  "contentHash": "a1b2c3d4e5f6...",
  "id": "bsb-001",
  "datasetVersion": "2.0",
  "difficulty": "basic",
  "challenges": ["credit-debit-columns", "balance-validation", "multi-line-descriptions"],
  "normalizedScore": {
    "overall": 0.94,
    "extraction": 0.97,
    "integrity": 0.97,
    "fields": { "date": 0.98, "description": 0.91, "amount": 0.95 },
    "alignment": { "matched": 11, "missing": 1, "extra": 0, "total": 12 }
  },
  "parsedScore": {
    "overall": 0.95,
    "extraction": 0.98,
    "integrity": 0.97,
    "fields": { "Date": 1.00, "Description": 0.95, "Withdrawal (-)": 1.00, "Deposit (+)": 1.00, "Balance": 0.94 },
    "alignment": { "matched": 11, "missing": 1, "extra": 0, "total": 12 }
  }
}
FieldDescription
contentHashEcho of the submitted hash
idStatement ID (e.g. bsb-001)
datasetVersionGround truth version
difficultybasic, intermediate, or advanced
challengesParsing challenges present in the statement
normalizedScoreScores against canonical values (ISO dates, numeric amounts)
parsedScoreScores against raw PDF cell values

Scoring details:

  • overall — headline score: extraction × integrity (0-1). Both components must be strong.
  • extraction — weighted accuracy across all fields (0-1)
  • integrity — structural correctness: do credit/debit sums match, are all rows present, are polarities correct? (0-1)
  • fields — per-field accuracy across matched rows. Normalized uses canonical names (date, description, amount). Parsed uses the original column headers from the PDF (e.g. Date, Withdrawal (-), Balance) — these vary by statement.
  • alignmentmatched, missing (in ground truth but not submission), extra (in submission but not ground truth), total (ground truth count)

Webhooks

Configure webhook URLs when creating an API key in the Developer Dashboard, or update them later. HTTPS endpoints only.

Events

EventFired when
document.completedProcessing finished successfully
document.failedProcessing failed permanently

Payload

document.completed:

{
  "event_id": "evt_a1b2c3d4-...",
  "event_type": "document.completed",
  "created_at": "2026-02-27T10:15:30Z",
  "data": {
    "id": "d290f1ee-...",
    "contentHash": "a1b2c3d4e5",
    "status": "completed",
    "exports": {
      "json": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/json",
      "csv": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/csv",
      "xlsx": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xlsx",
      "qbo": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/qbo",
      "xero": "https://api.bankstatemently.com/v1/documents/d290f1ee-.../export/xero"
    }
  }
}

document.failed:

{
  "event_id": "evt_e5f6g7h8-...",
  "event_type": "document.failed",
  "created_at": "2026-02-27T10:15:30Z",
  "data": {
    "id": "d290f1ee-...",
    "contentHash": "a1b2c3d4e5",
    "status": "failed",
    "error": "Could not extract transactions from this PDF"
  }
}

Verification

Each request includes:

  • X-Webhook-Signature -- HMAC-SHA256 hex digest of the raw body, signed with your webhook secret
  • X-Webhook-Id -- unique delivery ID for idempotency
  • X-Webhook-Event-Id -- matches the event_id in the payload body
import hmac, hashlib
expected = hmac.new(webhook_secret.encode(), request_body.encode(), hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, request.headers["X-Webhook-Signature"])

Retries

Failed deliveries (non-2xx or timeout) retry up to 3 times with backoff: immediate, ~30s, ~5min.

Error Schema

All errors return a consistent envelope with a broad type, a specific code, a human-readable message, and an optional meta object with structured data:

{
  "error": {
    "type": "quota_exceeded",
    "code": "insufficient_credits",
    "message": "Insufficient credits. This file requires 3 credit(s) but you have 1 remaining. Purchase more at https://bankstatemently.com/pricing.",
    "doc_url": "https://bankstatemently.com/developers/docs/errors/insufficient_credits",
    "meta": {
      "credits_required": 3,
      "credits_remaining": 1
    },
    "request_id": "req_7yNm3kPqW2a4bZ1c"
  }
}
  • type -- broad category (5 values, frozen). Switch on this for your retry/handling strategy.
  • code -- specific error (append-only, never renamed). Switch on this for granular handling.
  • message -- human-readable with actionable context. Do not parse -- use code instead.
  • doc_url -- link to the documentation page for this error code. Always present.
  • meta -- optional structured data for programmatic use. Shape is deterministic by code (same code always produces the same meta shape). Absent when no structured data applies.
  • request_id -- unique request identifier. Include when contacting support. Uses snake_case for compatibility with error-handling conventions.

Error Types

TypeStrategyHTTP Codes
authentication_errorFix API key401
invalid_request_errorFix the request -- never retry as-is400, 404, 409, 422, 429
quota_exceededBuy credits or upgrade plan -- do NOT retry403
rate_limit_errorRetry with exponential backoff429
api_errorRetry later or contact support500, 503

Error Codes

authentication_error

CodeHTTPDescription
invalid_api_key401Missing, malformed, or unrecognized API key
key_revoked401API key has been revoked
key_expired401API key has expired

invalid_request_error

CodeHTTPDescriptionmeta
missing_file400No file attached to the request--
invalid_file_type400File is not a supported format (PDF only)--
wrong_password422Incorrect password for encrypted PDF{ attempts, max_attempts }
unlock_attempts_exceeded429Too many wrong password attempts{ attempts, max_attempts }
invalid_pdf400File could not be read as a valid PDF--
unsupported_document_type400Document is not a bank or credit card statement{ detected_type, confidence, reason } — see reason values below
invalid_parameters400Request parameters failed validation, or file field has wrong name (must be file){ fields: [{ field, message }] } (when applicable)
invalid_export_flavor400Invalid export flavor value--
endpoint_not_found404The requested API endpoint does not exist--
document_not_found404Document does not exist or does not belong to you--
document_not_ready409Document processing not yet completed{ status }
already_processing409Document is already being processed, or was previously uploaded and failed (re-upload with retry=true to reprocess)--
export_failed422Export generation failed--

unsupported_document_typemeta.reason values:

reasonDescription
document_type_mismatchPre-processing identified a known non-statement type (e.g. invoice, receipt, tax form) with high confidence. No VLM call was made.
no_financial_contentDocument has readable text but no financial signals (no amounts, balances, or transaction patterns). No VLM call was made.
vlm_rejectedDocument passed pre-processing checks but the VLM found zero transactions. meta.vlm_reasoning may contain additional detail.

quota_exceeded

CodeHTTPDescriptionmeta
insufficient_credits403Not enough credits for this file{ credits_required, credits_remaining }
page_limit_exceeded403File exceeds your plan's page limit{ page_count, page_limit }
file_too_large403File exceeds your plan's size limit{ max_size_mb }

rate_limit_error

CodeHTTPDescriptionmeta
rate_limit_exceeded429Too many requests{ retry_after }

api_error

CodeHTTPDescription
internal_error500Unexpected server error
service_unavailable503Service temporarily unavailable

Response Headers

HeaderDescription
X-Request-IdUnique request identifier (use when contacting support)
X-API-VersionAlways 1
RateLimit-LimitMax requests allowed in the current window
RateLimit-RemainingRequests remaining in the current window
RateLimit-ResetUnix timestamp (seconds) when the window resets
Retry-AfterSeconds until rate limit resets (on 429 only)
X-Idempotency-Replayedtrue when returning a cached idempotent response

Rate Limits

Limits are per org. Exceeding a limit returns 429 with a Retry-After header.

EndpointLimit
POST /v1/documents10 req/min
POST /v1/documents/:id/unlock5 req/min
GET /v1/documents/:id/export/:format30 req/min
GET /v1/documents/:id/data60 req/min
GET /v1/documents, GET /v1/documents/:id120 req/min
GET /v1/credits120 req/min
POST /v1/benchmark/evaluate50 req/hour
Global baseline120 req/min

Quick Start Examples

Python

import requests, time

API_KEY = "bsk_live_..."
BASE = "https://api.bankstatemently.com/v1"
headers = {"X-API-Key": API_KEY}

# 1. Upload
with open("statement.pdf", "rb") as f:
    resp = requests.post(f"{BASE}/documents", headers=headers, files={"file": f})
doc = resp.json()
if "error" in doc:
    print(f"Upload failed: {doc['error']['code']} - {doc['error']['message']}")
    exit(1)
doc_id = doc["id"]

# 2. Handle password-protected PDFs
if doc["status"] == "password_required":
    password = input("PDF is encrypted. Enter password: ")
    resp = requests.post(f"{BASE}/documents/{doc_id}/unlock", headers=headers,
                         json={"password": password})
    doc = resp.json()

# 3. Poll until complete
while doc["status"] == "processing":
    time.sleep(2)
    doc = requests.get(f"{BASE}/documents/{doc_id}", headers=headers).json()

if doc["status"] == "failed":
    print("Processing failed:", doc.get("error"))
    exit(1)

# 4. Get structured data (accounts + transactions)
data = requests.get(f"{BASE}/documents/{doc_id}/data", headers=headers).json()
for txn in data["transactions"]:
    print(f"{txn['date']} {txn['description']} {txn['amount']} {txn['direction']}")

# 5. Export as file
csv_resp = requests.get(f"{BASE}/documents/{doc_id}/export/csv", headers=headers)
with open("output.csv", "wb") as f:
    f.write(csv_resp.content)

Node.js

import { readFileSync, writeFileSync } from "node:fs";
import { createInterface } from "node:readline/promises";

const API_KEY = "bsk_live_...";
const BASE = "https://api.bankstatemently.com/v1";
const headers = { "X-API-Key": API_KEY };

async function convert(pdfPath) {
  // 1. Upload
  const form = new FormData();
  form.append("file", new Blob([readFileSync(pdfPath)]), "statement.pdf");
  let doc = await fetch(`${BASE}/documents`, {
    method: "POST", headers, body: form
  }).then(r => r.json());

  if (doc.error) {
    console.error(`Upload failed: ${doc.error.code} - ${doc.error.message}`);
    return;
  }

  // 2. Handle password-protected PDFs
  if (doc.status === "password_required") {
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    const password = await rl.question("PDF is encrypted. Enter password: ");
    rl.close();
    doc = await fetch(`${BASE}/documents/${doc.id}/unlock`, {
      method: "POST",
      headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({ password })
    }).then(r => r.json());
  }

  // 3. Poll until complete
  while (doc.status === "processing") {
    await new Promise(r => setTimeout(r, 2000));
    doc = await fetch(`${BASE}/documents/${doc.id}`, { headers }).then(r => r.json());
  }

  if (doc.status === "failed") {
    console.error("Processing failed:", doc.error);
    return;
  }

  // 4. Get structured data (accounts + transactions)
  const data = await fetch(`${BASE}/documents/${doc.id}/data`, { headers }).then(r => r.json());
  console.log(`Found ${data.transactions.length} transactions`);

  // 5. Export as file
  const csv = await fetch(`${BASE}/documents/${doc.id}/export/csv`, { headers });
  writeFileSync("output.csv", Buffer.from(await csv.arrayBuffer()));
}

convert("statement.pdf");

FAQ

What file formats does the API accept?

The API accepts PDF files. Upload via multipart form data or raw binary with Content-Type: application/pdf.

How does pricing work?

1 credit per page of bank statement processed. Check your balance anytime via GET /v1/credits.

What output formats are supported?

JSON (default), CSV, XLSX, QuickBooks (QBO), and Xero. CSV and XLSX open directly in Google Sheets. Specify the format in the export endpoint path.

How do I handle password-protected PDFs?

Upload the PDF normally, then use POST /v1/documents/:id/unlock with the password to decrypt and process it.

Does the API support webhooks?

Yes. Pass a webhook_url when uploading and receive POST callbacks for document.completed and document.failed events with HMAC signature verification.

Is there an OpenAPI spec?

Yes. The OpenAPI 3.1 specification is available at /v1/openapi.json for code generation and API client integration.