sandbox.bankconnector.com API Reference

Submitting payments

One endpoint submits a payment:

POST /api/journal/payments

Request

{
  "companyId":  "<companyId>",    // scope (required): the API key's own company
  "bankKey":    "danske-dk",      // which bank to target (required)
  "payment":    { /* canonical payment instruction: see the Payment field reference */ },

  // optional:
  "environment": "test",          // which of THIS bank's connections to use, see below (default: "production")
  "maker":       { "email": "anna@example.com" }, // who prepared the payment, see below
  "source":      { "reference": "PAY-0031", "name": "Payments 2026-09-28" }, // the ERP journal this came from (label only)
  "erpNotes":    [ /* facts your ERP records for an auditor, see below */ ],
  "idempotencyKey": "..."         // alternative to the Idempotency-Key header
}

source names the journal in your ERP that the payment came from (its reference and its name, each optional and at most 140 characters). The Payments list and the approval screen show it next to the payment's id, so an approver can tell which journal they are approving. It is a label only: it is not part of the payment and not part of the idempotency check, so re-sending the same payment after the journal was renamed replays the original answer rather than answering 409.

erpNotes records what your ERP showed the maker about the batch. The one kind today is a payee whose bank account changed since their last payment:

"erpNotes": [{
  "kind": "payee-account-changed",
  "paymentRef": "PAY-0031-3",            // which payment in the batch
  "from": "DK5000400440116243",          // the account their last payment went to
  "to": "DK5000400440119999",            // the account this batch pays
  "shownTo": "Frederik Larsen",          // who your ERP showed the warning to
  "shownAt": "2026-10-01T09:12:00+02:00" // when, with Z or an offset
}]

Each note goes into the payment's tamper-evident audit trail, without the accounts, and onto its approval request, and the approval screen shows it above Approve. The approval request keeps the accounts masked to their first two and last four characters. At most 500 notes; each text field 1–140 characters with no line breaks or other control characters. Notes are not part of the payment: they never reach the bank file, and a retry with different notes replays the original answer.

maker names who prepared the payment, for maker-checker. maker.reference counts only when it is the id of a BankConnector user in this company, and then that user is excluded from approving it when maker-checker is on. Any other reference, and name, are display labels, and email is recorded for the audit trail as sent and never checked.

Every call also sends BankConnector-Version: 2026-10-21; see API versioning & compatibility. Without it a call runs on 2026-06-01, where a malformed payment answers 400 instead of 422.

Send the idempotency value as the Idempotency-Key header (preferred) or the body field. The header wins if both are present. See Idempotency: on host-to-host the payment's messageId is its identity whether or not you send a key, and a submission with neither is refused with 400 idempotency_key_required.

What environment does

A bank connection is set up as either a test or a production connection. A test connection points at the bank's own test/UAT endpoint, a production one at the live endpoint. environment on the payment picks which of your connections for this bank to send over (default production). It does not change which BankConnector server you're talking to.

A sandbox company's connection is a test one, to the simulated demo bank (danske-dk unless you pick another under Banks after signing up). Send "environment": "test" there: left out, it means production, and the sandbox answers 422 with "No active integration". The sandbox can never send to a bank's production system.

Response: 201 Created

The journal document's own fields (id, journalNo, messageId, status, …) come back at the top level, alongside the conversion output:

{
  "id":           "...",
  "journalNo":    "OUT-000123",
  "messageId":    "ORDER-2026-10432",
  "status":       "queued-for-delivery", // released to delivery; "pending-approval" if a policy holds it
  "xml":          "<Document>...</Document>",   // the generated bank file, for your records
  "deliveryMode": "host-to-host",               // always host-to-host: BankConnector delivers it for you
  "autoSelected": [ ... ],   // present if the payment type was inferred
  "autoFilled":   [ ... ],   // present if bank settings filled missing fields
  "unknownFieldsDropped": [ ... ], // present if the payment carried fields the engine does not read
  "warnings":     [ ... ],   // present when the payment was accepted with advisories
  "approval":     { ... },   // present when the payment is held for approval
  "delivery":     { ... }    // delivery planning info
}

There is no Location header and no "document" wrapper. A 201 means accepted and converted, not yet at the bank. status tells you where it went next: pending-approval when its approval policy holds it for a User approver, or queued-for-delivery when it was released straight to delivery because no approval was needed.

The two branches after submission

Every payment is governed by an approval policy: the one your company has set for that bank, or its company default. Until your company locks a policy of its own, the built-in default applies, which requires two distinct approvers on every payment, so the company needs two people with the Approver role. With fewer, the payment waits and an approval.quorum-unreachable event fires; with none, the submission is refused 422. A policy sets two separate controls:

  • Approval required (four eyes): whether a payment needs approving before it goes to the bank, and by how many distinct approvers. A policy can require approval on every payment, or only above an amount threshold. The maker may be one of the approvers. A payment takes exactly the number of approvers its policy requires: the default policy requires two, and a policy that requires one releases a payment on that one signature, the maker's own included unless maker-checker is on.
  • Maker-checker: whether the user who created the payment (the maker) is excluded from approving it. It is off in the default policy; turn it on per policy, or for every policy at once in your company's settings.

