bankstract

Quickstart

Make your first parse in under a minute.

1. Get an API key

Keys look like bsk_live_… (production, billed) or bsk_test_… (free up to 25 parses/owner/month, then a canned sample). Signing up auto-provisions your test key. Manage it from the dashboard: rotate the test key or issue live keys there. Or self-host and issue your own.

2. Send a statement

Post a PDF as multipart/form-data with your key as a Bearer token:

curl -X POST https://bankstract.logickoder.dev/v1/parse \
  -H "Authorization: Bearer bsk_live_xxx" \
  -F "pdf=@statement.pdf"
import requests

resp = requests.post(
    "https://bankstract.logickoder.dev/v1/parse",
    headers={"Authorization": "Bearer bsk_live_xxx"},
    files={"pdf": open("statement.pdf", "rb")},
)
data = resp.json()
print(data["totals"], len(data["transactions"]), "rows")
const form = new FormData()
form.append('pdf', file) // a File or Blob

const resp = await fetch('https://bankstract.logickoder.dev/v1/parse', {
  method: 'POST',
  headers: { Authorization: 'Bearer bsk_live_xxx' },
  body: form,
})
const data = await resp.json()

3. Read the response

{
  "format_version": "fbn-2026-01",
  "metadata": { "bank": "fbn", "account_number_masked": "****1111", "...": "..." },
  "totals": { "credit": "500.00", "debit": "120.00" },
  "reconciliation": { "totals": "passed", "row_wise": "passed", "row_wise_reason": null },
  "transactions": [
    {
      "date": "2026-01-05T09:30:00",
      "narration": "FOO TRANSFER",
      "debit": "0",
      "credit": "500.00",
      "balance": "600.00",
      "reference": "REF1",
      "currency": "NGN",
      "has_time": true
    }
  ]
}

Money is always a decimal string ("500.00"), never a JSON number. Parse it with a decimal type, not a float, so you don't lose precision.

reconciliation reports the two checks run on every parse. totals compares the debit and credit sums to the statement header. row_wise walks the running balance row by row. Each is passed, not_available (the statement lacks the data), or disabled (the bank's balance column can't be checked row by row). When row_wise is disabled, row_wise_reason says why; otherwise it is null. A failed check never returns 200. It returns a 422 ReconciliationError.

has_time is true only when the statement printed a time for that row. When it is false, the time in date is 00:00:00 padding, not data.

Next: Authentication, response formats, and the error envelope.

On this page