Before going to production
Everything on this list breaks real integrations.
Data and validation
Section titled “Data and validation”- The beneficiary’s
phoneis always included, also when you pay bytransferencia. Registration does not require it; the payment network does. → -
bankCodeis validated against the catalog of the method you are going to use (GET /payouts/banks?method=…). The two lists do not match: 31 vs. 24 banks. -
accountNumberis 20 digits and starts with thebankCode. -
fullNamegoes exactly as the destination bank has it, not as you have it: the most frequent bank rejection is about ownership. - You send the
senderblock, or you have your program’s default sender configured. With neither, the payout fails before it goes out. →
- All amounts travel as integers in minor units, and you parse them
back with arbitrary-precision integers, not with
Number. → - No amount sits right at the payment network’s minimum (~Bs 925 today, and it moves with the rate). →
- You know your cap per transaction and have asked to raise it if your use case needs it (pago móvil: Bs 100,000.00 by default).
- You quote right before paying out: the TTL is ~60 seconds and the quote is single use. →
Idempotency and retries
Section titled “Idempotency and retries”- Every call to
POST /payoutscarries anIdempotency-Key, with a key unique per operation. → - A timeout or a dropped connection is retried with the same key.
- A
status: "unknown"is never retried. - A
status: "failed"is retried with a new quote, after fixing the cause inreason. - Your HTTP client respects
Retry-Afteron a429.
The outcome
Section titled “The outcome”- Your system marks the payment as paid with the
payment.settledwebhook, not with thePOST /payoutsresponse. → - Your receiver verifies the HMAC signature over the raw bytes and rejects timestamps older than 5 minutes. →
- Your handler deduplicates on
id: delivery is at-least-once. - Your receiver responds
2xxin under 5 seconds and processes in the background. - You have an alert on
GET /v1/events?status=dead: if your receiver was down, that is where whatever did not arrive is. - You handle
payment.reversed: the bank can return a payment days later.
Errors
Section titled “Errors”- Your code programs against
error.code, never againstmessage, and has adefaultfor unknown codes. → - You distinguish
error.code(HTTP error) fromreason(payment network rejection inside a201response). They are two different taxonomies. - You log the
request_idof every error: it is what support needs.
Operations
Section titled “Operations”- You split the keys: one with
payments:writefor the backend that pays out, another read-only one for the dashboard. → - You know whether you operate prefunded or with daily close, and which
figure to watch (
prefundingBalanceorcreditDrawn). → - Your reconciliation process uses
from/toonsettled_at, not the creation date. → - You store the
bank_referenceof every payment: it is the beneficiary’s receipt.
The test you must run, no matter what
Section titled “The test you must run, no matter what”When moving to live
Section titled “When moving to live”- You swap
sk_test_…forsk_live_…and confirm withGET /v1/tenant/methatmodeislive. - Your production
webhookUrlis registered and its secret stored separately from the API key (with yoursk_live_: see test your webhook). - You have made a real payout, small but above the minimum, and
followed it to
settledwith itsbank_reference.