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 =
platformId+companyId. You pass it on essentially every request (in the body for POSTs, as query params for GETs). Missing scope โ400 scope_required.
As an integration client you operate as one company. Your API key fixes the platform; you name the company in each call.
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:
- validates it (structure, then generic rules, then bank rules, then payment-type rules),
- applies your saved bank settings (charge bearer, execution-date offset, etc.),
- converts it to the correct
pain.001flavour for the target bank, - 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).
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.
amountis a STRING, not a number:"1250.00". Format^\d{1,15}\.\d{2,3}$(2โ3 decimal places). Precision must match the currency's ISO 4217 minor unit โ JPY and ISK take 0 decimals ("1000.50"is rejected), most currencies 2, KWD/BHD 3."0.00"parses but fails the positive-amount rule.creationDateTime, if you send it, must be UTC with aZ. Offsets like+02:00are rejected. Omit it and the server stamps "now."executionDateis 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.accountis exactly one ofibanXORother. Providing both, or neither, is a validation error. IBANs are checked three ways: format, mod-97 checksum, and exact per-country length.messageIdis 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.namefields 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
converted โโฌโโถ pending-approval โโถ queued-for-delivery โโถ sending โโถ sent
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโถ queued-for-delivery โผ
validated / executed
(any stage) โโถ sanctions-hold partially-accepted
โโถ delivery-failed rejected
cancelled
- Terminal statuses are
executed,rejected,cancelledโ sticky. A late or replayed bank message cannot flip them back. sanctions-holdmeans screening paused the payment for review โ this is visible via polling and the audit log, but is not one of the fanned-out webhook events (see Receiving Results).- An ambiguous send stays in
sendingand is not auto-retried (it's the money path); it surfaces as an SLA breach for a human to 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/listis the designated "ERP poll," with anunretrievedOnlyflag so you can drain only what you haven't seen. Best for simplicity and for batch/back-office systems.
Most robust integrations use webhooks for latency and a periodic poll as a safety net.
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 key | Canonical key |
|---|---|
jpmorgan | jpmorgan-us |
danske-bank | danske-dk |
deutsche-bank | deutsche-bank-de |