Best Practices: Building a Resilient Client
Audience: 🔌 Integration Client
The rest of the guide is how it works. This is how to build against it well. Each item is a concrete, testable rule.
Correctness
- Send an
Idempotency-Keyon every payment, and freeze the key with the payload. Mint both when you decide to pay, store them, reuse both on every retry. This is the single most important rule for a money-moving integration. → Idempotency
- Treat
201as "accepted," not "sent." The bank send is asynchronous. Never mark a payment "paid" in your system on the201. Wait forpayment.sentand then the bank acknowledgement (executed/rejected). → Core Concepts
- Respect terminal statuses.
executed,rejected, andcancelledare final. Don't build logic that expects them to change; late bank messages can't flip them and neither should you.
- Branch on
code, notmessage. Messages are readable prose and may change. Codes are stable. → Errors
- Model your error type on the object envelope (
{ error: { code, message, details? } }) on every non-2xx response; the envelope is universal. → Errors
- Amounts are strings, always two decimals.
"1250.00", not1250or1250.0. You don't adjust decimals per currency; the engine emits each currency's correct precision. It only rejects an amount the currency can't represent (a non-zero sub-unit, e.g."1000.50"JPY) rather than truncate and change what you're paying. → Validation
- Timestamps you send must be UTC
Z. Offsets are rejected. Prefer to omitcreationDateTimeand let the server stamp it.
- Keep
paymentIdandendToEndIdunique within a batch. Duplicates are hard errors because banks silently de-duplicate on them.
Resilience
- Retry only what's retryable:
429,503, in-progress409, and cautiously5xx. Never blind-retry other4xx. HonourRetry-After. → Errors
- Throttle yourself to both buckets. API-key traffic shares one 300-req/60 s bucket per platform (expensive POSTs, including
/journal/payments/preview), across all your workers and companies. On top of that, a pre-auth per-IP cap of 600/60 s counts every request, GETs included — so a poller behind a NAT'd/shared egress IP can429on reads. Put a client-side limiter in front of your POSTs, keep polling intervals sane, and back off on429even forGETs. → Errors
- Verify every webhook signature over the raw body, with a constant-time compare, and enforce a timestamp window yourself (it's off by default). Accept any
v1=value during secret rotation. → Receiving Results
- Deduplicate webhooks on the event
id. Delivery is at-least-once; the same event can arrive more than once. Theidis stable across retries.
- Monitor for webhook auto-disable. Five consecutive failures silently disables your endpoint. Watch the delivery log and
X-Delivery-Sequencegaps, and always run a polling backstop so a disabled webhook can't lose you data.
- Ack webhooks fast, process afterward. Return
2xxin well under the 10 s timeout, then do the real work asynchronously keyed on the event id.
- Make retries idempotent end-to-end. Because a crash mid-payment self-heals on the server, your side must also tolerate seeing a
201withIdempotency-Replayed: trueand treat it as success, not a new payment.
Observability
- Log
X-Request-IDon every response. It's how support finds your exact request. Store it alongside your own correlation id.
- Persist the
messageId↔ your-order mapping. Reconciliation (pain.002,camt.053) correlates onmessageId; you'll want to join back to your domain objects.
- Log validation warnings, don't drop them. A truncation or non-banking-day warning is often the earliest signal of an upstream data problem.
Environment hygiene
- Do all integration work in sandbox first, against the demo bank. The demo bank channel gives you a full payment lifecycle in ~5 seconds, and by construction, since it's a simulated bank, never moves real money. The deployed sandbox also hard-stops every production send server-wide, so
sandbox.bankconnector.comcan't move real money for anyone (see Going Live for the full boundary).
- Get the spec from docs.bankconnector.com, not a live server. The docs and OpenAPI spec live only on the docs site (the spec is vendored there at build time). Don't fetch the spec from an API host at runtime: on a production instance
GET /openapi.jsonandGET /openapi.yamlreturn404by design. (GET /docsisn't 404'd anywhere; it302s to the hosted site.) Treat the docs site as the single source in every environment.
- Pin the API version with
BankConnector-Version: YYYY-MM-DDso a server-side version bump can't silently change behaviour under you. It's echoed back on every response.
A pre-launch checklist
Before you call the integration done:
- Idempotency key sent on every payment, stable across retries
-
201handled as "accepted"; "paid" is driven by later events - Retry logic covers
429/503/in-progress409and honoursRetry-After - Client-side rate limiter respects the shared per-platform bucket
- Webhook signatures verified over raw body, constant-time, rotation-aware
- Webhook events deduplicated on
id - Polling backstop drains
unretrievedOnly=trueon a timer - Webhook auto-disable is monitored and alertable
-
X-Request-IDlogged everywhere - Error handling branches on
code, error type models the object envelope - Amounts are currency-correct strings; timestamps are UTC
Z - Everything validated end-to-end in sandbox before production credentials