sandbox.bankconnector.com API Reference

Core concepts

Seven short sections cover integrating with BankConnector.

1. The tenancy model

Company → (bank connections, accounts, payments, webhooks, users, API keys)
  • A company is the tenant: the organisation that holds the bank agreement and pays and gets paid. Every payment, account, connection and webhook belongs to a company, and companies are isolated from each other.
  • Your API key is bound to one company. You pass that companyId (in the body for POSTs, as a query param for GETs); missing it → 400 scope_required, and any other company's id → 403 forbidden.

A company comes into being when its own people sign up; see Managing companies.

2. You send canonical JSON, not XML

You never write ISO 20022 XML. You send a canonical payment instruction (a bank-agnostic JSON object) and BankConnector:

  1. fills fields you left out from your saved bank settings (charge bearer, execution-date offset, etc.); anything you send wins,
  2. validates it (structure, then generic rules, then bank rules, then payment-type rules),
  3. converts it to the correct pain.001 flavour for the target bank,
  4. queues it for asynchronous delivery.

3. The canonical payment instruction

One instruction contains one or more payments (each a group that 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 has the required-vs-optional detail for each 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

The Validation reference lists the validation issue codes.

  • amount is a string, not a number: "1250.00". Format ^\d{1,15}\.\d{2}$ (exactly two 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). 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. Two exceptions are refused, because the bank rejects the file outright: a non-banking day at a bank on the Danish BEC data centre, and a Finnish salary on a weekend or Finnish bank holiday. Both errors name the banking day to use. 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 payment's identity: the bank de-duplicates on it within roughly a 90-day window, and BankConnector treats one messageId as one payment, whatever Idempotency-Key you send. Make it stable and unique per logical payment, and reuse it only to retry that same payment. See Idempotency.
  • name fields allow up to 140 characters, but SEPA banks process 70. Text is transliterated to the bank's character set for the rail then truncated, and transliteration can lengthen text (ß→ss). A German SEPA file keeps umlauts, ß and & as you wrote them. 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 /api/journal/payments validates, converts and plans approval synchronously and returns 201 with the converted document and its XML. The send to the bank is always asynchronous. The bank's acknowledgement (pain.002) arrives later still.

The status belongs to the whole instruction you submitted (one messageId), not to the individual payments or transactions inside it.

A 201 tells you the payment is well-formed and accepted, not that the bank received or executed it. Follow the payment lifecycle through webhooks or polling.

The payment lifecycle

POST /api/journal/payments
   │  (validated → converted to pain.001 → recorded)
   ▼
pending-approval ──▶ queued-for-delivery ──▶ sending ──▶ sent ──▶ validated ──▶ executed
   │  no approval needed: released straight through              │  partially-accepted
   │  approval needed: held here until fully approved            └▶ rejected / cancelled
   │
   ├──▶ delivery-failed   (from pending-approval, queued-for-delivery or sending only)
   │
   └──▶ rejected-by-approver   (an approver vetoed it; final, never sent)

A payment the bank holds for your own signers after checking the payee takes one more step between sending it and the bank's verdict: sending / sent ──▶ awaiting-signer-release ──▶ validated / executed / rejected / cancelled.

  • A payment is recorded at pending-approval. Every payment is governed by an approval policy, and that policy decides whether this payment needs approving. If it does not, the payment moves straight to queued-for-delivery; if it does, it waits at pending-approval until approved. The built-in default policy requires approval on every payment; on the sandbox approvals are skipped while your company has no active approval policy for the bank. See Submitting payments. (payment.converted is a webhook event, not a status.)
  • Terminal statuses are executed, rejected, cancelled and rejected-by-approver, and sticky. A late or replayed bank message cannot flip them back.
  • rejected and rejected-by-approver are different outcomes. rejected is the bank's verdict on a file it received. rejected-by-approver means one of your own approvers vetoed the payment before it was sent: it never reached the bank and never will. To pay it anyway, submit a new payment with a new messageId.
  • validated and partially-accepted are not final: the bank's later reports move them on.
  • awaiting-signer-release means the bank holds the payment until your own signers release or cancel it there. It is not final. See the event catalog.
  • 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. Check with your bank whether it received the file, then contact support: only BankConnector staff can resolve a payment stuck in sending.

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 /api/journal/list, with an uncollectedOnly flag so you can fetch only documents you haven't yet marked collected. Reading does not mark anything: see Polling. 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

List endpoints return this envelope (total appears only on some):

{ "items": [ ... ], "nextCursor": "…", "total": 123 }
  • Pass ?limit= to page; a non-null nextCursor means there are more rows. 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 cap 200, and with no limit it returns up to 1000 (500 when searching); /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; nextCursor 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. danske-dk and danske-se), each with its own BIC, pain version, payment types, and rules.

GET /api/profiles lists every supported bank.