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:
- You submit โ
201, statuspending-approval, and anapproval.neededevent fires. - Approvers (humans) approve in the UI, or via
POST /approvals/<id>/approvewith a TOTP code. The default policy requires two approvals and forbids self-approval. - On the final approval, delivery is queued and you get
approval.completedfollowed by the normal delivery events. - A rejection (
POST /approvals/<id>/reject) stops it; you getapproval.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.