sandbox.bankconnector.com API Reference

Core Concepts

Audience: 🔌 Integration Client · 🏗️ Platform Operator

Five ideas explain almost everything about integrating with BankConnector.

1. The tenancy model

Platform → Company → (bank connections, accounts, payments, webhooks)
  • A platform is the operating tenant (BankConnector the vendor, for most customers; see below). 🏗️
  • A company is who actually pays and gets paid. Every payment, account, connection, and webhook belongs to a company.
  • Scope = platform + companyId. Your API key implies the platform, so you never pass platformId on a data-plane call; you pass only companyId (in the body for POSTs, as a query param for GETs). Missing it → 400 scope_required.

As an integration client you operate as one company at a time. Your API key fixes the platform; you name the company in each call. (The platformId still exists, and you'll see it on control-plane routes like creating companies, but data-plane payment calls don't take it.)

2. You send canonical JSON, not XML

You never author ISO 20022 XML. You send a canonical payment instruction (a clean, bank-agnostic JSON object) and BankConnector's engine:

  1. validates it (structure, then generic rules, then bank rules, then payment-type rules),
  2. applies your saved bank settings (charge bearer, execution-date offset, etc.),
  3. converts it to the correct pain.001 flavour for the target bank,
  4. queues it for asynchronous delivery.

The same canonical object works across banks; the engine handles the per-bank dialect.

3. The canonical payment instruction

One instruction contains one or more payments (each becomes a PmtInf), and each payment contains one or more transactions (each becomes a CdtTrfTxInf). The tree below is the shape; the Payment field reference is the full required-vs-optional table for every field.

PaymentInstruction
├── messageId                 (required, ≤35 chars, your dedup handle)
├── initiatingParty { name, organisationId? }
└── payments[]  (1..1000)
    ├── paymentId              (required, ≤35)
    ├── paymentType            ("sepa", "domestic", "international", ...)
    ├── executionDate          (required, "YYYY-MM-DD", a real calendar date)
    ├── debtor  { name, country?, account, agent? }
    └── transactions[]  (1..10000)
        ├── endToEndId         (required, ≤35)
        ├── amount             (required, STRING, see below)
        ├── currency           (required, ISO 4217)
        ├── creditor { name, account, agent? }
        └── remittance?        (structured or unstructured)

A complete, valid example

{
  "messageId": "ORDER-2026-10432",
  "initiatingParty": {
    "name": "Acme Holding",
    "organisationId": { "id": "NL-1", "scheme": "CUST" }
  },
  "payments": [
    {
      "paymentId": "p1",
      "paymentType": "sepa",
      "executionDate": "2026-07-15",
      "debtor": {
        "name": "Acme Holding",
        "country": "NL",
        "account": { "iban": "NL91ABNA0417164300", "currency": "EUR" },
        "agent": { "bic": "ABNANL2A" }
      },
      "transactions": [
        {
          "endToEndId": "e1",
          "amount": "1250.00",
          "currency": "EUR",
          "creditor": {
            "name": "Supplier BV",
            "account": { "iban": "FR1420041010050500013M02606" },
            "agent": { "bic": "BNPAFRPP" }
          },
          "remittance": {
            "creditorReference": { "type": "scor", "value": "RF18539007547034" }
          }
        }
      ]
    }
  ]
}

Field rules that trip people up

These are the ones worth reading twice. The full catalog is in Validation Reference.

  • amount is a STRING, not a number: "1250.00". Format ^\d{1,15}\.\d{2,3}$ (2–3 decimal places). You do not tailor decimals per currency: send the ordinary two-decimal form for everything and the engine emits each currency's correct ISO 20022 precision (JPY "1000.00"1000; KWD keeps its third place). Trailing zeros are always fine. The one thing we reject is an amount the currency cannot represent: a non-zero sub-unit, e.g. "1000.50" JPY (there is no half-yen). We refuse rather than silently truncate it, because truncating would change the amount you're paying. "0.00" parses but fails the positive-amount rule.
  • creationDateTime, if you send it, must be UTC with a Z. Offsets like +02:00 are rejected. Omit it and the server stamps "now."
  • executionDate is warn-only for the past, weekends, and holidays: it is never silently shifted, but those don't block. An impossible date (2026-02-30) is a hard schema error.
  • account is exactly one of iban XOR other. Providing both, or neither, is a validation error. IBANs are checked three ways: format, mod-97 checksum, and exact per-country length.
  • messageId is required and is your deduplication handle: the bank de-duplicates on it within roughly a 90-day window, and BankConnector uses it as a fallback idempotency key. Make it stable and unique per logical payment. See Idempotency.
  • name fields allow up to 140 characters, but SEPA banks process 70. Text is transliterated to the SEPA character set then truncated, and transliteration can lengthen text (ßss). A name in a script with no Latin mapping is a hard error, not a silent blank.

