EnglishChangelog

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: bankSlipUrl stays null for good — show identificationField to the payer.
  • Once an issuance under verification is confirmed, retrying with the same externalRef returns the boleto (200, "idempotent": true).

See Create a boleto charge.

2026-10-15 — Boleto: full payer data and issuance under verification

  • New optional object customer.address (street, number, complement, district, city, zipCode, state) on POST /v1/boleto. Some accounts now require the payer’s email, phone and address: on those, a missing or invalid field returns 400 naming 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, bankSlipUrl and identificationField may be null while 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 400 with "Boleto em verificação: a emissão não confirmou. NÃO reenvie — consulte a transação <id> em instantes.". Retrying with the same externalRef repeats this response until it is confirmed, instead of issuing a second boleto.
  • A transient failure where nothing was issued now returns 503 with "Boleto temporariamente indisponível. Tente novamente em instantes." — you can retry. Unexpected errors remain 500 (the docs said 502 by mistake).

See Create a boleto charge.

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 returns 400 with "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.expired carries the new optional field unpaidFeeCents (omitted when there is no fee), and the charge shows in GET /v1/transactions as a type: "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/transactions accepts ?type=fee and ?type=fee_reversal in the filter.

See Create a boleto charge.

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 400 with "Dados recusados pelo provedor: <campo> inválido. Corrija e reenvie. (<código>)" (previously a generic 500). <campo> names what to fix and <código> is stable (e.g. invalid_cpfCnpj) — branch on the code, not the text.
  • customer.phone is 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, with netCents net of your account’s boleto fee) or expired (transaction.expired) after the due date. The method field 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.

See Create a boleto charge.

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 in GET /v1/recipients and become usable in POST /v1/withdraw via recipientId.
  • 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/cnpj key must match the document. 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.

  • 4xx is not retried (400/401/403/422…): it means the endpoint refused the event; hammering would not help. Fix the cause and use manual resend.
  • eventId is preserved across attempts — dedup by X-Coufai-Event-Id is mandatory. The timestamp and X-Coufai-Signature are regenerated on each attempt; the eventId is not.
  • With this, an event-driven flow is recommended: react to the webhook + maintain your balance from netCents + lightweight reconcile, no aggressive polling of GET /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.type in POST /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 confusing 502 Erro ao comunicar com gateway.
  • pixKeyType in POST /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: 400

Recommendation: 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_refunded now carry:

    • endToEndId — BACEN-standard E2E ID of the PIX (format E<ISPB><YYYYMMDDhhmm><id>).
    • payer — who paid: { name, document, documentType, bankAccount: { ispb, branch, account } }.
  • Webhooks transfer.paid, transfer.refunded, transfer.partially_refunded now carry:

    • endToEndId — E2E ID of the PIX OUT.
    • beneficiary — who received the withdrawal: same structure as payer.
    • 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/:id and GET /v1/transactions return 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.* and transfer.* events with stable names in X-Coufai-Event and in the body’s event field. 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-Id with 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. eventId is preserved (idempotency); timestamp and signature regenerated. 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 nameCurrent canonical name
TRANSFER_PAIDtransfer.paid
transaction.refunded.partialtransaction.partially_refunded
transfer.refunded.partialtransfer.partially_refunded
chargeback.appliedtransaction.chargeback
med.openedtransaction.infraction
dispute.openedtransaction.dispute
funds.blockedtransaction.blocked
protest.openedtransaction.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/withdraw remain 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/transactions and GET /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.