Skip to content

Bank statement API

Convert CAMT.053 and MT940 bank statements to CSV, XLSX, MT940 or CAMT.053 by REST API. A second request may be needed when information is missing.

This capability is in beta: it works and is tested, and we may still change details.

On this page

The Klarbon bank statement API converts CAMT.053 and MT940 bank exports into CSV, XLSX, MT940 or CAMT.053 through REST. It reads the statement, returns the converted file with a preview of the account, period, balances and first transactions, and charges only when the conversion succeeds. Files are processed for the request, without a document archive.

What the API does

  • POST /api/v1/bank/convert with target set to bank-csv, xlsx, mt940 or camt-053.
  • A file can produce several statement files. If a choice or missing value is needed, the answer has status: "need"; repeat with the same file, the answer and resume. A need response is free; a completed conversion is charged.
  • MT940 output requires statementNumber in the request.
  • The CSV uses a fixed column set. Match those columns to your importer and check its date and decimal settings.

What it does not do

It does not read PDF statements yet, and it does not connect to banks. Reading CAMT.053 and MT940 is in beta. There is no JSON output target: the preview in the answer is a summary, not the full statement.

Quick start

Each snippet is the same call. The end-to-end test runs these snippets against a local Klarbon server and the real validation engine.

Create a key on your account page and put it in KLARBON_API_KEY. Never put it in the address.

POST /api/v1/bank/convert

curl + jq
curl -sS https://klarbon.app/api/v1/bank/convert \
  -H "Authorization: Bearer $KLARBON_API_KEY" \
  -H "Idempotency-Key: statement-2026-11-04-csv" \
  -F "file=@statement.xml;type=application/xml" \
  -F target=bank-csv \
  -o result.json
jq -e '.ok' result.json > /dev/null || { jq -r '.error + ": " + .message' result.json; exit 1; }
jq -e '.status == "converted"' result.json > /dev/null || { jq -r '"Klarbon needs " + .need' result.json; exit 1; }
jq -r '.files[0].content' result.json | base64 --decode > "$(jq -r '.files[0].filename' result.json)"
TypeScript, Node.js 22.18+
import { readFile, writeFile } from "node:fs/promises"

type KlarbonFile = { content: string; filename: string; mime: string; bytes: number }
type Failure = { ok: false; error: string; message: string }
type Converted = { ok: true; status: "converted"; files: KlarbonFile[]; credits: { spent: number; balance: number } }
type Need = { ok: true; status: "need"; need: string; resume: string }

const form = new FormData()
form.append("file", new Blob([await readFile("statement.xml")], { type: "application/xml" }), "statement.xml")
form.append("target", "bank-csv")

