Changelog
Relevant changes to Coufai’s public API.
2026-10-20 — Boleto: delivery by digitable line only
- On some accounts the boleto is delivered only as the digitable line, with no PDF:
bankSlipUrlstaysnullfor good — showidentificationFieldto the payer. - Once an issuance under verification is confirmed, retrying with the same
externalRefreturns the boleto (200,"idempotent": true).
2026-10-15 — Boleto: full payer data and issuance under verification
- New optional object
customer.address(street,number,complement,district,city,zipCode,state) onPOST /v1/boleto. Some accounts now require the payer’s email, phone and address: on those, a missing or invalid field returns400naming the field without creating the charge, and the due date must be today or later. On other accounts the address is ignored — we recommend always sending it. - Right after issuance,
bankSlipUrlandidentificationFieldmay benullwhile the bank finishes registering the boleto (usually under a minute); fetch the transaction afterwards. - Issuance under verification: when issuance does not confirm, the API returns
400with"Boleto em verificação: a emissão não confirmou. NÃO reenvie — consulte a transação <id> em instantes.". Retrying with the sameexternalRefrepeats this response until it is confirmed, instead of issuing a second boleto. - A transient failure where nothing was issued now returns
503with"Boleto temporariamente indisponível. Tente novamente em instantes."— you can retry. Unexpected errors remain500(the docs said502by mistake).
2026-09-23 — Boleto: account refusals split from data refusals
- When the refusal is about the account (issuance ceiling reached or issuance blocked), the API no longer returns
"Dados recusados pelo provedor: dados da cobrança inválidos. Corrija e reenvie. (invalid_action)"; it returns400with"Limite de emissão de cobranças da conta atingido. Não reenvie; fale com o suporte. (issuance_limit)"or"Emissão não permitida para esta conta no momento. Não reenvie; fale com o suporte. (account_restricted)". - For these two codes, retrying will not help: do not retry in a loop, contact support. Data refusals (
invalid_cpfCnpj,invalid_dueDate…) are unchanged.
2026-09-12 — Unpaid-boleto fee
- Accounts with an unpaid-boleto fee are now charged when a boleto passes its due date without payment. The boleto’s
transaction.expiredcarries the new optional fieldunpaidFeeCents(omitted when there is no fee), and the charge shows inGET /v1/transactionsas atype: "fee"line (feeKind: "boleto_unpaid",relatedTransactionId= the boleto). - A boleto paid after the due date returns the fee automatically (
type: "fee_reversal"line). - The balance can go negative because of the fee; withdrawals are refused for insufficient balance until the next credit covers it.
GET /v1/transactionsaccepts?type=feeand?type=fee_reversalin the filter.
2026-09-12 — Boleto: provider data rejections are now a clear 400; phone never blocks
- When the provider rejects payer or charge data, the API now returns
400with"Dados recusados pelo provedor: <campo> inválido. Corrija e reenvie. (<código>)"(previously a generic500).<campo>names what to fix and<código>is stable (e.g.invalid_cpfCnpj) — branch on the code, not the text. customer.phoneis optional: 11 digits = mobile, 10 = landline. If the provider rejects the phone, the boleto is issued without it; a bad phone no longer fails the charge.
2026-09-06 — Boleto (bank slip) via API
New endpoint POST /v1/boleto: issues a Brazilian bank slip and returns the public PDF link (bankSlipUrl, served by Coufai) and the typeable line (identificationField). Optional due date (dueDate, default today + 3 days); same idempotency by externalRef as PIX.
- Same lifecycle as PIX charges:
pending→paid(transaction.paid, withnetCentsnet of your account’s boleto fee) orexpired(transaction.expired) after the due date. Themethodfield is"boleto". - Enabled per account: without it, the call returns
400"Boleto não está habilitado para esta conta.". Contact support. - Payments whose notice was delayed are reconciled with the bank in the background and credited anyway — react to the webhook instead of polling aggressively.
2026-08-27 — Refusal reason on withdrawals
Refused withdrawals now tell you why they failed. The transfer.failed, transfer.rejected and transfer.cancelled events — and GET /v1/transactions/{id} — now carry two optional fields:
refusedCode— stable uppercase code (e.g.INSUFFICIENT_BALANCE,DAILY_LIMIT_EXCEEDED,INVALID_PIX_KEY). Branch on this field: never parse the text.refusedReason— human-readable description, currently returned in Portuguese and subject to change.
Previously a refused withdrawal became failed with no indication of cause, and the only option was to retry blind. With the code you can tell apart what is worth retrying (INSUFFICIENT_BALANCE, within minutes) from what is not without a fix (INVALID_PIX_KEY) or only the next day (DAILY_LIMIT_EXCEEDED).
Backwards compatible: the fields are omitted when the reason is unavailable, and no existing field changed name or type. Treat absence as “reason not reported” — never as success — and an unknown code as a generic refusal, since the list may grow.
A refused withdrawal is not debited: if the amount had already left your balance, it is returned before the event is sent.
See Refusal reason.
2026-06-04 — Third-party recipient registration via API
New endpoint POST /v1/recipients to submit third-party recipients (CPF/CNPJ different from the account holder) directly via API — previously, registration was only done through support.
- Registration creates the recipient as
pending_review: it does not approve and does not enable withdrawals. Approval (KYC/AML) is still performed by Coufai. Only after activation does the recipient appear inGET /v1/recipientsand become usable inPOST /v1/withdrawviarecipientId. - Idempotent by document: resending the same CPF/CNPJ returns the same
recipientId(no duplicates); an already-approved recipient is not downgraded. - Name-match: a
cpf/cnpjkey must match thedocument. Default limit of 10 recipients (active + under review) per account — adjustable per account via support.
See Register recipient.
2026-06-02 — Automatic webhook retry
Webhook deliveries that fail with a transient error (timeout, network error, HTTP 5xx, 429, 408) are now automatically redelivered with backoff: 1min → 5min → 30min → 2h → 6h (up to 6 attempts, ~9h). Before, there was a single automatic attempt — recovery depended on polling or manual resend.
4xxis not retried (400/401/403/422…): it means the endpoint refused the event; hammering would not help. Fix the cause and use manual resend.eventIdis preserved across attempts — dedup byX-Coufai-Event-Idis mandatory. ThetimestampandX-Coufai-Signatureare regenerated on each attempt; theeventIdis not.- With this, an event-driven flow is recommended: react to the webhook + maintain your balance from
netCents+ lightweight reconcile, no aggressive polling ofGET /v1/balance.
A change in behavior, not contract: no new fields, no action needed for anyone already dedup’ing by eventId. See Retry and idempotency.
2026-05-27 — Automatic normalization of document.type and pixKeyType + more informative errors
Additive change: requests already sending correct values continue working without adjustment. What changes is how we handle variations and report errors.
Automatic normalization (Postel’s law) in enum fields:
customer.document.typeinPOST /v1/pix: accepts"CNPJ","Cpf"," cnpj "etc. — case and whitespace variations are normalized internally to lowercase. Before, any non-lowercase was silently converted to"cpf"in sanitization, causing the provider to reject numbers with incompatible length and producing a confusing502 Erro ao comunicar com gateway.pixKeyTypeinPOST /v1/withdraw: same normalization —"CPF","Email"," evp "accepted.
Document length validation: customer.document.number must have 11 digits when type=cpf or 14 digits when type=cnpj (after removing punctuation). Before, numbers with incompatible length were accepted by Coufai and rejected by the PSP.
More informative errors — all 400 payload-validation messages now include the value received (truncated) and what is expected:
// Before
{ "error": "Erro ao comunicar com gateway. Tente novamente." }
// status: 502
// Now
{ "error": "customer.document.type deve ser 'cpf' ou 'cnpj'. Recebido: 'CNPJ '." }
// status: 400Recommendation: sending values lowercase without whitespace is the clean default, but the API no longer requires it.
See Create charge, Initiate withdrawal and Errors.
2026-05-09 — Withdrawal to pre-approved third parties via API
POST /v1/withdraw gained support for the optional recipientId field to withdraw to third-party recipients (CPF/CNPJ that is not the account holder).
- Without
recipientId(default): the previous behavior — only the registered holder’s CPF/CNPJ, other types rejected. - With
recipientId: enables all 5 PIX key types (cpf,cnpj,email,phone,evp) for a recipient pre-approved by the Coufai team (compliance/AML).
recipientId approval is manual — contact support with the recipient’s legal name and CPF/CNPJ. No self-service endpoint.
New endpoint GET /v1/recipients returns the list of active pre-approved recipients for your account with the corresponding recipientId — use them in POST /v1/withdraw. See List recipients.
Current defenses unchanged: API key, IP allowlist, rate limit, anti-burst, daily/monthly caps, balance validation, idempotency via externalRef. Every third-party withdrawal triggers an internal alert at Coufai (continuous audit).
See Initiate withdrawal for the full schema, key-type restrictions and examples.
2026-05-09 — PIX data in webhooks and REST queries
New optional fields delivered when the transaction has actually settled on the SPI network. Additive change — clients that do not use them ignore them, no adaptation needed.
-
Webhooks
transaction.paid,transaction.refunded,transaction.partially_refundednow carry:endToEndId— BACEN-standard E2E ID of the PIX (formatE<ISPB><YYYYMMDDhhmm><id>).payer— who paid:{ name, document, documentType, bankAccount: { ispb, branch, account } }.
-
Webhooks
transfer.paid,transfer.refunded,transfer.partially_refundednow carry:endToEndId— E2E ID of the PIX OUT.beneficiary— who received the withdrawal: same structure aspayer.refundEndToEndId— only on refunds (transfer.refunded/transfer.partially_refunded): E2E ID of the refund PIX, distinct from the original PIX OUT.
-
REST
GET /v1/transactions/:idandGET /v1/transactionsreturn the same fields when applicable. Polling becomes an equivalent fallback to the webhook.
Events without settlement (transaction.failed, cancelled, expired, transfer.created, processing, pending_approval, approved, rejected, failed, cancelled) do not carry these fields — they are omitted from the JSON, not returned as null.
LGPD note: payer.* and beneficiary.* contain personal data (name, CPF/CNPJ, bank account). You are the controller for subsequent processing. See Events.
2026-05-08 — Multi-webhook + delivery history
- Multiple webhook URLs per account (up to 6). Each URL filters events by canonical name or by semantic category (
cashin,cashout,refund,dispute,chargeback). See Configuring webhook. - Standardized canonical event catalog: 21
transaction.*andtransfer.*events with stable names inX-Coufai-Eventand in the body’seventfield. See Events. - New events delivered for the first time:
transaction.partially_refunded,transaction.infraction,transaction.dispute,transaction.protest,transaction.blocked,transaction.chargeback,transfer.created,transfer.processing,transfer.partially_refunded. - New header:
X-Coufai-Event-Idwith a unique identifier per event. Same across fanout URLs and manual resends — use it for dedup. - Delivery history in the dashboard (60-day retention): timeline of each attempt with exact payload, HTTP status and duration. Available in the transaction or in the global list.
- Manual webhook resend: button to fire a new attempt for a historical delivery.
eventIdis preserved (idempotency);timestampandsignatureregenerated. Limit 10/h per account. See Manual redelivery.
Naming breaks (note if you integrated before May/2026)
The following names were replaced by the canonical ones:
| Old name | Current canonical name |
|---|---|
TRANSFER_PAID | transfer.paid |
transaction.refunded.partial | transaction.partially_refunded |
transfer.refunded.partial | transfer.partially_refunded |
chargeback.applied | transaction.chargeback |
med.opened | transaction.infraction |
dispute.opened | transaction.dispute |
funds.blocked | transaction.blocked |
protest.opened | transaction.protest |
If you compared by the old name, update. The status in the body (stable field) remains the same.
2026-05-08 — Dashboard 2FA (no API impact)
- Merchants can now enable two-factor authentication (TOTP) in the dashboard, under Settings → Security.
- No change in the REST API contract. API keys continue to work exactly as before — no extra code, no new header, no change in requests or responses.
- Withdrawals initiated through the dashboard now require a TOTP code at the time of the operation. Withdrawals via
POST /v1/withdrawremain unchanged (auth by API key + IP allowlist + rate limit).
2026-05 — Public documentation launch
- Initial version covering:
POST /v1/pix— PIX charge creation.POST /v1/withdraw— PIX withdrawal.GET /v1/balance— balance.GET /v1/transactionsandGET /v1/transactions/:id— listing and lookup.- Single webhook delivered to the endpoint configured by the merchant, HMAC-signed.
[TO BE CONFIRMED WITH THE COUFAI TEAM]: align the API versioning policy (semver, breaking changes, deprecation timelines) and the final format of this changelog.