Setting up webhooks
- Go to Settings > Webhooks in the dashboard.
- Click Add webhook and enter your endpoint URL. The URL must use HTTPS.
- Select the events you want to receive.
- Save the generated secret somewhere safe — it is shown only once and is required to verify incoming requests.
Event types
These are the twelve subscribable event types. The value in this column is exactly what arrives in theX-Webhook-Event header.
A thirteenth value,
TEST, is not subscribable. It is used for the test ping
you can fire from the dashboard and for the probe that reactivates a suspended
endpoint; it arrives as X-Webhook-Event: TEST.
PAYMENT_EXPIRED is not always final for boleto. A boleto paid on or shortly after its due date can settle late, so you may receive PAYMENT_EXPIRED and then PAYMENT_PAID for the same charge. Always treat the body’s status field as the source of truth rather than assuming PAYMENT_EXPIRED is terminal.Webhook payload
Events are delivered as JSONPOST requests.
A PAYMENT_PAID body, verbatim:
id is the payment’s UUID and amount is in reais, not centavos. The
endToEndId is the PIX-network end-to-end identifier (Banco Central
standard), safe to use for reconciliation.
Every field inside payer is nullable, and the whole block is null for
historical records or open-payer charges where no payer data was captured.
Some PIX participants report the payer’s document masked (for example
***405566**). When that is all the network gave us, the masked value is
what payer.document carries — a full document is preferred whenever one is
on file, and the masked string is the fallback rather than null.So payer.document is a string of digits or a partially-starred string,
and it is still null when no document of either kind was captured (an
open-payer charge, most historical records). Parse it defensively: match on
* before treating the value as a CPF or CNPJ, and never make settlement
logic depend on it. payer.documentType is normally null alongside a
masked value, since the network does not say which kind it masked — but it
can still be CPF or CNPJ when you supplied customer.documentType on
the charge, so use the * in payer.document, not documentType, to
detect a mask.WITHDRAWAL_SENT body, verbatim:
recipient.document is populated when the withdrawal was created with a
destination.recipientDocument, or with a CPF or CNPJ destination.pixKey
(see Withdrawals → Recipient document),
or when the settling provider reported the receiving account back to us. It is
null otherwise. The other recipient fields are populated only when the
provider reports them, which not every provider does — and when all six
fields would be null, the whole recipient block is null rather than an
object of nulls. The same all-null collapse applies to payer.
WITHDRAWAL_FAILED has the same shape plus a failureReason string. On
WITHDRAWAL_SENT the failureReason key is absent entirely rather than
null, and on WITHDRAWAL_FAILED it is absent too when the network supplied
no usable reason text. It is never null.
Body per event type
Four things to design around:
- How a payment is identified varies by event.
PAYMENT_PAIDcarries bothid(the payment UUID) andtxId.PAYMENT_CONFIRMED,PAYMENT_CHARGEBACKand somePAYMENT_EXPIRED/PAYMENT_CANCELLEDdeliveries carry onlytxId.PAYMENT_REFUNDED,PAYMENT_REFUND_FAILED,MED_RECEIVEDandMED_RESOLVEDcarry onlypaymentIdand notxIdat all. Key your lookup on whichever of the three is present, not on one of them. - Absent means
null, except for a few keys that vanish. Most optional fields arrive as an explicitnull—paidAt,endToEndId,payer,recipientall do. A small set is dropped from the JSON instead:failureReasononWITHDRAWAL_SENT, andsource,providerMedId,refundIdandamounton the MED and refund paths that do not set them. Read the value with a null-safe accessor rather than testing key presence. - The dashboard “resend” button sends a thinner body. A delivery you
trigger by hand from a transaction in the dashboard carries a generic
{ id, txId, status, amount }shape (plusnetAmountfor withdrawals) — not the per-event body documented here — and gets a freshX-Webhook-Delivery-Id. Note it keys the resource asid, not aspaymentId/withdrawalId. Do not treat a resend as byte-identical to the original delivery. txIdformats. Payments arePAY+ 32 uppercase hex characters. Withdrawals areWIT+ a millisecond timestamp + 5 uppercase alphanumerics (e.g.WIT1746280868443K3F9Q). Refunds vary by origin. TreattxIdas an opaque string; do not parse or validate its shape.
Sandbox
None of the bodies above are delivered for sandbox activity. Sandbox payments and withdrawals send no webhooks to a configured endpoint, for any event type, so a sandbox integration pollsGET /v1/payments/{id} or
GET /v1/withdrawals/{id} and drives terminal outcomes with
POST /v1/payments/{paymentId}/simulate
instead — see Sandbox testing.
The only delivery a sandbox account receives is the TEST ping, whose body
carries data.livemode, which reflects whether the account is a
sandbox-only account — not which API key you used. The reactivation probe sends
a different body: data there carries message and webhookConfigId, with no
livemode.
Request headers
Each webhook delivery includes these headers:Verifying signatures
YuvexPay signs each delivery with the webhook secret you saved when creating the endpoint. The recommendedv1 algorithm signs both a timestamp and the raw request body, which prevents both tampering and replay attacks.
- Read
X-Webhook-TimestampandX-Webhook-Signaturefrom the headers. - Compute the expected signature using your stored secret and the raw, unparsed request body.
- Compare in constant time. Reject if they don’t match.
- Reject any request whose timestamp is more than 300 seconds off the current time. This is our recommended replay window; pick your own if your clocks or queues need more slack.
Delivery semantics
YuvexPay aims to deliver every event at least once. A few characteristics worth designing around:- At-least-once delivery. The same event may arrive more than once. Treat
X-Webhook-Delivery-Idas the deduplication key. If you’ve processed a delivery id before, return200and skip the side effects. - Order is not guaranteed. Deliveries are dispatched in parallel; under retry, a
PAYMENT_REFUNDEDmay arrive before its priorPAYMENT_PAID. Always reconcile against the resource state in the body’sstatusfield, not against the order of arrival. - Synchronous timeout: 10 seconds. Return
2xxwithin 10 seconds. Process work asynchronously if you need longer. - Redirects are not followed. Your endpoint must respond directly.
Retry behavior
Whether a failed delivery is retried depends on how it failed.
A
4xx other than 408/429 means your endpoint rejected the request, and repeating it will not change the answer. We stop immediately rather than spending your attempt budget — and ours — on a request that cannot succeed.
Retries use exponential backoff with jitter, starting at 5 seconds and doubling, capped at 6 hours:
The actual delay is randomised around the nominal value so that a large batch of deliveries to the same endpoint does not arrive as a thundering herd.
A delivery stops being retried when any of the following is true:
- the configured retry budget for the endpoint is exhausted (capped at 24 attempts regardless of configuration);
- the delivery is older than 72 hours;
- the endpoint has been suspended.
FAILED state and produces no further attempts. Failed deliveries can be inspected and manually replayed from the dashboard.
Automatic endpoint suspension
An endpoint that stops accepting deliveries is suspended automatically. This protects your other integrations and ours: one dead endpoint absorbing every retry slot used to degrade delivery for everyone. An endpoint is suspended when either threshold is crossed:- a long run of consecutive failed deliveries; or
- a sustained failure rate over a rolling window, once enough attempts have been observed to be meaningful.
- it is deactivated and receives no new events;
- every delivery still queued for it is moved to
FAILED; - everyone on the account with developer access is notified in-app and by email, with the endpoint URL, the reason, and the last HTTP status and response excerpt we saw.
Reactivating a suspended endpoint
Fix the endpoint, then reactivate it from Developers → Webhooks in the dashboard, or:webhook.test event to the endpoint and only re-enable it if your server answers 2xx. If it does not, the call returns 400 with code REACTIVATION_PROBE_FAILED and the status we observed, and the endpoint stays suspended.
Deliveries that failed while the endpoint was down are not resent automatically — replay the ones you need from the dashboard once the endpoint is back.
Best practices
- Verify before trusting. Always validate the
X-Webhook-Signatureand theX-Webhook-Timestampskew. - Return 200 quickly. Acknowledge receipt synchronously and do business logic in a background job.
- Deduplicate by delivery id. The same event can be delivered more than once —
X-Webhook-Delivery-Idis stable across retries and replays. - Don’t rely on order. Use the body’s
statusfield to decide what to do, not the sequence of arrivals. - Switch on the header.
X-Webhook-Eventis the only place the event name appears. The body has notypefield. - Use HTTPS. Webhook endpoints must use HTTPS.
- Don’t log the secret. Treat the webhook secret like an API key.

