Disputes — Special Return Mechanism (MED)

MED is the process through which the payer of a PIX transfer requests the money back, usually on suspicion of fraud. When a dispute is filed against one of your accounts, you receive a webhook, have 48 hours to present your defense, and we forward it to the team that conducts the dispute.

Three facts shape how this module works, and they are worth knowing before you integrate:

  • The dispute is not decided by this API. There is no route to refund the PIX, hold balance or refuse the refund. What you do here is record your side of the case; conducting it within the scheme happens outside the API.
  • The API does not show dispute status. The settlement bank offers no infraction-report lookup — what we know is what the last webhook brought, and there is no way to confirm it still holds. Instead of showing a state that could be days old, the API exposes what is verifiable: whether you answered (answered) and what your deadline is (clientAnswerDeadline).
  • The defense accepts attachments. Unlike the plain text of before, you can upload documents (delivery receipt, invoice, screenshot of a conversation) and they travel along with your answer.

Response deadline

clientAnswerDeadline is always opening + 48 hours, and it is the only deadline exposed by the API.

Nothing is auto-rejected on time. Missing the deadline neither closes the dispute nor triggers a refund by itself: it means the case moves on without your side of the story. Watch clientAnswerDeadline — or the dashboard, which warns you when fewer than 12 hours remain.

Receiving disputes by webhook

pix.med.opened on filing, pix.med.updated on every change the settlement bank reports. Subscribe to both.

{
"id": "pix-med-opened-204cc938-da3d-4f04-baf3-0b2e6a2f1283-OPEN",
"type": "pix.med.opened",
"occurredAt": "2026-02-18T19:34:14.000000000Z",
"schemaVersion": "1.0",
"environment": "production",
"tenantId": "tenant-yourcompany",
"accountId": "773107de-...",
"data": {
"medId": "204cc938-da3d-4f04-baf3-0b2e6a2f1283",
"tenantId": "tenant-yourcompany",
"accountId": "773107de-...",
"originalEndToEnd": "E303062942026021812490000005QLMv",
"amount": 1000.00,
"reasonCode": "unauthorized-transaction",
"claimMessage": "I do not recognize this transaction",
"openedAtIso": "2026-02-18T19:34:14.000000000Z",
"clientAnswerDeadlineIso": "2026-02-20T19:34:14.000000000Z",
"status": "OPEN"
}
}

The status in the webhook is the state at the instant of that event — and in that it is reliable, because it is the settlement bank’s own notification. That is exactly why it does not appear in the GET: there it would be a stored value, with no lookup to confirm it. If your system needs to track the state of a dispute, accumulate the pix.med.updated events; they are the only source.

A redelivery of the same state produces no new event, and a late event (one whose settlement-bank timestamp predates what we already stored) is dropped — you never see a dispute move backwards.

reasonCode is the scheme’s own code, passed through as received (unauthorized-transaction, fraud, scam-or-fraud, …). We do not normalize it into an enum of ours.

The report identifies the claimant’s bank, not the claimant’s name or tax ID. That information is not made available to the receiving party.

Listing disputes

GET /v1/accounts/{accountId}/pix/med?limit=50&offset=0

Paginated by limit (max 200) and offset. There is no status filter.

{
"accountId": "773107de-...",
"tenantId": "tenant-yourcompany",
"items": [
{
"medId": "204cc938-da3d-4f04-baf3-0b2e6a2f1283",
"accountId": "773107de-...",
"tenantId": "tenant-yourcompany",
"endToEndId": "E303062942026021812490000005QLMv",
"transactionId": "tx-8f2c...",
"amount": 1000.00,
"reasonCode": "unauthorized-transaction",
"claimMessage": "I do not recognize this transaction",
"claimantBank": "Claimant's bank",
"openedAt": "2026-02-18T19:34:14Z",
"clientAnswerDeadline": "2026-02-20T19:34:14Z",
"answered": false,
"updatedAt": "2026-02-18T19:34:16Z"
}
],
"count": 1,
"limit": 50,
"offset": 0
}

transactionId links the dispute to the PIX entry in your statement, when that entry exists on the platform. After you respond, answered becomes true and answeredAt, answerResult and answer (the justification you sent) appear.

Attaching evidence

Upload the files before you answer: the answer is what closes the package and triggers the forwarding, and an attachment added later does not make it in.

There are three steps. The file goes straight from your side to storage, without passing through the body of our API.

1. Ask for the upload URL

