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 to the effective organization — the same key from two different organizations is treated as two distinct requests
- Cached responses are returned for the configured idempotency window (
IDEMPOTENCY_RETRY_WINDOW_SEC, default 300 seconds) - 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 builds a fingerprint from:senderAccountId- recipient details (ISPB, branch, account, holder document)
- amount
- purpose
DUPLICATE_GUARD_TTL_SEC). The organization is not part of the fingerprint — tenant isolation comes from the Redis key prefix. 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 | Retry? | Notes |
|---|---|---|
400 | No | Validation error — fix the request before retrying |
404 | No | Not found — the resource does not exist |
409 | No | Duplicate — idempotent; use the original response |
410 | No | Expired — create a new initiation |
422 | No | Business rule (operating hours, limits) — the condition must change first |
429 | Yes | Rate limit — wait for the Retry-After header value (seconds) |
500 | Yes | Internal error — retry with backoff |
503 | Yes | Unavailable — 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 headers your endpoint uses to verify authenticity:X-Webhook-Signature— versioned HMAC-SHA256 signature in the formv1,sha256=<hex>X-Webhook-Timestamp— Unix timestamp in seconds (UTC) when the plugin built the requestX-Webhook-Event— the event type (for example,transfer.completed); not part of the signature
v1:, followed by the timestamp value (the string sent in X-Webhook-Timestamp), a single ASCII dot (.), and 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 string:
"v1:" + timestamp + "." + rawBody. - Compute
HMAC-SHA256over the signed string using yourWEBHOOK_SIGNING_SECRETand hex-encode the result. - Prepend
v1,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-Signature and X-Webhook-Timestamp, the plugin sets only X-Webhook-Event (the event type). It does not send X-Webhook-Event-Type, X-Webhook-Routing-Key, or X-Webhook-Delivery-Attempt.
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 | 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 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 a validPLUGIN_AUTH_ADDRESS(HTTPS)

