Skip to main content
This guide is for developers who implement the Bank Transfer plugin integration. It covers the patterns and decisions that go beyond individual endpoint calls: idempotency, retry strategy, state handling, and webhook validation. For endpoint parameters and response schemas, see the API Reference.

Idempotency


Every mutating request (initiate, process, cancel) requires an X-Idempotency header. A request without it is refused with 400 BTF-0001. If you send the same request twice under one key, the plugin returns the original response without creating a duplicate operation. Rules:
  • Use a UUID v4 or a unique business identifier (e.g. your internal order ID)
  • Maximum length: 255 characters
  • The plugin scopes each key to the effective organization. The same key from two organizations counts as two distinct requests.
  • The plugin replays a successful response for 7 days. A replay carries the same status code and body, plus the header X-Idempotency-Replayed: true.
  • A 4xx answer does not spend the key: fix the request and send it again under the same key.
  • After a 5xx answer the outcome is unknown: read the resource first, and send the request again only under a new key. A key that the answer fenced (X-Idempotency-Fenced: true) is refused with 422 BTF-0014. A 503 BTF-9000 or 503 BTF-9017 ran nothing: send it again under the same key.
  • The same key with a different body is refused with 422 BTF-0017. A resend while the first request still runs gets 409 BTF-0016 with Retry-After.
  • Every idempotency refusal states its remedy in detail. Read it before you resend.
Do not reuse idempotency keys across different operations. Do not reuse an initiate key to process or cancel the same transfer.

Duplicate detection

Beyond idempotency keys, the plugin detects content-based duplicates. It builds a fingerprint from:
  • senderAccountId
  • recipient details (ISPB, branch, account, holder document)
  • amount
  • purpose
The plugin stores the fingerprint in Redis for 5 minutes. The default is 300 seconds. Operators tune it per tenant through the systemplane setting idempotency.duplicate_guard_ttl_seconds. The organization is not part of the fingerprint. Tenant isolation comes from the Redis key prefix. The plugin rejects the request with 409 BTF-0012 if the client already submitted a matching transfer inside the window. This catches cases where the client sends the same transfer with a different idempotency key. One example is a retry after a timeout, when the client did not receive the original response.

Retry strategy


Use exponential backoff for transient errors. Do not retry every error. Recommended backoff schedule for 5xx: 0s, 5s, 25s, 60s, 120s (5 attempts total).
During POST /v1/transfers/process, when JD SPB is unavailable, the response is HTTP 503 with code BTF-1000, or BTF-1003 on a JD timeout. upstream.code carries JD’s own code when JD returned one. transferId and transferStatus name the transfer. A transfer in PROCESSING keeps its hold while reconciliation asks JD for the outcome: do not process it again.

State handling


TED OUT state machine

Transfers follow a strict progression. You cannot cancel a transfer after it leaves CREATED or PENDING.
TED OUT state machine
What to do in each state:

Initiation state machine

The initiate endpoint creates a PaymentInitiation entity. This entity has its own lifecycle before the plugin creates a Transfer.
Initiation state machine

TED IN state machine

TED IN state machine

P2P state machine

P2P does not have a PENDING state. Settlement is atomic and instant.
P2P state machine

Polling vs. webhooks

Prefer webhooks for real-time status. If you have not configured webhooks yet, poll GET /v1/transfers/{transferId}. Use a maximum of 10 attempts with the same backoff schedule as retries. Flag the transfer for manual review after 10 minutes with no terminal state (COMPLETED, REJECTED, FAILED, CANCELLED). See Get Transfer and Webhooks.

Webhook integration


For event payload schemas and the full list of events, see Webhooks.

Signature validation

Every webhook request includes headers your endpoint uses to verify authenticity:
  • X-Webhook-Signature: versioned HMAC-SHA256 signature in the form v1,sha256=<hex>
  • X-Webhook-Timestamp: Unix timestamp in seconds (UTC) when the plugin built the request
  • X-Webhook-Event: the event type (for example, transfer.completed). This header is not part of the signature.
The plugin computes the signature as:
The signed string has four parts in order: the prefix v1:, the timestamp value from X-Webhook-Timestamp, one ASCII dot (.), then the raw request body bytes. Use the body bytes exactly as they arrive on the wire. Do not parse or re-encode them first. To validate:
  1. Read X-Webhook-Signature and X-Webhook-Timestamp from the request headers.
  2. Build the signed string: "v1:" + timestamp + "." + rawBody.
  3. Compute HMAC-SHA256 over the signed string with the signingSecret of the webhook registration, then hex-encode the result.
  4. Prepend v1,sha256=, then compare against X-Webhook-Signature with a constant-time equality function.
  5. Reject the request if the timestamp is outside an acceptable freshness window (a 5-minute tolerance is typical) to prevent replay.

Idempotent webhook processing

Your endpoint may receive the same event more than once (at-least-once delivery). Use transferId + the X-Webhook-Event header as a composite key to deduplicate.

Error handling patterns


Map API error codes to user-facing actions. See the full error list for all codes. Error responses are RFC 9457 problem documents (application/problem+json). Route-specific fields sit at the top level:

Go-live checklist


Before enabling the integration in production:
  • Send X-Idempotency on every initiate, process, and cancel request
  • Retry logic implemented with exponential backoff for 5xx/503 errors
  • Webhook endpoint deployed and returning 200 within 5 seconds
  • Signature validation active on the webhook endpoint
  • Webhook event deduplication implemented using transferId + event
  • Operating hours validated client-side before calling initiate (reduces unnecessary 422s)
  • Both transferId and confirmationNumber stored for reconciliation
  • Terminal states (COMPLETED, REJECTED, FAILED, CANCELLED) handled in UI
  • Initiation expiry handled (expiresAt): prompt the user to restart when it passes
  • Service readiness monitored in your alerting system for BYOC deployments
  • Redis reachable and monitored: the service rejects requests when Redis is down
  • PLUGIN_AUTH_ENABLED=true configured in production, with a valid PLUGIN_AUTH_ADDRESS (HTTPS)