The first control decides which branch below a payment takes. On the sandbox approvals are skipped while your company has no active approval policy, so every payment there takes branch A; once a policy is active, its rule decides, as in production. A payment sent over a test connection to the bank's own test server is released without approval too.

A. No approval needed

The policy that governs the payment does not require approval for it (for example, the amount is within the policy's threshold), or you are on the sandbox. The payment is queued for delivery immediately. Watch for payment.queued → payment.sent, then bank acknowledgement moves it to validated / executed (or rejected).

B. An approval policy applies

The payment lands in pending-approval and will not go to the bank until it's fully approved. Approval is a user action; your API key cannot approve. The flow:

  1. You submit → 201, status pending-approval, and an approval.needed event fires.
  2. Approvers (Users) approve in the UI, or programmatically via POST /api/approvals/<id>/approve. This call authenticates with the approver's session token (X-Session-Token), _not_ an API key, and takes the company in the body: { "companyId": "<companyId>" }. An approver who has set up 2FA, or whose company requires it, adds their current authenticator code as "code": "123456" (or "backupCode": "…"). The default policy requires two distinct approvers, and the maker may be one of them.
  3. On the final approval, delivery is queued and you get approval.completed followed by the normal delivery events.
  4. A rejection (POST /api/approvals/<id>/reject) stops it for good: you get approval.rejected, and the payment's status becomes rejected-by-approver, a final status. It is never sent and cannot be approved later. To pay it anyway, submit a new payment with a new messageId; re-sending the vetoed messageId is refused with 409 message_id_already_used.

Under the default policy every payment lands in pending-approval. Handle both statuses, because a policy with an amount threshold releases payments within it straight to delivery.

Preview before you submit

POST /api/journal/payments/preview runs the same validation and conversion with no journal write and no delivery. Use it to validate user input or as a dry run. It takes only companyId, bankKey and payment: on BankConnector-Version: 2026-10-21 any other field (environment, maker, idempotencyKey…) is refused 422. It returns 200 with the xml, or 422 with issues.

The 200 also says whether submitting would wait for approval: approval.required is true when the payment would land in pending-approval, false when it would go straight to the bank. It is the same decision the submit makes, so you can skip asking your user "send this?" when approval is the gate anyway.

Batching

One instruction can carry up to 1000 payments, each with up to 10000 transactions. The HTTP body is capped at 1 MB, so large batches hit the size cap first. For high volumes, send more requests with smaller batches, within the rate limits.

Worked example (TypeScript)

This runs against the sandbox you signed up for in Getting started: BC_KEY is the key you created under Company settings › API keys, BC_COMPANY is your company id, and payment can be the payment you saved in step 2 (a payment's messageId is single-use, so change it for each new payment).

const BASE = "https://sandbox.bankconnector.com";
const KEY  = process.env.BC_KEY!;              // key_...

async function submitPayment(idempotencyKey: string, payment: unknown) {
  const res = await fetch(`${BASE}/api/journal/payments`, {
    method: "POST",
    headers: {
      "X-API-Key": KEY,
      "Content-Type": "application/json",
      "BankConnector-Version": "2026-10-21",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      companyId:   process.env.BC_COMPANY,  // the key's own company
      bankKey:     "danske-dk",             // the sandbox's demo bank
      environment: "test",                  // the sandbox connection is a test one
      payment,
    }),
  });

  const requestId = res.headers.get("x-request-id");   // log this always

  if (res.status === 201) {
    const replayed = res.headers.get("idempotency-replayed") === "true";
    const doc = await res.json();       // { id, journalNo, status, xml, deliveryMode, ... } flat, no wrapper
    return { ok: true, replayed, doc, requestId };
  }

  const body = await res.json();        // { error: { code, message, details? } }

  if (res.status === 409) {
    // Two very different meanings: see Idempotency + Errors chapters.
    return { ok: false, conflict: body.error, requestId };
  }

  if (res.status === 422) {
    return { ok: false, issues: body.error.details?.issues, requestId };
  }

  if (res.status === 400) {
    // No version header on an older pin, a missing companyId or Idempotency-Key, or a malformed body.
    return { ok: false, rejected: body.error, requestId };
  }

  if (res.status === 402) {
    // Production connections only: body.error.code is "trial_payments_exhausted" or "billing_past_due".
    // Fix the account's billing; retrying the same request will not help.
    return { ok: false, billing: body.error, requestId };
  }

  throw new Error(`Unexpected ${res.status} (request ${requestId})`);
}

Prerequisites for real delivery

A payment needs an active connection to the bank. BankConnector delivers the payment over that connection and never hands you a file to upload, so without a connection, submission fails 422. A user sets up the connection once in the UI (your API key can't). See Going live.