Webhooks
Webhooks are notifications sent by our API to your registered URL whenever a relevant event occurs (e.g., a PIX is received or a payment is completed).
Webhook Configuration
How you create and maintain subscriptions depends on your product:
BaaS (tenant)
Internet Banking (account)
You have two equivalent, always-in-sync paths:
- API —
POST /v1/webhooksand the other routes described on this page. This is how you automate the setup alongside the rest of your deployment. - Integrator Portal — besides creating and editing subscriptions, this is where you run day-to-day operations: resend failed deliveries, debug errors (HTTP status and body returned by your endpoint), browse the delivery history of every event and rotate the HMAC key without interrupting the flow.
Use the API to provision and the Portal to operate and investigate.
List Available Events
Response (array of { event, description }; illustrative excerpt):
The full list also includes PIX out/refund/MED, QR Code, boleto, TED, internal transfers, and fees. Onboarding events are documented in Onboarding Webhooks.
Receiving Flow
- An event occurs on our platform.
- We send a
POSTrequest to the registered URLs. - Your application must process the notification and return a
2xxstatus.
Configuration Flexibility
Our webhook infrastructure supports various delivery methods:
- Grouping: You can receive multiple event types (e.g.,
pix.in.completedandpix.out.completed) at the same URL. - Segregation: You can configure different URLs for each event type.
- Redundancy: We can send the same event to multiple independent URLs simultaneously.
- Per-account scoping: a subscription can receive only one account’s events — see below.
Per-account subscriptions (accountId)
By default a subscription belongs to the tenant: it receives events from all of your accounts. Passing accountId at creation time makes it receive only that account’s events:
The field comes back in GET /v1/webhooks (accountId: null for tenant-wide subscriptions).
Rules worth knowing before you use it:
- It is a filter, not extra routing. A subscription with
accountIdstops receiving the other accounts’ events. If you want everything in one place and a slice in another, create two subscriptions. - It cannot be edited.
accountIdis only accepted at creation. Changing the account of a live subscription would silently redirect one account’s event stream to the endpoint configured for another — creating a new subscription is explicit and leaves the old one auditable. - A credential restricted to accounts must provide it. If the credential only reaches a subset of accounts (the case for delegated credentials), omitting
accountIdreturns 422account_id_required, and pointing at an account outside the subset returns 403forbidden. Without this, a single-account credential could create a subscription for another account’s movement and read everything over the webhook, bypassing the control the read routes apply. - An account from another tenant returns 404
account_not_found. - Events with no account (accreditation events, before the account exists) are only delivered to tenant-wide subscriptions.
Security (Destination Authentication)
When creating or updating a webhook subscription, you can choose how our delivery infrastructure authenticates requests to your endpoint. The authentication method is configured per subscription via the API or dashboard.
Available Authentication Methods
You configure the method when creating or updating a subscription, via the API or the Integrator Portal (Webhooks > Edit Subscription).
Rotating or removing the key
The secret is never returned in any response — the subscription only tells
you hmacSecretSet: true|false. Because of that:
- Omitting
secreton aPUTkeeps the current key. This is what lets you change only the URL without losing the signature. - Sending
secretreplaces the key (rotation). - Sending
authType: "NONE"deletes the key and stops signing with it. This is the only way to turn it off.
Any other authType value is rejected with 400 unsupported_auth_type.
HMAC Signature Verification
When authType is set to HMAC, each request includes an X-Signature header containing a Base64-encoded HMAC-SHA256 hash of the raw request body, computed using your secret as the key.
Formula:
Compare the computed value with the X-Signature header. If they match, the request is authentic.
Always use the raw request body bytes for verification, not a re-parsed/re-serialized version. Re-serializing JSON may change field order or whitespace, which will invalidate the signature.
Verification Examples
Node.js:
Python:
Go:
Informational Headers
In addition to the authentication headers above, our delivery infrastructure adds the following informational headers to every request:
These headers are informational and do not need to be validated.
IP Whitelist
To increase the security of your integration, we recommend that your destination server validates the source IP address of incoming requests. Only accept notifications from our infrastructure’s official IPs:
34.138.140.22334.138.161.10035.231.250.19335.196.71.2934.138.56.192
We suggest adding these addresses to a whitelist in your firewall or web server.
Retries
If your application returns an error (status other than 2xx) or a timeout occurs, our system will attempt to resend the notification following an exponential backoff strategy:
- Attempts: Up to 6 times.
- Intervals: Progressively increasing.
After exhausting all attempts, the delivery is marked as failed. You can request a manual retry of that specific delivery.
Retrying a Delivery
To resend a failed delivery, use the per-delivery retry endpoint, passing the
subscriptionId and deliveryId (both visible in the subscription’s delivery
list):
Request Example:
The API queues a new delivery attempt for that deliveryId.
Notification Format
All notifications follow a standard envelope format. The specific content of each event resides in the data object.
Standard Envelope
Monetary values (amount, paidAmount, etc.) are always in BRL. Decimals use a point (150.50), never a comma. PIX-out events (pix.out.completed, pix.out.failed, pix.out.timeout, and pix.refund.* from the same flow) include data.currency: "BRL". Other events may omit the field — the amount is still in reais.
The POST to your URL is the envelope (id, type, occurredAt, schemaVersion, environment, tenantId, accountId, data). In the integrator portal, the “webhook delivered” JSON shows only the data object (sometimes labeled deliveredData) — that is not the HTTP body.
Common fields inside data
A handful of fields appear in every event type that represents a ledger entry (PIX IN/OUT, refund, QR paid, internal transfer, fee). They are the recommended way to correlate webhooks with other API endpoints:
The first three fields can coexist in the same payload. If only one is present it means the other isn’t relevant for that event type (e.g., internal transfers carry transactionId + coreId but not endToEnd).
data.status values
The status field uses a standardized vocabulary across all events:
For MED (dispute) events, status uses its own vocabulary:
Events: pix.med.opened, pix.med.updated.
This status is the state at the instant of the event, and in that it is
reliable: it is the settlement bank’s own notification. That is why it does not
exist in GET /v1/accounts/{id}/pix/med — there it would be a stored value,
with no lookup to confirm it. If you need to track the state of a dispute,
accumulate these events: they are the only source.
status guarantee
status is always present in events that represent a ledger entry. If you receive a webhook missing the field (or with an empty value), it’s a purely informational event (e.g., account.balance_updated) — in that case use the type field itself to determine semantics.
Date and time format
Date/time fields use ISO 8601 / RFC 3339 with an explicit offset — read the offset, don’t assume. Webhooks (envelope occurredAt and fields inside data such as receivedAt, completedAt, initiatedAt, chargedAt, openedAt) are always UTC (Z). In REST responses, “our-side” fields (createdAt, updatedAt, reconciledAt, fetchedAt, balance) are UTC (Z) too; the transaction time in the statement and lookups — timestamp (/statement, /pix/transactions) and occurredAt (/pix/payments/lookup, boleto, fee.occurredAt) — is in São Paulo time (-03:00).
Examples:
We never emit timestamps in Brazilian local time (BRT) nor “naive” timestamps (without timezone indicator). If you ever receive a date field without the Z suffix, treat it as a bug and report it — we will normalize as soon as we identify the source.
Event Types and Contents (data)
1. pix.in.completed
Sent when a PIX is successfully received (inbound) into the account.
Fields always present in data:
transactionId: Ledger transaction ID (the settlement-bank operation id when available; otherwisepix-in-{endToEnd}). MatchesGET /v1/accounts/{accountId}/statement. The envelopeidispix-in-{endToEnd}.endToEnd: Unique transaction ID at the Central Bank.accountId/tenantId: repeated insidedata(they are also on the envelope).amount: Transaction amount in reais.status:"SUCCESS".method: How the payer started the PIX —STATIC_QR_CODE,DYNAMIC_QR_CODE,DICT(PIX key) orMANUAL(bank details).receivedAt: Timestamp of receipt (RFC 3339 UTC).
Conditional fields:
identifier: When the credit came from a QR Code, this is the txid of the QR that was paid — use it to reconcile with the QR you created.description: Description from the settlement bank (may be an empty string).payer: Sender. Only keys with a value are included:name,document,bankCode,bankIspb,branch,account/accountNumber(same value;accountNumberis a compatibility alias).payee: Receiver (your account). Same keys, pluspixKeywhen available. Omitted if the settlement bank sent no payee data.
This event does not send reconciliationId.
Full Example:
2. qrcode.paid
Sent when a QR Code generated by you is paid. For the same credit you also receive pix.in.completed — this event is the QR-specific confirmation (identifier / qrcodeId).
Fields always present in data:
qrcodeId: Internal QR Code ID (for a static QR this is the same asidentifier).identifier: txid (identifier) of the QR Code you created.accountId/tenantId: Account that owns the charge.type:"static"or"dynamic".amount: Amount paid.endToEnd: Unique transaction ID at the Central Bank.transactionId: Settlement-bank transaction ID, when available.status:"SUCCESS".receivedAt: Timestamp of receipt (RFC 3339 UTC).
Conditional fields:
payer: Payer. Dynamic QR typically carriesnameanddocument; static QR may also includebankCode,bankIspb,branch,accountNumber.payee: Receiver (your account). Present on static QR when the settlement bank sent those fields.
This event does not send reconciliationId.
Full Example:
3. pix.out.completed
Sent when an outbound PIX transfer is completed successfully.
The POST to your URL is the envelope (id, type, occurredAt, schemaVersion, environment, tenantId, accountId, data). In the integrator portal, the “webhook delivered” JSON shows only the data object (sometimes labeled deliveredData) — that is not the HTTP body.
Fields always present in data:
paymentId: Order ID (same as the202response of the async POST). The envelopeidispix-out-{paymentId}.tenantId,accountId: repeated insidedata(they are also on the envelope).status:"SUCCESS".endToEnd: BACEN E2E ID (empty string if the settlement bank has not issued one yet).transactionId: Settlement-bank transaction ID, when available.amount: Amount in reais.currency: always"BRL".description: Description sent on the POST (may be an empty string).identifier: Your reconciliation key.originalTransactionId: Original transaction ID on refunds; empty string on a regular PIX out.initiatedAt/completedAt: RFC 3339 UTC (Z), with fractional seconds (up to nanoseconds).
Conditional fields:
key:{ "type", "key" }when the transfer was sent to a PIX key.payee: Destination. Only keys with a value are included:name,document,bankCode,bankIspb,branch,accountNumber,pixKey,accountType.payer: Origin account. Present only when the settlement-bank webhook carried those fields — do not assume it always arrives.late:trueonly when confirmation arrived after apix.out.timeoutfor the samepaymentId. Absent in the normal case. See Late confirmation.
This event does not send reconciliationId, method, or a payment object.
Full Example:
4. pix.out.failed
Sent when an outbound PIX transfer fails definitively — the banking partner rejected the operation (insufficient funds, invalid key, anti-fraud, BACEN rejection etc.) and no balance was moved (or it was returned).
pix.out.failed is terminal: once received, no later pix.out.completed will arrive for the same order. Uncertainty scenarios (e.g. communication timeout with the partner, outcome unconfirmed) emit pix.out.timeout — never pix.out.failed.
data uses the same fields as pix.out.completed, plus:
status:"FAILED".error/errorCode/errorReason: rejection reason (erroris the friendly alias).partner: raw settlement-bank error, when available.late:trueonly when the rejection arrived after apix.out.timeoutfor the samepaymentId. See Late confirmation.
This event does not send reconciliationId.
Full Example:
5. payment.sent (removed)
This event was deprecated in v1.15.0 (2026-02-20) and has been permanently disabled in v1.28.0 (2026-04-17). Use pix.out.completed with method: PAYMENT and the payment object in the payload instead.
6. payment.refunded (removed)
This event was deprecated in v1.15.0 (2026-02-20) and has been permanently disabled in v1.28.0 (2026-04-17). Use pix.refund.completed instead. That event does not include originalEndToEnd or refundEndToEnd — the refund E2E is endToEnd, and the original transaction is originalTransactionId.
7. pix.refund.completed
Sent when a PIX refund we initiated is completed. Different from pix.refund.received, which is a return started by the original payee without us asking.
data uses the same fields as pix.out.completed, with:
- Envelope
id:pix-refund-{paymentId}. status:"SUCCESS".originalTransactionId: ID of the original PIX IN being refunded (not empty, unlike a regular PIX out).
This event does not send originalEndToEnd, refundEndToEnd, or reconciliationId. To correlate with the original credit, use originalTransactionId / identifier and the statement.
Full Example:
8. pix.refund.failed
Sent when a refund we initiated fails definitively.
data uses the same fields as pix.refund.completed / pix.out.failed, with:
status:"FAILED".error/errorCode/errorReason: rejection reason (erroris the friendly alias).partner: raw settlement-bank error, when available.
This event does not send originalEndToEnd, refundEndToEnd, or reconciliationId.
Full Example:
9. fee.charged
Sent when a banking fee is charged to the account (e.g., per-transaction PIX fee).
data structure:
transactionId: Deterministic ledger row id (fee-{coreId}).accountId/tenantId: Charged account and tenant.amount: Fee amount, always positive in the account currency.description: Fee description.feeServiceType: Underlying service that triggered the fee (PIX,BOLETO, …) when the bank reports it.originalRef/transactionRef: Cross reference to the originating movement when available.occurredAt: Timestamp at which the bank booked the fee.status: Always"SUCCESS".kind:"CHARGED".
Full Example:
9.1. fee.refunded
Sent when the bank reverses a previously charged fee (cashback, adjustment). Same shape as fee.charged, with kind="REFUNDED". Funds are already credited to the account.
10. pix.med.opened
Sent when a new MED (Special Return Mechanism) dispute is opened against a transaction credited to one of your accounts. It starts the 48-hour clock for your response — see the Disputes guide.
data Structure:
medId: Dispute identifier (infraction report). It is the{medId}used by the REST routes.originalEndToEnd:endToEndIdof the disputed PIX transaction.amount: Disputed amount.reasonCode: Reason reported by the scheme, raw (e.g.scam-or-fraud,unauthorized-transaction).claimMessage: Free-text details provided by the claimant (when present).openedAtIso: MED opening date (ISO 8601).clientAnswerDeadlineIso: Your response deadline —openedAtIso+ 48h.status:OPENon filing.
Full Example:
10. pix.med.updated
Sent when the status of a MED dispute changes. This is how you learn the outcome, and it is the only source of state: POST .../answer merely records your defense (which goes on for analysis outside the API), and the dispute also moves through action by BACEN and by the claimant’s bank.
A redelivery of the same status produces no new event, and a late event (settlement-bank timestamp older than what we already stored) is dropped — the status never moves backwards.
data Structure:
medId: Dispute identifier (same aspix.med.opened).originalEndToEnd:endToEndIdof the disputed PIX transaction.amount: Disputed amount.reasonCode: Reported reason.clientAnswerDeadlineIso: Your response deadline (the same as on filing).status: Current MED status — dedicated vocabulary (OPEN,PENDING_DECISION,ACCEPTED,REJECTED,CANCELED; see the status table above).
Settlement bank status mapping — the raw value never leaves in the event:
An unknown status becomes OPEN, not a closure: missing the filing of a dispute costs you your response window, while receiving one extra only costs a lookup.
Full Example:
11. transfer.internal.in
Sent to the account that receives an internal transfer between accounts of the same banking ecosystem. Internal transfers are instant and settled at the same bank — they do not travel through the PIX SPI, so they have no endToEndId.
data structure:
transactionId: Transaction ID in our system (formatinternal-{uuid}). Use it onGET /v1/accounts/{accountId}/statement.amount: Amount received.status: AlwaysSUCCESS(internal transfers are atomic).completedAt: Settlement timestamp.identifier: Identifier the sender supplied onPOST /transfers/internal*(when given). The same value arrives on both legs (transfer.internal.outandtransfer.internal.in).payerDescription/description: Description supplied by the sender.source: The payer —name,taxId,providerAccountIdand, when the account is managed by us,accountIdandtenantId.destination: The receiver, with the same fields.payer/receiver: The same two parties, withdocumentin place oftaxId. They appear only when the transfer was initiated outside the API (the liquidator’s app or console). Kept for compatibility — prefersource/destination, which arrive from both origins.
When the receiver is an account at another bank, destination carries name and taxId (the ones you supplied in the request) without accountId/tenantId. In that case only transfer.internal.out exists.
Full Example:
12. transfer.internal.out
Sent to the account that sends an internal transfer. Same shape as transfer.internal.in, with two differences:
identifieris populated on both legs when you passedidentifierin the original request (POST /v1/accounts/{accountId}/transfers/internal*). Use it to reconcile the transfer on your side.description— if you passeddescriptionin the request, that same text comes back here (instead of the defaultTransferência Interna).
Example:
When calling POST /v1/accounts/{accountId}/transfers/internal (or the by-document / by-bank-account variants), send your own unique identifier. The API response returns that identifier together with transactionId. The transfer.internal.out webhook arrives afterwards with the same identifier + transactionId, so you can close the transaction on your side without querying the statement.
13. pix.out.timeout
Sent when the order outcome is indeterminate at the end of the confirmation window: the partner call was dispatched but the response/final confirmation did not arrive in time (this includes communication timeouts on the submit call itself). Not a definitive failure — a late pix.out.completed or pix.out.failed may still arrive afterwards. Check the statement before retrying with a new idempotency key; retrying with the same key is safe.
data structure (fields specific to this event):
status: Always"TIMEOUT".errorCode/errorReason/warning/error: What to do next (confirmation_timeout).hold: Present when the order is being held at the settlement bank. Carriesowner: "partner"andreason(partner_authorization= the settlement bank’s internal authorisation queue;partner_risk_analysis= risk review;partner_unspecified= held, queue unknown). When this object is present, there is nothing to approve on your side or on ours — the decision belongs to the settlement bank.partnerStatus/partnerStatusId: Raw snapshot of the state at the settlement bank, useful to attach to a support ticket. These are the settlement bank’s values, not our canonical vocabulary: do not use them in status comparisons.
Late confirmation (late: true)
TIMEOUT is an indeterminate state, not a final one. When the order is held at the settlement bank, we keep polling it for up to 7 days, and if it settles (or rejects) within that window you receive the matching terminal event:
pix.out.completedwithlate: true— the money left the account.pix.out.failedwithlate: true— the settlement bank rejected it.
The late event carries the same paymentId as the pix.out.timeout you already received: it corrects that outcome, it is not a second payment. Treat it as the order’s definitive outcome.
After 7 days without news no further event is sent and the order stays TIMEOUT — which literally means “we do not know”. Check the statement (GET /v1/accounts/{accountId}/statement) before reissuing. Reissuing with the same Idempotency-Key is safe; a new key may result in a duplicate payment.
14. pix.refund.received
Sent when the receiver of a PIX out returns the funds on their own initiative (without us asking). Different from pix.refund.completed, which confirms a refund we triggered.
data structure:
transactionId: Deterministic ID of the received refund row (pix-refund-received-{refundEndToEnd}).refundEndToEnd: Refund D-code (BACEN).originalEndToEnd: E-code of the original PIX out being reversed.originalIdentifier: Identifier of the original PIX out (echo of the value sent at payment creation), when available.accountId/tenantId: Account that received the refund.amount: Reversed amount.description: Description from the partner.receivedAt: Refund timestamp.payer: Party that returned the funds (counterparty / original PIX out recipient): name, document, bank, branch, account.payee: Party that received the credit (our account / original PIX out payer): name, document, bank, branch, account.status: Always"SUCCESS".
15. qrcode.expired
Sent when a dynamic QR Code expires without being paid within the configured expiration.
data structure:
qrcodeId: Internal QR Code ID.identifier: txid of the QR Code you created.accountId/tenantId: Account that owns the charge.type:"dynamic"(expiration only applies to dynamic QR Codes).amount: Amount that was awaiting payment.status: Always"EXPIRED".
16. qrcode.cancelled
Sent when the integrator explicitly cancels a dynamic QR Code via DELETE /v1/accounts/{accountId}/pix/qr-code?identifier={txid} before payment.
data structure:
qrcodeId: Internal QR Code ID.identifier: txid of the QR Code you created.accountId/tenantId: Account that owns the charge.type:"dynamic".status: Always"CANCELLED".reason/cancelledBy: Present when the cancellation carries a reason and who cancelled it.
17. boleto.paid
Sent when a boleto paid via POST /v1/accounts/{accountId}/boleto/pay is confirmed by the issuing bank.
data structure:
paymentId: Canonical boleto id (bol_{uuid}) — same value returned byPOST /boleto/pay. Use this to correlate. The envelopeidisboleto-{paymentId}.boletoId: Alias ofpaymentId(same value).partnerId: Settlement-bank reference (operationReferenceId), for support/reconciliation.accountId/tenantId: Payer account.amount: Amount paid.line: Digitable line / barcode.status: Always"SUCCESS".
This event does not send transactionId.
18. boleto.failed
Sent when the issuer rejects the payment. data.error is a string with the reason (not an object). The same text is also in data.errorReason; data.errorCode is present when the settlement bank sent a code.
19. ted.out.requested
Sent immediately after POST /v1/accounts/{id}/ted/out is accepted. Status PROCESSING — TED registered at the settlement bank, waiting for the BACEN window to settle. See the TED guide.
20. ted.out.confirmed
Settlement confirmed by the settlement bank (terminal — success).
Deprecated alias: the
ted.paymentevent is dispatched in parallel withted.out.confirmedwith the same payload, kept only for compatibility with legacy subscriptions. New integrators should subscribe toted.out.confirmedonly.ted.paymentwill be removed in v3.0.
21. ted.out.failed
Settlement rejected/expired (terminal — failure). data.errorReason carries the canonical reason.
Possible errorReason values: invalid_bank_code, insufficient_funds, outside_banking_hours, bank_unreachable, limit_exceeded, or timeout aguardando confirmação do parceiro (>48h) (defensive polling exhausted).
22. ted.in.received
A TED was received in your account (originated by a third party). Includes data.payer (name, document, bank, branch, account) when the settlement bank provides those fields — use them to reconcile with your books.
Remember: for third parties to send a TED to your account, they must use bank 681 (MT Instituição de Pagamentos) — see the TED guide.
23. policy.violation
A policy rule configured for your account or tenant was violated. The event covers every policy section — PIX out, PIX in, QR Code, keys, and refunds — and is sent both when the operation is rejected (action: "BLOCK") and when it only produces a warning (NOTIFY_ONLY in monitor mode, ALLOW_AND_NOTIFY and AUTO_REFUND on incoming PIX). Use blocked to tell them apart without interpreting the action.
When blocked is true on a PIX out, you also receive the matching pix.out.failed, with errorCode: policy_denied. They are two events with distinct ids: this one is the rule warning, that one is the payment outcome.
In monitor mode (NOTIFY_ONLY) the payment goes through normally and will have its own outcome (pix.out.completed, for example) — this event is only a heads-up that the transaction would have been rejected had the rule been set to BLOCK.
Violation on an incoming PIX (phase: "pix_in")
An incoming PIX cannot be refused: the settlement bank credits the account and only then tells us. The violation always comes after the credit, and the payload swaps paymentId for transactionId and endToEnd, adding refunded:
pix.in.completed is delivered either way, and before this event. With refunded: true, the refund then produces the usual PIX out events (pix.out.completed or pix.out.failed).
Rule and configuration details are in the Policies and Rules guide.
Best Practices
- Idempotency: Your application should be prepared to receive the same webhook more than once. Use the envelope
idto avoid duplicate processing. - Quick Response: Return a
200 OKstatus as soon as you receive the webhook and process the business logic asynchronously to avoid timeouts. - Timestamp Validation: Check that the
occurredAtis not too old (we recommend a 5-minute tolerance) to prevent replay attacks.