BBankConnectorDeveloper Docs

Submitting Payments

Audience: ๐Ÿ”Œ Integration Client

This is the core of the integration. One endpoint does the work:

POST /journal/payments

Request

{
  "platformId": "<platformId>",   // scope (required)
  "companyId":  "<companyId>",    // scope (required)
  "bankKey":    "abn-amro-nl",    // which bank to target (required)
  "payment":    { /* canonical payment instruction โ€” see Core Concepts */ },

  // optional:
  "environment": "test",          // "test" | "production" โ€” overrides connection default
  "validate":    true,            // also XSD-validate the generated XML
  "maker":       "erp-service",   // maker identity, for maker-checker approval flows
  "idempotencyKey": "..."         // alternative to the Idempotency-Key header
}

Send the idempotency value as the Idempotency-Key header (preferred) or the body field. The header wins if both are present. See Idempotency โ€” for host-to-host banks it is mandatory.

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":       "converted",          // or "pending-approval", etc.
  "xml":          "<Document>...</Document>",   // the generated pain.001
  "deliveryMode": "host-to-host",               // or "file-return"
  "autoSelected": { ... },   // present if the payment type was inferred
  "autoFilled":   { ... },   // present if bank settings filled missing fields
  "approval":     { ... },   // present if an approval policy applies
  "delivery":     { ... }    // delivery planning info
}

There is no Location header and no "document" wrapper โ€” the created resource's own fields are spread directly into the response body.

Remember: 201 = accepted + converted. status tells you where it went next โ€” typically pending-approval (if a policy applies) or converted / queued-for-delivery.

The two branches after submission

A. No approval policy

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 human action with 2FA step-up โ€” your API key cannot approve. The flow:

  1. You submit โ†’ 201, status pending-approval, and an approval.needed event fires.
  2. Approvers (humans) approve in the UI, or via POST /approvals/<id>/approve with a TOTP code. The default policy requires two approvals and forbids self-approval.
  3. On the final approval, delivery is queued and you get approval.completed followed by the normal delivery events.
  4. A rejection (POST /approvals/<id>/reject) stops it; you get approval.rejected.

If your company uses maker-checker, design your system to submit and then wait โ€” don't expect a submitted payment to send on its own.

Preview before you submit

POST /journal/payments/preview runs the exact same validation and conversion with no journal write and no delivery. Use it to validate user input in your UI, or as a dry run in a pipeline. Same request shape (minus idempotency); returns 200 with the xml, or 422 with issues.

Batching

One instruction can carry up to 1000 payments, each with up to 10000 transactions. But the HTTP body is capped at 1 MB, so in practice large batches hit the size cap first. If you're sending high volumes, prefer more requests with smaller batches over a single giant one, and remember the rate-limit note below.

Worked example (TypeScript)

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}/journal/payments`, {
    method: "POST",
    headers: {
      "X-API-Key": KEY,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      platformId: process.env.BC_PLATFORM,
      companyId:  process.env.BC_COMPANY,
      bankKey:    "abn-amro-nl",
      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 };
  }

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

  if (res.status === 422) {
    const body = await res.json();          // { error: { code, message, details: { issues } } }
    return { ok: false, issues: body.error.details?.issues, requestId };
  }

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

Prerequisites for real delivery

A host-to-host payment requires an active host-to-host connection to exist first. If there isn't one, submission fails 422 โ€” there is no automatic fall-back to file-return. Setting up that connection is a one-time human/UI step (your API key can't do it). See Going Live.