Skip to main content
Webhooks let your application receive HTTP POST notifications when events happen in your YuvexPay account, such as a payment being confirmed or a withdrawal completing.

Setting up webhooks

  1. Go to Settings > Webhooks in the dashboard.
  2. Click Add webhook and enter your endpoint URL. The URL must use HTTPS.
  3. Select the events you want to receive.
  4. Save the generated secret somewhere safe — it is shown only once and is required to verify incoming requests.
You can register up to two active webhook endpoints per event type per company.
Webhooks are delivered for production activity only. Sandbox payments and withdrawals do not send webhooks to your configured endpoints, for any event type on this page. An endpoint belongs to the account rather than to an environment, so there is no separate sandbox endpoint or sandbox secret to register.Test a sandbox integration by polling GET /v1/payments/{paymentId} and by forcing terminal outcomes with POST /v1/payments/{paymentId}/simulate. To exercise your verifier itself, use the dashboard Send test button, which delivers a signed TEST event. See Sandbox testing.

Event types

These are the twelve subscribable event types. The value in this column is exactly what arrives in the X-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 JSON POST requests.
There is no envelope. The HTTP body is the event object itself — there is no wrapping id, type, version, or data key. The event name travels only in the X-Webhook-Event header. Read request.headers['x-webhook-event'], not body.type, and read the fields directly off the parsed body, not off body.data.
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.
A 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_PAID carries both id (the payment UUID) and txId. PAYMENT_CONFIRMED, PAYMENT_CHARGEBACK and some PAYMENT_EXPIRED / PAYMENT_CANCELLED deliveries carry only txId. PAYMENT_REFUNDED, PAYMENT_REFUND_FAILED, MED_RECEIVED and MED_RESOLVED carry only paymentId and no txId at 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 explicit nullpaidAt, endToEndId, payer, recipient all do. A small set is dropped from the JSON instead: failureReason on WITHDRAWAL_SENT, and source, providerMedId, refundId and amount on 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 (plus netAmount for withdrawals) — not the per-event body documented here — and gets a fresh X-Webhook-Delivery-Id. Note it keys the resource as id, not as paymentId / withdrawalId. Do not treat a resend as byte-identical to the original delivery.
  • txId formats. Payments are PAY + 32 uppercase hex characters. Withdrawals are WIT + a millisecond timestamp + 5 uppercase alphanumerics (e.g. WIT1746280868443K3F9Q). Refunds vary by origin. Treat txId as 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 polls GET /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 recommended v1 algorithm signs both a timestamp and the raw request body, which prevents both tampering and replay attacks.
To verify a request:
  1. Read X-Webhook-Timestamp and X-Webhook-Signature from the headers.
  2. Compute the expected signature using your stored secret and the raw, unparsed request body.
  3. Compare in constant time. Reject if they don’t match.
  4. 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.
Always verify the v1 signature and the timestamp drift before processing any event. Never trust the payload without verification.

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-Id as the deduplication key. If you’ve processed a delivery id before, return 200 and skip the side effects.
  • Order is not guaranteed. Deliveries are dispatched in parallel; under retry, a PAYMENT_REFUNDED may arrive before its prior PAYMENT_PAID. Always reconcile against the resource state in the body’s status field, not against the order of arrival.
  • Synchronous timeout: 10 seconds. Return 2xx within 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.
In every case the delivery lands in a terminal FAILED state and produces no further attempts. Failed deliveries can be inspected and manually replayed from the dashboard.
A manual replay reuses the same X-Webhook-Delivery-Id as the original delivery. If you dedupe strictly on that header — which you should for retries — a replay you asked for will be silently discarded as a duplicate. Clear the delivery id from your dedupe store before requesting a replay.

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.
When an endpoint is suspended:
  1. it is deactivated and receives no new events;
  2. every delivery still queued for it is moved to FAILED;
  3. 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.
Sustained failures against a single endpoint also trip a per-endpoint circuit breaker before suspension. While the breaker is open, deliveries to that endpoint are deferred rather than attempted — they do not consume attempts, and no other endpoint is affected.

Reactivating a suspended endpoint

Fix the endpoint, then reactivate it from Developers → Webhooks in the dashboard, or:
Reactivation is gated on a live test delivery. We send a 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-Signature and the X-Webhook-Timestamp skew.
  • 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-Id is stable across retries and replays.
  • Don’t rely on order. Use the body’s status field to decide what to do, not the sequence of arrivals.
  • Switch on the header. X-Webhook-Event is the only place the event name appears. The body has no type field.
  • Use HTTPS. Webhook endpoints must use HTTPS.
  • Don’t log the secret. Treat the webhook secret like an API key.