sandbox.bankconnector.com API Reference

Idempotency

A payment sent twice can be paid twice. BankConnector makes retries safe, but only if you reuse the same identity on every retry. Read this before you write retry logic.

The one rule

One messageId is one payment. Give every logical payment its own messageId, and send that same messageId on every retry of it.

On POST /api/journal/payments, the messageId (your pain.001 MsgId) is the payment's duplicate identity, whether or not you also send an Idempotency-Key. A new Idempotency-Key does not turn a messageId the server has already seen into a second payment.

Send a stable Idempotency-Key as well:

Idempotency-Key: 2f9a1c7e-order-10432
  • It's accepted only on POST /api/journal/payments (header, or body idempotencyKey; header wins).
  • It is validated, and echoed back in the Idempotency-Key response header when your payment runs.
  • It is optional: messageId is the identity.

Key format

8–255 characters, printable ASCII, no spaces (^[\x21-\x7e]+$). A UUID or a stable business identifier like order-10432-attempt works. Make it deterministic for a given logical payment so a retry reuses the same key.

Scope and lifetime

  • Uniqueness is per company: (companyId, messageId).
  • A messageId is remembered for 90 days. Within that window, re-sending it returns the original result instead of paying again (see the outcomes) or, when the original payment was rejected, cancelled or rejected-by-approver, refuses the re-send with 409 message_id_already_used.
  • After the 90 days the stored response is forgotten, but the messageId's claim on the payment it created is permanent. Re-sending it later never creates a second payment: the submission is stopped at the point of recording. Documents are purged at 90 days too, so past that the answer can be an error instead of the original payment. Inside the window you get the original response body back verbatim.
  • A replay returns the stored body, so its status is the payment's status at submit time. Read the current state from the journal (see Receiving results).
  • A refused submit (a 422, a 402, or no active connection for the bank) is not stored. Fix the cause and send it again under the same messageId.

The outcomes

When you POST, one of these happens:

OutcomeWhenWhat you get
newfirst time this identity is seenthe payment runs; 201
replaysame identity and identical bodythe stored response, verbatim, plus header Idempotency-Replayed: true
conflictsame identity, different body409 conflict: a different payload under an identity already used
in-progresssame identity, still processing409 request_in_progress: retry the same request shortly
messageId already usedsame messageId, and its original payment is rejected, cancelled or rejected-by-approver (within 90 days)409 message_id_already_used; the re-send is not attempted

While the original payment is in flight or executed, a re-send is a replay (the ordinary timeout retry). Only a rejected, cancelled or rejected-by-approver original is refused. A rejected-by-approver payment never reached the bank, but its messageId is spent all the same.

The three 409s mean different things:

  • 409 request_in_progress: your first attempt is still running. Retry the same request shortly.
  • 409 conflict: you sent a changed payload under a messageId (or key) that was already used. Never retry this as-is. If it is a different payment, it needs its own messageId; a new Idempotency-Key does not help, because the messageId governs.
  • 409 message_id_already_used: the payment you first sent under this messageId was rejected or cancelled, and this re-send was not attempted. Correct the payment and submit it with a new messageId. The error's details names the original:

``json { "messageId": "ORDER-2026-10432", "originalJournalNo": "OUT-000123", "originalStatus": "rejected", "originalIsTerminal": true, "remedy": "new_message_id" } ``

Each of the three has its own code: branch on code, never on the message (see Errors).

When the answer never arrived: look the batch up

If a send ends without an answer (a timeout, a dropped connection), the first choice is to retry the same request: the same messageId and payload replay the original result, or run it once if it never arrived. When you instead need to know whether the payments may be sent again as a new payment, ask:

GET /api/journal/payments/batch?companyId=<companyId>&messageId=ORDER-2026-10432
outcomeWhat it meansMay you send again under a new messageId?
foundBankConnector recorded the batch. payments lists each endToEndId with its reconciliation state.No
purgedIt recorded the batch; the document has since passed retention.No
processingA send of this batch is running, or was interrupted; your next same-batch send finishes or releases it.No: retry the original request
not-foundNothing holds this batch, and no send of it is running.Yes
  • Look up by the messageId, not by your Idempotency-Key.
  • Every outcome is a 200. An unknown batch is never a 404, so a 404 (a proxy, a wrong URL) must never be read as "not found".
  • not-found describes the moment of the read. Ask only once your own send has ended, so a request still on its way is not missed.
  • Treat an outcome you do not recognise as "BankConnector holds it".
  • For found, each payment's recon.state says where its money is, and which states are final: see Which reconciliation states are final.

Why "same body" matters

A replay needs an equivalent body. The server fingerprints the bank, the environment, the payment body and the other submit options. If you change any of those under the same identity, you get the different-body 409. Freeze the payload, its messageId and the key together: generate them when you first decide to send, store them, and reuse them on every retry.

Crash safety

If BankConnector crashes mid-payment, a retry after 5 minutes either replays the committed document (as 201) or, if nothing was recorded, takes back the stale claim and runs the payment normally. The one terminal error is a 500 when a stored response cannot be read back, and that one says to retry with a new messageId.

// 1. When you decide to pay, mint the messageId + key + payload TOGETHER.
const messageId = `ORDER-${orderId}`;           // one payment, one messageId
const idempotencyKey = `order-${orderId}-v1`;   // deterministic, stable
const payload = buildPayment(order, messageId); // freeze this too
persist(orderId, { messageId, idempotencyKey, payload }); // store before sending

// 2. Send. On any transient failure (timeout, 429, 503, 409 request_in_progress),
//    retry with the SAME messageId, key and payload.
// 3. On a 201 with Idempotency-Replayed: true, treat it as success:
//    it already ran; you're just seeing the stored result.
// 4. On 409 conflict, alert; do not retry. It means the payload
//    changed under a messageId or key that was already used.
// 5. On 409 message_id_already_used, the original payment was rejected or
//    cancelled: correct it and submit it as a new payment with a NEW messageId.

Beyond payments

Idempotency-Key is not honoured on other endpoints. For non-payment writes, rely on natural keys where they exist, and look at what a resubmit does before you retry it. POST /api/journal/export is safe to repeat: see Receiving results.