1. Webhooks
Pix2DePix API
  • ⚑ Quickstart
  • Guides
    • πŸ”‘ Authentication
    • πŸ”„ Possible Statuses
    • πŸ”€ Synchronous requests & safe retries
    • ⚠️ Errors & the response envelope
    • 🧩 Troubleshooting
    • βœ… Best Practices
    • πŸš€ Features
      • ⏱️ QR Delay
    • πŸ›‘οΈ Security & Limits
      • 🚦 API Limits
      • 🧱 Firewall
      • πŸͺ² Bug Bounty
  • API Endpoints
    • Ping
      GET
    • Deposit (PIX βž” DePix)
      POST
    • Deposit Status
      GET
    • Deposits
      GET
    • User Info
      GET
    • Withdraw
      POST
    • Withdraw Status
      GET
  • Webhooks
    • πŸͺ Webhooks
    • Deposit Webhook
    • Withdraw Webhook
    • MED Webhook
  • Reference
    • πŸ“– Glossary
    • πŸ“ Changelog
  • Schemas
    • JWTClaims
    • ErrorObj
    • PingObj
    • ErrorResponse
    • DepositObj
    • PingResponse
    • DepositResponse
    • DepositStatusObj
    • DepositStatusResponse
    • DepositWebhookBody
    • DepositsResponse
    • DepositStatus
    • UserInfoResponse
    • WithdrawStatusResponse
    • WithdrawStatusObj
    • WithdrawResponse
    • WithdrawObj
    • WithdrawStatus
    • WithdrawWebhookBody
    • MEDWebhookBody
    • RejectionReasons
  1. Webhooks

πŸͺ Webhooks

Webhooks are still in beta
Breaking changes to the payloads and events on this page may still happen. When one does, we announce it in advance β€” on the Changelog and in Eulen's partner announcements group on Telegram β€” so you have time to adapt before it takes effect.

Overview#

A webhook is a way for our system to send real-time events to your server via HTTP requests, without your server needing to ask for the event. You register a URL that will receive events right after they happen β€” a deposit clears, a withdrawal completes, or a MED is filed against a past deposit.
See also: Possible Statuses

Registering a webhook#

Registration is self-service. On your Telegram channel with Eulen:
/registerwebhook <type> <url> <secret>
Type: What kind of event data will be sent. Currently, webhook type can be:
deposit
withdraw
med
URL: The URL that events will be sent to. It must start with https:// β€” plain http://, localhost, and private address ranges (10.*, 172.*, 192.168.*) are rejected, so a tunnel with a public HTTPS hostname is the way to test against a local server.
Secret: You should create a secret of at least 16 characters. For maximum security, you can generate 32 random bytes as hex to use as a secret. You can do this by running this command on your terminal: openssl rand -hex 32.
Example Telegram command:
/registerwebhook deposit https://example.com/webhooks/deposit <your-secret>
Each type is registered separately, so a deposit webhook and a withdraw webhook can point at different URLs and use different secrets.

Changing or removing a webhook#

There is no update command. To point a type at a new URL, delete it first and register again:
/deletewebhooks deposit
/registerwebhook deposit https://example.com/webhooks/deposit-v2 <new-secret>
/deletewebhooks <type> removes every registration of that type for your account and answers There are no webhooks registered for this type. when there was nothing to remove. Use it on its own to turn a webhook off.
Never leave two URLs registered for the same type
Registering a second URL for a type you already registered is accepted, and the reply still says βœ… Webhook registered successfully β€” but it is not fan-out. Only one of the two endpoints receives the events, which one is not defined, and it can change between deliveries.
Always /deletewebhooks <type> before registering the replacement. During a migration, run the old and the new endpoint behind a single URL on your side.

Authorization Header#

Every delivery carries the secret you registered, verbatim, after Basic :
Authorization: Basic <your-secret>
The value is not re-encoded β€” whatever string you gave to /registerwebhook is exactly what arrives. That matters if your framework parses the header as standard HTTP Basic auth, because a hex secret is not valid Base64 credentials and the parse will fail.
If you want the standard form, register the Base64 of username:password as the secret. For example, partner:your-password becomes cGFydG5lcjp5b3VyLXBhc3N3b3Jk, and that is the string you register.
To encode the credentials in Base64, you can use this command on your terminal: echo -n 'username:password' | base64
Compare the header in constant time and reject anything that does not match. The secret is the only proof that a delivery came from us.

Responding to webhooks#