const response = await fetch("https://klarbon.app/api/v1/bank/convert", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KLARBON_API_KEY}`,
    "Idempotency-Key": "statement-2026-11-04-csv",
  },
  body: form,
})
const result = (await response.json()) as Converted | Need | Failure
if (!result.ok) throw new Error(`${response.status} ${result.error}: ${result.message}`)
if (result.status === "need") throw new Error(`Klarbon needs ${result.need}: send it with resume`)
for (const file of result.files) await writeFile(file.filename, Buffer.from(file.content, "base64"))
console.log(`${result.files.length} file(s) written, ${result.credits.balance} credits left`)
Python 3.9+, requests
import base64
import os

import requests

with open("statement.xml", "rb") as upload:
    response = requests.post(
        "https://klarbon.app/api/v1/bank/convert",
        headers={
            "Authorization": f"Bearer {os.environ['KLARBON_API_KEY']}",
            "Idempotency-Key": "statement-2026-11-04-csv",
        },
        files={"file": ("statement.xml", upload, "application/xml")},
        data={"target": "bank-csv"},
        timeout=120,
    )
result = response.json()
if not result["ok"]:
    raise SystemExit(f"{response.status_code} {result['error']}: {result['message']}")
if result["status"] == "need":
    raise SystemExit(f"Klarbon needs {result['need']}: send it with resume")
for written in result["files"]:
    with open(written["filename"], "wb") as output:
        output.write(base64.b64decode(written["content"]))
print(len(result["files"]), "file(s) written,", result["credits"]["balance"], "credits left")
PHP 8+, curl
<?php
$curl = curl_init('https://klarbon.app/api/v1/bank/convert');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('KLARBON_API_KEY'),
        'Idempotency-Key: statement-2026-11-04-csv',
    ],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('statement.xml', 'application/xml', 'statement.xml'),
        'target' => 'bank-csv',
    ],
]);
$body = curl_exec($curl);
if ($body === false) {
    fwrite(STDERR, curl_error($curl) . "\n");
    exit(1);
}
$result = json_decode($body, true);
if (!$result['ok']) {
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    fwrite(STDERR, "$status {$result['error']}: {$result['message']}\n");
    exit(1);
}
if ($result['status'] === 'need') {
    fwrite(STDERR, "Klarbon needs {$result['need']}: send it with resume\n");
    exit(1);
}
foreach ($result['files'] as $written) {
    file_put_contents($written['filename'], base64_decode($written['content']));
}
echo count($result['files']) . " file(s) written, {$result['credits']['balance']} credits left\n";
C#, .NET 10
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient();
var key = Environment.GetEnvironmentVariable("KLARBON_API_KEY");
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);

var upload = new ByteArrayContent(await File.ReadAllBytesAsync("statement.xml"));
upload.Headers.ContentType = new MediaTypeHeaderValue("application/xml");

using var request = new HttpRequestMessage(HttpMethod.Post, "https://klarbon.app/api/v1/bank/convert?target=bank-csv") { Content = upload };
request.Headers.Add("Idempotency-Key", "statement-2026-11-04-csv");
using var response = await http.SendAsync(request);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var result = json.RootElement;
if (!result.GetProperty("ok").GetBoolean())
{
    Console.Error.WriteLine($"{(int)response.StatusCode} {result.GetProperty("error")}: {result.GetProperty("message")}");
    return 1;
}
if (result.GetProperty("status").GetString() == "need")
{
    Console.Error.WriteLine($"Klarbon needs {result.GetProperty("need")}: send it with resume");
    return 1;
}
foreach (var written in result.GetProperty("files").EnumerateArray())
{
    await File.WriteAllBytesAsync(written.GetProperty("filename").GetString()!,
        Convert.FromBase64String(written.GetProperty("content").GetString()!));
}
Console.WriteLine($"{result.GetProperty("files").GetArrayLength()} file(s) written, {result.GetProperty("credits").GetProperty("balance")} credits left");
return 0;

What comes back

The answer to the curl call above, recorded from that run with Klarbon synthetic fixture, CC0-1.0.

Shortened for the page: files[0].content: base64 cut

{
  "ok": true,
  "status": "converted",
  "format": "camt-053",
  "files": [
    {
      "content": "77u/IkJvb2tpbmcgZGF0ZSI7IlZhbHVlIGRhdGUiOyJBbW91...",
      "filename": "statement.csv",
      "mime": "text/csv;charset=utf-8",
      "bytes": 300
    }
  ],
  "preview": {
    "bank": null,
    "accountHolder": null,
    "iban": "DE89370400440532013000",
    "currency": "EUR",
    "period": {
      "from": "2026-11-02",
      "to": "2026-11-04"
    },
    "openingBalance": "1000.00",
    "closingBalance": "1075.00",
    "transactionCount": 2,
    "firstTransactions": [
      {
        "bookingDate": "2026-11-02",
        "valueDate": "2026-11-02",
        "amount": "100.00",
        "description": "Fictional customer payment",
        "reference": "TEST-1"
      },
      {
        "bookingDate": "2026-11-03",
        "valueDate": "2026-11-03",
        "amount": "-25.00",
        "description": "Fictional office supplies",
        "reference": "TEST-2"
      }
    ],
    "reconciliation": {
      "ok": true,
      "difference": "0.00",
      "checkedBalances": 1
    },
    "balanceSource": {
      "opening": "printed",
      "closing": "printed"
    }
  },
  "warnings": [],
  "credits": {
    "spent": 1,
    "balance": 196,
    "plan": 0,
    "bought": 196
  }
}

What it costs

Prepaid API packs, bought by card on your account page: 1,000 calls for €39.00, 5,000 calls for €129.00, 25,000 calls for €449.00, 100,000 calls for €990.00, VAT included, never expiring. Your first key brings 25 free calls once per email address, excluding disposable addresses. Paid calls spend the API balance first, then the same credits as the site. A call that fails costs nothing.

Call Cost
POST /open Free, fair use 1,000 a day per account
POST /check Free within your plan's daily checks (20 a day on the free plan)
POST /convert, POST /make 1 call, only when the written file validates
POST /bank/convert 1 call per started 500 transactions, only when converted

Limits

  • Files up to 20 MB, PDFs up to 50 pages.
  • 60 requests a minute per key, 120 per account. A per-minute rate-limit refusal carries Retry-After.
  • Up to 10 keys per account. Idempotency-Key is honoured for 24 hours, so a retry never charges twice.

Errors

Every error is { ok: false, error, message }: a stable code and one plain sentence.

Status Code When
401 auth No key, or an unknown or revoked key.
402 no_credits The combined API balance and credits cannot cover this call. Nothing was charged; topUpUrl says where to buy.
413 too_large Over 20 MB or 50 PDF pages.
415 unsupported A file format the route does not take.
422 invalid The file could not be read, or the written file did not validate. Not charged.
429 limit A rate limit or the day's free calls.
503 engine_unavailable Validation is briefly unavailable. Not charged.

Privacy

Files are processed for the request, without a document archive. Klarbon's application logs carry no file name, no content and no key. The full key is not stored, only an HMAC of it and a short display prefix.

Next steps

Frequently asked questions

Which statement formats does the API read?

CAMT.053 and MT940, with other structured bank formats in beta. PDF statements are planned and not offered yet.

Which formats can it write?

CSV, XLSX, MT940 and CAMT.053, plus supported beta outputs. The target parameter selects one format per call; the answer can contain several statement files.

What if the file holds several statements?

It can return a file for each statement. If it needs an account choice or a missing field, the response has status need. Repeat with the same file, the answer and resume. A need response is free; a completed conversion is charged.

Does it connect to my bank?

No. You send a file your bank already exported. Klarbon never asks for bank credentials.

Sources

Updated 6 November 2026