Disputes (MED)
How a contested PIX reaches you, what the four dispute events carry, and how to handle each leg.
What a MED is
A MED (Mecanismo Especial de Devolução) is the PIX contestation the payer's bank opens when the payer reports a transfer as fraudulent. Bacen gives the payer's bank a window measured in days to open it — long after the PIX settled and long after the asset was delivered.
That timing is the whole reason disputes need their own handling:
- A PIX lands on the account and funds one of your operations.
- The asset is delivered on-chain, irreversibly, within seconds.
- Days later the payer's bank opens a MED. The disputed amount is frozen on the receiving account.
- The MED is accepted, rejected, or withdrawn.
By the time you receive DISPUTE_CREATED, the money has already left the chain.
The event is a fraud signal about the payer, not a payment you can still
stop.
Do not treat a dispute as a reversible order. Act on the end user — freeze the account, force re-KYC, block further deposits — not on the blockchain.
Subscribing
Dispute events are registered like any other webhook, from the Hodle dashboard:
- Open API Keys → Webhooks and click Configurar Webhook.
- Enter your public HTTPS endpoint.
- Select the dispute events you want:
DISPUTE_CREATED,DISPUTE_ACCEPTED,DISPUTE_REJECTED,DISPUTE_CANCELED.
Each event is a separate subscription, so you can register only the legs you act on. Signature verification, headers, and the secret are identical to every other event — see Webhooks.
The four events
| Event | What happened |
|---|---|
DISPUTE_CREATED | The MED was opened. The disputed amount is frozen on the receiving account. |
DISPUTE_ACCEPTED | The contestation was accepted — the amount goes back to the payer. |
DISPUTE_REJECTED | The contestation was rejected — the amount stays with you. |
DISPUTE_CANCELED | The contestation was withdrawn before any decision. |
The four events carry the same payload shape, so one handler covers all of
them; branch on event (or on data.status, which always matches it) for the
outcome.
Only DISPUTE_ACCEPTED costs you the money. REJECTED and CANCELED both leave
the amount with you.
Payload
{
"event": "DISPUTE_CREATED",
"data": {
"status": "CREATED",
"providerStatus": "OPENED",
"disputeId": null,
"endToEndId": "E00416968202608231253t7wOSElb8rU",
"value": 10000,
"valueInBrl": "100.00",
"reason": "No intuito de recuperar meu primeiro investimento me pediu mais dinheiro",
"payerName": null,
"occurredAt": "2026-08-23T12:53:00.000Z",
"walletCharge": {
"id": "6650b21c9f4d3a0012ab34cd",
"externalId": "my-order-123",
"trackId": "9f4d3a0012ab34cd6650b21c",
"correlationID": "3f1a9c7e-2b44-4d10-9a51-8c2d6e0f4b73",
"asset": "USDT",
"network": "polygon",
"status": "COMPLETED",
"valueInBrl": "100.00",
"fee": "2.00",
"transactionHash": "0xeafe9c4985963a7a7d6e49f763cca5c6006693031402d46c0da2fced4519fe03",
"createdAt": "2026-08-23T12:52:41.000Z"
}
}
}Fields
| Field | Type | Description |
|---|---|---|
status | string | Normalized status: CREATED, ACCEPTED, REJECTED or CANCELED. Always matches the event. |
providerStatus | string | null | The provider's own status string (e.g. OPENED). Free-form — report it, never route on it. |
disputeId | string | null | Provider id of the dispute, when the provider sends one. |
endToEndId | string | End-to-end id of the contested PIX. This is the deduplication key — one MED per PIX. |
value | number | Contested amount in BRL cents. |
valueInBrl | string | Contested amount in BRL, decimal string. |
reason | string | null | Free-text reason the payer gave their bank. |
payerName | string | null | Name of the payer who opened the contestation, when the provider sends it. |
occurredAt | string | ISO-8601 timestamp of this event. |
walletCharge | object | null | The operation the contested PIX funded. null when the PIX funded no charge — see below. |
status is normalized by us and is the only field safe to branch on.
providerStatus is the provider's own free-form string: it has no documented
domain, so store it for support and never route logic on it.
walletCharge — reconciling against your order
A MED identifies nothing but the contested PIX, by its endToEndId. It carries
no order id, no account id, and no reference to whatever the PIX paid for.
So we resolve it for you. Every incoming PIX has its endToEndId recorded on the
operation it funded, and on a dispute we walk that back and send the operation
inline, so you can match the dispute to the order you already settled without
a second lookup.
| Field | Type | Description |
|---|---|---|
id | string | Hodle id of the charge. |
externalId | string | null | The external id you sent when creating the deposit — your order key. |
trackId | string | null | Hodle tracking id of the operation. |
correlationID | string | null | Provider correlation id of the PIX charge. |
asset | string | null | Asset delivered (USDT, USDC, BRLA, …). |
network | string | null | Network the asset was delivered on. |
status | string | null | Charge status at the time of the event (COMPLETED, FAILED, …). |
valueInBrl | string | Value of the charge in BRL. May differ from the contested amount on a partial MED. |
fee | string | Fee charged, in BRL. |
transactionHash | string | null | On-chain tx hash of the delivery, when there is one. |
createdAt | string | null | ISO-8601 creation timestamp of the charge. |
walletCharge is null when the contested PIX did not fund a charge — a direct
transfer into the account, or a PIX received before we started recording the
endToEndId of incoming payments. The event is still delivered: match it by
endToEndId against your own records in that case.
transactionHash is what proves the delivery happened. Keep it: it is the
evidence that answers the contestation.
Attribution — and its one gap
Because the MED payload has no account identifier, we attribute the dispute by
matching its endToEndId, in order, against:
- the PIX2STABLE delivery that consumed the PIX (which already knows its merchant account),
- the wallet charge the PIX paid,
- the transaction ledger entry for the PIX.
The first match wins and identifies the account — and therefore you, the customer the event is sent to.
If none of the three match, the dispute is still recorded on our side but there
is no customer to forward it to, so no webhook is sent. In practice this
means a PIX that never funded a recorded operation. If you are notified of a
frozen amount you never received an event for, contact support with the
endToEndId.
Attribution is resolved once, when the MED is first seen. Later legs
(ACCEPTED, REJECTED, CANCELED) reuse it, so every leg of one MED is
delivered to the same endpoint with the same walletCharge.
Delivery semantics
- One delivery attempt per event. A
2xxis success; anything else is logged as a failure and not retried — same policy as every other Hodle webhook. - There is no dispute endpoint to poll. Unlike payouts, a dispute cannot be
re-read over the API. Return
2xxfast and process asynchronously; a delivery your endpoint drops is recoverable only through support. - Deduplicate on
endToEndId+status. A PIX has exactly one MED, but the provider can redeliver a leg, so the same(endToEndId, status)pair may arrive more than once. - Expect legs out of order, or missing. A MED may be canceled without ever being accepted or rejected, and it can sit open for days with no further event.
Handling the event
type DisputeData = {
status: 'CREATED' | 'ACCEPTED' | 'REJECTED' | 'CANCELED'
endToEndId: string
value: number
reason: string | null
walletCharge: { externalId: string | null } | null
}
const handleDispute = async (data: DisputeData): Promise<void> => {
const orderRef = data.walletCharge?.externalId ?? null
if (data.status === 'CREATED') {
return flagOrderUnderDispute({
dedupeKey: data.endToEndId,
orderRef,
amountInCents: data.value,
reason: data.reason,
})
}
return closeDispute({
dedupeKey: data.endToEndId,
orderRef,
lostToPayer: data.status === 'ACCEPTED',
})
}Checklist
- On
DISPUTE_CREATED: flag the order, freeze the end user, and keep thetransactionHash— it is your evidence. - Do not attempt to reverse or re-run the delivery. The asset is gone.
- On
DISPUTE_ACCEPTED: book the loss and treat the end user as confirmed fraud. - On
DISPUTE_REJECTED/DISPUTE_CANCELED: close the flag. The money stayed with you.