Only 200 and 201 count as delivered
A 202 Accepted, a 204 No Content, a redirect, or any other status is recorded as a failed delivery β€” even though your server processed the event fine. If you acknowledge before processing, answer 200, not 202.
Please remember to:
⚠️ Verify the Authorization header to ensure the request is coming from the correct source.
Return 200 OK (or 201) on successful processing.
Return a response within 15 seconds. After that the connection is dropped and the delivery counts as failed.
Use the deposits API as a fallback in case the webhook fails.
Keep the handler fast: do the heavy lifting (database writes, downstream calls) in a background job, and still answer 200.
Process idempotently. Use a stable identifier from the payload (qrId for deposits, id for withdrawals) as a dedupe key, and make a repeat delivery a no-op.
One delivery attempt, with a single exception
For every event on this page but one, there is no automatic retry. If your endpoint is down, slow, or answers anything other than 200/201, that event is not redelivered β€” you get a warning message in your Telegram channel with Eulen, and nothing else.
The exception is the deposit webhook announcing that a deposit was refunded. That one is delivered by a durable job, which retries a failed send for a limited number of attempts with growing intervals between them, so the same refund can reach you more than once. It changes nothing about what you build: dedupe on qrId as described above and a repeat is a no-op.
Reconciliation is mandatory rather than optional: a periodic sweep with GET /deposits is the only way to recover an event you missed. Outside that one case, do not build a design that waits for a second attempt.
Delivery order
Deposit webhooks for the same payment are delivered in order. Withdraw and MED webhooks are not ordered, and a status you already passed may still arrive after a newer one β€” compare against the state you have stored before overwriting it.
Discriminate on webhookType
Every webhook body carries a webhookType field. Switch on it to route the payload to the right handler. The three values are "deposit", "withdraw", and "med".

deposit event#

Sent when a deposit reaches a terminal or notable state. The status field carries the same values as GET /deposit-status, and the payload mirrors that response plus the webhookType discriminator. The full set of values is on Possible Statuses.
DepositWebhookBody

FieldTypeDescription
webhookTypestringAlways "deposit".
bankTxIdstringBank transaction ID for the Pix payment.
blockchainTxIDstringLiquid Network transaction ID for the DePix transfer.
customerMessagestringHuman-readable message about the deposit.
payerNamestringName of the payer.
payerTaxNumberstringCPF/CNPJ of the payer.
pixKeystringPix key used for the payment.
qrIdstringID of the deposit (the id returned by POST /deposit).
statusstringCurrent deposit status. See deposit statuses.
valueInCentsintegerDeposit amount, in cents of BRL.
expirationstringQR expiry, ISO 8601.
payerEUIDstringEUID of the payer.
delayUntilstringWhen a delayed deposit will be processed, ISO 8601.
rejectionReasonsarray of stringsCodes explaining why the deposit was held or refused: PAST_DAILY_LIMIT, BLOCKED_USER, HIGH_VELOCITY, PAYER_MISMATCH. The field is absent when none of them applies.
Recommended confirmation status
For crediting your user, confirm on approved β€” the funds have been received and cleared, and DePix will be sent shortly. This fires sooner than depix_sent.
{
  "webhookType": "deposit",
  "bankTxId": "processor_E12345678202601010000a1b2c3d4e5f",
  "blockchainTxID": "ce45cbc2f7bdbf9ddda5e7a9fac1e2135c3be2b342b4a93c9a06032672fbf8cc",
  "customerMessage": "Pedido 4821",
  "payerName": "Maria Silva Souza",
  "payerTaxNumber": "12345678909",
  "pixKey": "550e8400-e29b-41d4-a716-446655440000",
  "qrId": "019b76daa8007000000000000000000d",
  "status": "approved",
  "valueInCents": 15000,
  "expiration": "2026-01-01T00:20:00-03:00",
  "payerEUID": "EU011234567890128",
  "delayUntil": null
}

withdraw event#

Sent as a withdrawal (DePix β†’ Pix payout) moves through its lifecycle. Mirrors the GET /withdraw-status response, plus the webhookType discriminator.
WithdrawWebhookBody

