Skip to content

Before going to production

Everything on this list breaks real integrations.

  • The beneficiary’s phone is always included, also when you pay by transferencia. Registration does not require it; the payment network does. →
  • bankCode is 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.
  • accountNumber is 20 digits and starts with the bankCode.
  • fullName goes exactly as the destination bank has it, not as you have it: the most frequent bank rejection is about ownership.
  • You send the sender block, 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. →
  • Every call to POST /payouts carries an Idempotency-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 in reason.
  • Your HTTP client respects Retry-After on a 429.
  • Your system marks the payment as paid with the payment.settled webhook, not with the POST /payouts response. →
  • 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 2xx in 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.
  • Your code programs against error.code, never against message, and has a default for unknown codes. →
  • You distinguish error.code (HTTP error) from reason (payment network rejection inside a 201 response). They are two different taxonomies.
  • You log the request_id of every error: it is what support needs.
  • You split the keys: one with payments:write for 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 (prefundingBalance or creditDrawn). →
  • Your reconciliation process uses from/to on settled_at, not the creation date. →
  • You store the bank_reference of every payment: it is the beneficiary’s receipt.
  • You swap sk_test_… for sk_live_… and confirm with GET /v1/tenant/me that mode is live.
  • Your production webhookUrl is registered and its secret stored separately from the API key (with your sk_live_: see test your webhook).
  • You have made a real payout, small but above the minimum, and followed it to settled with its bank_reference.