This guide is for developers implementing 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.Documentation Index
Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
Use this file to discover all available pages before exploring further.
Idempotency
Every mutating request (initiate, process, cancel) requires an
X-Idempotency header. If you send the same key twice, 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
- Keys are scoped per organization — the same key from two different organizations is treated as two distinct requests
- Cached responses are returned for 24 hours
- Replayed responses are byte-identical to the original (same status code and body); the response does not currently expose a header to distinguish replays from fresh executions, so design your client to be safe under either case
Duplicate detection
Beyond idempotency keys, the plugin detects content-based duplicates. It generates a hash from the organization (from theX-Organization-Id header), senderAccountId, recipient details, and amount, and stores it in Redis for 5 minutes (default 300 seconds, configurable via DUPLICATE_GUARD_TTL_SEC). If a matching transfer was already submitted within the window, the request is rejected with 409 BTF-0012.
This catches cases where the client sends the same transfer with a different idempotency key — for example, after a timeout where the original response was not received.
Retry strategy
Use exponential backoff for transient errors. Not all errors should be retried.
| HTTP status | Error type | Retry? | Notes |
|---|---|---|---|
400 | Validation error | No | Fix the request before retrying |
404 | Not found | No | Resource does not exist |
409 | Duplicate | No | Idempotent — use the original response |
410 | Expired | No | Create a new initiation |
422 | Business rule | No | Operating hours, limits — condition must change first |
429 | Rate limit | Yes | Wait for Retry-After header value (seconds) |
500 | Internal error | Yes | Retry with backoff |
503 | Unavailable | Yes | Retry with backoff |
When JD SPB is unavailable, the response is
HTTP 503 and the error.code field carries the raw JD vendor code (for example, TRANSPORT for transport failures or ACE95 for timeouts) — JD-chain failures are not wrapped in a BTF- code. After exhausting retries, the transfer should be flagged for manual reconciliation. Do not keep retrying indefinitely — the JD SPB network has defined operating hours.State handling
TED OUT state machine
Transfers follow a strict progression. Once a transfer leavesCREATED or PENDING, it cannot be cancelled.

| State | Meaning | Recommended action |
|---|---|---|
CREATED | Confirmed by user, queued for submission | Show “Processing” in UI; poll or wait for webhook |
PENDING | Submitted to JD, awaiting acknowledgment | Show “Processing”; do not allow cancellation |
PROCESSING | JD accepted and is routing the transfer | Show “Processing”; typical SLA under 10 minutes |
COMPLETED | Settled | Show confirmation with confirmationNumber |
REJECTED | JD rejected (invalid data, rule violation) | Show error to user; funds already released |
FAILED | JD unreachable or timed out | Show error; funds already released; allow retry if desired |
CANCELLED | Cancelled before submission | Show cancellation confirmation |
Initiation state machine
ThePaymentInitiation entity (created by the initiate endpoint) has its own lifecycle before a Transfer is created.

TED IN state machine

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

Polling vs. webhooks
Prefer webhooks for real-time status. If webhooks are not yet configured, pollGET /v1/transfers/{transferId} with a maximum of 10 attempts using the same backoff schedule as retries. After 10 minutes with no terminal state (COMPLETED, REJECTED, FAILED, CANCELLED), flag the transfer for manual review.
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 two headers your endpoint must use to verify authenticity:X-Webhook-Signature—sha256=<hex>HMAC-SHA256 signatureX-Webhook-Timestamp— Unix timestamp in seconds (UTC) when the plugin built the request
X-Webhook-Timestamp), followed by a single ASCII dot (.), followed by the raw request body bytes — exactly as received, before any JSON parsing or re-encoding. Use the bytes from the wire, not a re-serialized version of the parsed object.
To validate:
- Read
X-Webhook-SignatureandX-Webhook-Timestampfrom the request headers. - Build the signed payload:
timestamp + "." + rawBody. - Compute
HMAC-SHA256over the signed payload using yourWEBHOOK_SIGNING_SECRETand hex-encode the result. - Prepend
sha256=and compare againstX-Webhook-Signatureusing a constant-time equality function. - Reject the request if the timestamp is outside an acceptable freshness window (a 5-minute tolerance is typical) to prevent replay.
X-Webhook-Event-Type, X-Webhook-Routing-Key, and X-Webhook-Delivery-Attempt for observability — these are not part of the signed payload and must not be used for authentication.
JavaScript
JavaScript
Python
Python
Go
Go
Idempotent webhook processing
Your endpoint may receive the same event more than once (at-least-once delivery). UsetransferId + event 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.
| Scenario | Code | User-facing message | Action |
|---|---|---|---|
| Outside operating hours | BTF-0010 | ”Transfers available Mon–Fri, 06:30–17:00 (Brasília). Next window: “ | Show next available time |
| Daily limit exceeded | BTF-0011 | ”Daily transfer limit reached. Try again tomorrow.” | Show remaining limit |
| Duplicate transfer | BTF-0012 | ”This transfer was already submitted.” | Return original transferId |
| Invalid recipient data | BTF-0001 | ”Check recipient details and try again.” | Highlight invalid fields |
| Initiation expired | BTF-0202 | ”Session expired. Please start a new transfer.” | Restart initiation flow |
| JD SPB unavailable | TRANSPORT (HTTP 503) | “Transfer service temporarily unavailable. Try again in a few minutes.” | Retry with backoff; detect via 503 + raw JD vendor code (TRANSPORT, ACE95, …), not by a BTF- prefix |
| Midaz unavailable | BTF-2000 | ”Service temporarily unavailable. Try again in a few minutes.” | Retry with backoff |
Go-live checklist
Before enabling the integration in production:
-
X-Idempotencyis sent on every initiate, process, and cancel request - Retry logic implemented with exponential backoff for 5xx/503 errors
- Webhook endpoint deployed and returning
200within 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
transferIdandconfirmationNumberstored for reconciliation - Terminal states (
COMPLETED,REJECTED,FAILED,CANCELLED) handled in UI - Initiation expiry (24h) handled — user is prompted to restart if window passes
- Service readiness monitored in your alerting system for BYOC deployments
- Redis is available and monitored — service will not accept requests if Redis is unreachable
-
PLUGIN_AUTH_ENABLED=trueconfigured in production, with validPLUGIN_AUTH_ADDRESS,PLUGIN_AUTH_CLIENT_ID, andPLUGIN_AUTH_CLIENT_SECRET