FieldTypeDescription
webhookTypestringAlways "withdraw".
idstringWithdrawal ID (the withdrawalId returned by POST /withdraw).
pixKeystringDestination Pix key.
statusstringCurrent withdraw status. See withdraw statuses.
expirationstringExpiry of the withdrawal, ISO 8601.
depositAddressstringLiquid address to send DePix to.
depositAmountInCentsintegerAmount of DePix sent, in cents.
payoutAmountInCentsintegerAmount of BRL paid out, in cents. Not always the value returned by POST /withdraw: when the DePix received diverges from what the withdrawal expected, it is recalculated from what actually arrived. See below.
blockchainTxIDstringLiquid Network transaction ID for the DePix transfer.
receiptUrlstringURL of the payout receipt.
receiverNamestringName of the Pix key owner.
receiverTaxNumberstringCPF/CNPJ of the receiver (masked).
transferDatestringWhen the Pix payout was made, ISO 8601.
centralBankIdstringCentral Bank Pix transaction ID.
refundAddressstringThe Liquid address you declared on POST /withdraw for returning the DePix if the payout cannot be completed, echoed back so a typo is visible before a refund needs it. The field is absent when you did not supply one.
{
  "webhookType": "withdraw",
  "id": "019b76daa800700000000000000000a1",
  "pixKey": "example@example.com",
  "status": "sent",
  "expiration": "2026-01-01T00:20:00-03:00",
  "depositAddress": "lq1qqtkuz7nwp3cymteg0px5pcltrm26uvsak32t04fawfsacafutjg22fhvc8xhj0ue96cevuatnmt757rf4z2r5cwntsemgngcr",
  "depositAmountInCents": 124647,
  "payoutAmountInCents": 123400,
  "blockchainTxID": "79ad78f779839d2cbbe59612c7f9eff0919892ae601888b40ab877607ea54217",
  "receiptUrl": "https://resources.eulen.app/receipt/pix/ddb95453-25f8-57f7-8a89-2225cfb18540",
  "receiverName": "Maria Silva Souza",
  "receiverTaxNumber": "***.456.789-**",
  "transferDate": "2026-01-01T00:01:12-03:00",
  "centralBankId": "E12345678202601010000a1b2c3d4e5f"
}

When the DePix received does not match#

The DePix you send to fund a withdrawal is compared against the amount that withdrawal expects.
Within 5%, in either direction, the withdrawal proceeds and the payout is recalculated from the amount that actually arrived. payoutAmountInCents on this webhook is the recalculated value, not the one POST /withdraw returned. Reconcile against this field, never against the value you stored at creation.
Above 5%, the deposit is ignored and the withdrawal is left untouched. You are notified on Telegram, and the notification names the withdrawal id and the transaction id.
A recalculated payout is checked again against the R$ 2,00 to 6.000,00 withdrawal range. Falling outside it also ignores the deposit, which is why a deposit slightly under the expected amount can be dropped: after fees there is less than the minimum left.
When the deposit is ignored and you declared a refundAddress, the DePix is returned to it automatically, in full and with no fee withheld, and the withdrawal moves to refunded. No withdraw event is sent for this return: the announcement is on Telegram and it names the Liquid transaction id, so read GET /api/withdraw-status if you reconcile from the API. Without a refundAddress the withdrawal is left as it is and can still be funded by a new deposit of the correct amount.

med event#

Sent when a MED (Pix special refund / chargeback) is filed against a past deposit. Use it to reconcile the affected deposit on your side.
No enablement step
med is a webhook type like the other two: register it with /registerwebhook med <url> <secret> and the events start arriving. There is nothing to switch on, and nothing to ask for.
MEDWebhookBody

FieldTypeDescription
webhookTypestringAlways "med".
qrIdstringID of the deposit the MED was filed against.
bankTxIdstringBank transaction ID of the original Pix payment.
blockchainTxIDstringLiquid Network transaction ID for the original DePix transfer.
creationDateReportstringWhen the MED was filed, ISO 8601.
euidstringEUID associated with the deposit.
namestringName tied to the report.
partnerIdstringYour Partner ID.
principalValueInCentsintegerPrincipal amount recorded for the MED, in cents of BRL. It is not always the amount in dispute β€” reconcile against the deposit it names, not against this figure alone.
taxNumberstringCPF/CNPJ tied to the report.
{
  "webhookType": "med",
  "qrId": "019b76daa8007000000000000000000d",
  "bankTxId": "processor_E12345678202601010000a1b2c3d4e5f",
  "blockchainTxID": "ce45cbc2f7bdbf9ddda5e7a9fac1e2135c3be2b342b4a93c9a06032672fbf8cc",
  "creationDateReport": "2026-01-02T00:00:00-03:00",
  "euid": "EU011234567890128",
  "name": "Maria Silva Souza",
  "partnerId": "acme",
  "principalValueInCents": 15000,
  "taxNumber": "12345678909"
}

Reconciliation / missed webhooks#

Webhook delivery is best-effort and single-attempt, so reconciliation is part of a correct integration rather than a safety net you add later. If a delivery is missed β€” your endpoint was down, a 200 never reached us, or the response took longer than 15 seconds β€” pull the state directly.
GET /deposits β€” list deposits over a date range, filtered by status. Returns up to 200 rows of { qrId, status, bankTxId }. This is the documented fallback for sweeping over deposits you may have missed.
GET /deposit-status?id=<id> β€” fetch the full current state of a single deposit.
GET /withdraw-status?id=<id> β€” fetch the full current state of a single withdrawal.
Reconcile on a schedule
A periodic sweep with GET /deposits (for example, over the last few hours) is a reliable safety net. Treat your reconciliation job and your webhook handler as two paths to the same idempotent state machine.
Modified atΒ 2026-08-26 01:30:06
Previous
Withdraw Status
Next
Deposit Webhook
Built with