4. Payments are accepted synchronously, delivered asynchronously

POST /journal/payments does validation, conversion, and approval-planning synchronously and returns 201 with the converted document and its XML. But the send to the bank is always asynchronous, via a transactional outbox and a delivery worker. The bank's acknowledgement (pain.002) arrives later still.

So a 201 tells you the payment is well-formed and accepted. It does not tell you the bank received or executed it. You observe that through the payment lifecycle, via webhooks or polling.

The payment lifecycle

POST /journal/payments
   │  (validated → converted to pain.001 → recorded)
   ▼
pending-approval ──▶ queued-for-delivery ──▶ sending ──▶ sent ──▶ validated / executed
   │  no policy: auto-released straight through            │       partially-accepted
   │  policy: held here until fully approved               │       rejected
   │                                                       ▼       cancelled
   └───────────────────────────────────────────▶ (any stage) ──▶ delivery-failed
  • A payment is recorded at pending-approval. With no approval policy (or a platform that auto-releases) it moves straight to queued-for-delivery; with a policy it waits at pending-approval until approved. (payment.converted is the webhook event that fires the moment conversion completes. Don't confuse it with a status; you won't see a converted _status_.)
  • Terminal statuses are executed, rejected, cancelled, and sticky. A late or replayed bank message cannot flip them back.
  • BankConnector performs no sanctions/AML screening. The duty to screen sits with you (the payment originator) and the executing bank; a payment is never held, delayed, or blocked by BankConnector on sanctions grounds. (Customers who have separately contracted the opt-in blocking-screening feature may additionally see a sanctions-hold status; it does not exist otherwise.)
  • If a send is ambiguous (the connection dropped and we can't be sure the bank received the file), the payment stays in sending and is not auto-retried, because a blind retry risks paying the beneficiary twice. It's surfaced for a User to confirm with the bank and resolve.

5. You learn results by webhooks or polling

Two supported patterns, covered in Receiving Results:

  • Webhooks: BankConnector POSTs signed events to your endpoint as things happen. Best for timeliness.
  • Polling: GET /journal/list is the designated "ERP poll," with an unretrievedOnly flag so you can drain only what you haven't seen. Best for simplicity and for batch/back-office systems.

Most production integrations use webhooks for latency and a periodic poll as a safety net.

Pagination

Every list endpoint returns the same envelope:

{ "items": [ ... ], "nextCursor": "…", "total": 123 }
  • Pass ?limit= to page; nextCursor (non-null ⇒ more rows) is echoed back, and you pass it as ?cursor= on the next request to walk the rest. When nextCursor is null, you've reached the end.
  • A malformed cursor returns 400 invalid_cursor.
  • Defaults & caps vary by endpoint: /journal/list default 500, cap 1000; /journal/documents default 50, cap 200; /journal/events cap 5000; /journal/reconciliation default 500, cap 1000.

An ERP draining a backlog larger than the default must follow nextCursor rather than assume one response holds everything; total tells you if there's more.

Bank keys

A bankKey is <bank>-<country-code>: the country code is part of the key so the same bank in different countries resolves to its own profile (e.g. jpmorgan-us, and other JPMorgan country flavours), each with its own BIC, pain version, payment types, and rules. This convention is enforced at startup.

BankConnector's bank registry covers hundreds of banks across dozens of countries and grows continuously; the authoritative, current list is always GET /profiles, not a number written down here.

Backward-compatible aliases. A few keys were historically bare (no country suffix); they still resolve to their canonical form, so existing integrations keep working:

Legacy keyCanonical key
jpmorganjpmorgan-us
danske-bankdanske-dk
deutsche-bankdeutsche-bank-de