POST /v1/accounts/{accountId}/pix/med/{medId}/evidence/upload-url
{
"filename": "comprovante-entrega.pdf",
"contentType": "application/pdf",
"sizeBytes": 184320
}
{
"medId": "204cc938-...",
"evidenceId": "medev_9f1c...",
"key": "med-evidences/tenant-suaempresa/204cc938-.../medev_9f1c...-comprovante-entrega.pdf",
"uploadUrl": "https://...",
"method": "PUT",
"contentType": "application/pdf",
"sizeBytes": 184320,
"expiresIn": 900
}

sizeBytes is the file’s actual size, not an estimate: it is part of the URL signature. Declaring one number and sending a different one makes storage refuse the PUT.

2. Send the file to the signed URL

PUT straight to the uploadUrl, with the same Content-Type and exactly the sizeBytes you declared. No authentication header of ours — the signature already authorizes the request, and one extra header makes storage refuse it.

3. Register the attachment

POST /v1/accounts/{accountId}/pix/med/{medId}/evidence/add
{
"key": "med-evidences/tenant-suaempresa/204cc938-.../medev_9f1c...-comprovante-entrega.pdf",
"evidenceId": "medev_9f1c...",
"filename": "comprovante-entrega.pdf",
"contentType": "application/pdf",
"sizeBytes": 184320
}

Without this step the file sits in storage and does not make it into the defense: the registration is what links the binary to the dispute. Type and size are checked again here, with the same limits and the same errorCodes from the table below — the two calls are independent.

Every upload goes through real file-type checking, antivirus and sanitization before becoming available — a few seconds. scanStatus tracks that: PENDING while processing, APPROVED cleared, REJECTED refused (with scanReason). A refused file does not travel with the defense.

Limits

LimitValue
Accepted typesapplication/pdf, image/jpeg, image/png, image/webp, text/plain, text/csv
Size per file5 MB
Total per dispute6 MB
Files per dispute10
HTTPerrorCodeWhen
413file_too_largeFile above 5 MB.
413payload_too_largeThe dispute’s total would go past 6 MB.
422unsupported_media_typecontentType outside the list.
422too_many_filesEleventh file on the same dispute.
409conflictDispute already answered — attachments have to come first.

Listing what is attached

GET /v1/accounts/{accountId}/pix/med/{medId}/evidence/download

Returns the dispute’s attachments with a signed downloadUrl (short-lived) for each one that passed the check. With ?evidenceId= it returns only one.

Responding to a dispute

POST /v1/accounts/{accountId}/pix/med/{medId}/answer
{
"result": "DISAGREE",
"reason": "Legitimate sale: customer registered 14 months ago, product delivered, tracking confirmed on Feb 19."
}
FieldRequiredDescription
resultyesAGREE acknowledges the fraud; DISAGREE contests the report.
reasonon DISAGREEYour defense, free text.

202 response:

{
"medId": "204cc938-...",
"result": "DISAGREE",
"answered": true,
"answeredAt": "2026-02-19T14:02:31Z",
"message": "resposta registrada e encaminhada ao time de análise; a CorpX não responde nem decide MED junto ao liquidante"
}

The answer and the attachments go to the team that conducts the dispute. Nothing is sent to the settlement bank by this call, and no status changes: answering records your side of the case, it does not close it.

Errors specific to this route:

HTTPerrorCodeWhen
400invalid_payloadresult outside AGREE/DISAGREE.
409conflictThe dispute was already answered and the forwarding has already gone out.
422invalid_payloadDISAGREE without reason.
404not_foundThe dispute does not exist, or does not belong to this account.

If the dispute is already answered but the forwarding has not gone out yet, repeating the call returns 202 and sends it again — the recorded content is not overwritten.

MED rate

GET /v1/backoffice/tenants/{tenantId}/med-stats?days=30&accountId={accountId}

Returns the deadline counters and the daily rate series:

FieldDescription
totalTotal disputes recorded.
awaitingAnswerDisputes still without your response.
dueSoonUnanswered with fewer than 12 hours left.
overdueUnanswered with the deadline already passed.
openAmountSummed amount of disputes not yet closed.
nextDeadlineThe tightest deadline among pending disputes (empty when none is pending).
stats[]Daily series: date, pixInCount, pixInAmount, medCount, medAmount, quantityRatePercent, amountRatePercent.

The equivalent per-tenant listing is GET /v1/backoffice/tenants/{tenantId}/meds, with the accountId, limit and offset filters.

Reference bands

Operational reference, used in the dashboard and in commercial follow-up. No error or block follows automatically from them — automatic actions by rate are configured in Policies and Rules.

RateSituation
< 0.4%Healthy
0.4% – 1.0%Watch it
> 1.0%High, action may be needed

MED policies

See Policies and Rules for:

  • Auto-block: automatically hold balance on disputes above a given amount.
  • Max rate: MED/PIX-in ratio ceiling with automatic actions.