Disputes and MED (Pix chargeback)
A Pix payment can be disputed after it settles. On Pix the channel for that is the MED (BACEN's Mecanismo Especial de Devolução, the Special Refund Mechanism): the payer asks their own bank to reverse a transaction they claim was fraud, a scam, or a mistake. When a MED is opened against one of your charges, AvioraPay opens a MED case for it, holds the amount from your account while the case runs, and notifies you — by webhook, by email and in the dashboard — so you don't discover the loss only at month-end.
This is different from a refund, which is a reversal you choose to issue. A MED is initiated by the payer, on their bank's side.
Everything on this page is available through the API (/v1/med/cases), the official SDKs
(medCases resource) and the Disputes & MED Cases panel of your dashboard. The three
read and change the same case.
#Lifecycle at a glance
charge paid
│
MED opened by the payer's bank
│
▼
med_case "open" ── amount held from your account,
│ charge becomes "disputed", refund blocked
┌───────────┴──────────────┐
▼ ▼
you accept you appeal (+ evidence)
│ │
▼ ▼
"accepted" "appealed"
(loss taken) │
┌───────────┴───────────┐
▼ ▼
"rejected" "closed"
(MED upheld, (in your favor, or
loss taken) closed without refund)A case can also go straight from open to closed — for example when the payer withdraws
the claim, or the ruling comes back in your favor before you respond.
#What happens when a MED opens
- A MED case is created (
med_case, idmed_…) linked to the disputed charge (charge_id,ch_…). It starts inopen. - AvioraPay holds the amount from your account. The disputed amount is debited from your operational balance (it may go negative) and held while the case runs. If the case closes in your favor, it comes back; if you lose, it was already reserved.
- The charge becomes
disputed. The public charge status (status vocabulary) moves frompaidtodisputed. Adisputedcharge cannot be refunded — a refund would double the reversal (your refund + the bank-executed MED). If the case closes in your favor the charge goes back topaid. - You are notified, through every channel below.
#How you are notified
| Channel | What you get |
|---|---|
Webhook med_case.created | The full med_case object when the case opens, including response_deadline when the deadline is known. |
Webhook med_case.updated | The full med_case object on every status change (appealed, accepted, rejected, closed) and when the platform asks you for evidence (evidence_requested_at). |
Webhook med.resolved | The final destination of the money: refunded_to_merchant, closed_without_refund or lost. |
| "MED opened" and "MED resolved" emails to your account's contact. | |
| Dashboard | Disputes & MED Cases panel, with the amount, the claimed reason and the actions below. |
| API | GET /v1/med/cases and GET /v1/med/cases/{id} — always current. |
Payloads and signatures of the three events are in Webhooks.
No
charge.*webhook is fired for a MED. The charge was paid; a MED is not a charge failure, and it is not a refund —charge.failedandcharge.refundedare not sent when a MED opens, is won or is lost. Subscribe tomed_case.created,med_case.updatedandmed.resolved. Re-reading the charge (GET /v1/charges/{id}) showsstatus: "disputed"while the case is open.
Webhooks are a signal; the API is the source of truth. Outcome notifications (
med.resolved, andmed_case.updatedforaccepted/rejected/closed) are on by default, but the platform can switch them off, and a MED case event that fails to be published is not retried. Reconcile withGET /v1/med/cases(for example once a day, filtered bycreated_after) and treatstatus+money_outcomefrom the API as final.
#Case states
status | Meaning | Money effect |
|---|---|---|
open | MED just opened. Waiting for you to accept or appeal. | Amount held. |
appealed | You appealed (sent your defense); waiting for the outcome. | Amount stays held until the outcome. |
accepted | You accepted the loss without appealing. | Amount stays debited (loss taken). |
rejected | The appeal did not prevail — the MED was upheld. | Amount stays debited (definitive loss). |
closed | The case is closed. Read money_outcome to know where the money went. | Returned to you, or not — see below. |
money_outcome is null while the case is open or appealed, and then tells you the
final destination of the money:
money_outcome | Meaning |
|---|---|
refunded_to_merchant | The held amount was returned to your balance (ruling in your favor, or claim withdrawn). |
closed_without_refund | The case was closed, but the amount was not returned to your balance. |
lost | The case ended as a loss (accepted or rejected); the amount stays debited. |
closedalone does not mean you won. Always readmoney_outcome.
Some outcomes are applied automatically when the payer's bank or BACEN rules on the case; others go through a review by the AvioraPay operations team first, and the case keeps its current status until that review is done.
#Deadlines
response_deadline (ISO 8601, UTC) is the deadline to respond to the case. It is filled in
when the deadline was informed to AvioraPay, and is null otherwise. It is sent in
med_case.created and returned by every read. Plan to appeal (with your evidence) well
before it — the ruling does not wait for you.
#Your defense: appeal, accept and evidence
While a case is open you choose one of two paths:
- Appeal (
POST /v1/med/cases/{id}/appeal) — you contest the MED with a written justification (reason, 5 to 2,000 characters). The case moves toappealed. - Accept (
POST /v1/med/cases/{id}/accept) — you give up contesting. The case moves toacceptedand the amount stays debited.
Evidence (invoice, delivery proof, conversations with the buyer — anything showing the charge was legitimate) is attached with the three-step upload below. You can attach evidence before or after appealing; attach it before the deadline.
Important — what the defense is, and isn't. Your appeal and your evidence are recorded on the AvioraPay platform, where the operations team uses them to assess and handle the case. They are not forwarded automatically to the payment provider, to the payer's bank or to BACEN: today no provider offers AvioraPay a channel to submit a MED defense. Appealing is therefore not a formal submission to BACEN. The official ruling reaches AvioraPay through the MED channel and is reflected on the case (
status,money_outcome) and in the webhooks above.
#API
All routes use your API key and only ever see your cases. The canonical path is
/v1/med/cases; /v1/infractions is an alias that serves the same routes.
apikey: ak_live_...| Method | Path | What it does |
|---|---|---|
GET | /v1/med/cases | List your MED cases (cursor pagination). |
GET | /v1/med/cases/{id} | Retrieve one case. |
POST | /v1/med/cases/{id}/appeal | Appeal an open case. |
POST | /v1/med/cases/{id}/accept | Accept an open case. |
GET | /v1/med/cases/{id}/evidence | List the case's evidence files. |
POST | /v1/med/cases/{id}/evidence | Request an upload URL for a new evidence file. |
POST | /v1/med/cases/{id}/evidence/{evidence_id}/complete | Confirm the upload of an evidence file. |
The case {id} accepts the med_… id.
#The med_case object
{
"id": "med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10",
"object": "med_case",
"type": "med",
"status": "open",
"amount": 15000,
"currency": "BRL",
"charge_id": "ch_6b0f4c1e-2a8d-4c1b-9d0e-1f2a3b4c5d6e",
"reason": "Payer reports a scam",
"response_deadline": "2026-09-30T23:59:59.000Z",
"evidence_requested_at": null,
"appeal_reason": null,
"appealed_at": null,
"evidence_count": 0,
"money_outcome": null,
"opened_at": "2026-09-23T14:02:11.000Z",
"closed_at": null,
"created_at": "2026-09-23T14:02:12.000Z",
"updated_at": "2026-09-23T14:02:12.000Z"
}| Field | Description |
|---|---|
id | Case id, med_…. |
object | Always med_case. |
type | med for a Pix MED. Other dispute types (chargeback, dispute, fraud) use the same object. |
status | open, appealed, accepted, rejected or closed (states). |
amount | Disputed amount, in the currency's minor unit (cents for BRL). |
currency | ISO 4217 code, e.g. BRL. |
charge_id | The disputed charge, ch_…. |
reason | The claimed reason, as shown in your dashboard. May be null. |
response_deadline | Deadline to respond, or null when unknown (deadlines). |
evidence_requested_at | When the platform asked you for evidence, or null. |
appeal_reason | The justification you sent when appealing, or null. |
appealed_at | When you appealed, or null. |
evidence_count | Number of evidence files whose upload was confirmed. |
money_outcome | refunded_to_merchant, closed_without_refund, lost, or null while undecided. |
opened_at | When the MED was opened. |
closed_at | When the case reached a final state, or null. |
created_at / updated_at | Record timestamps (ISO 8601, UTC). |
#List cases
curl "https://aviorapay.app/v1/med/cases?status=open&limit=25" \
-H "apikey: $AVIORAPAY_API_KEY"| Query parameter | Description |
|---|---|
limit | 1 to 100. Default 25. |
starting_after | A med_… id: returns the cases after it (the previous page's next_cursor). Must be one of your cases. |
status | open, appealed, accepted, rejected or closed. |
type | med, chargeback, dispute or fraud. Without it, every type is returned. |
charge_id | Only the cases of this charge (ch_…). |
created_after / created_before | ISO 8601 date-times (exclusive bounds). |
{
"object": "list",
"data": [
{ "id": "med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10", "object": "med_case", "status": "open", "...": "..." }
],
"has_more": true,
"next_cursor": "med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10"
}Cases are ordered newest first (created_at descending). While has_more is true, pass
next_cursor as starting_after to read the next page. An unknown value for status,
type or limit, or a malformed date, returns 400 — filters are never silently ignored.
#Retrieve a case
curl "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10" \
-H "apikey: $AVIORAPAY_API_KEY"Returns the med_case object, or 404 if the case does not exist
in your account.
#Appeal a case
curl -X POST "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/appeal" \
-H "apikey: $AVIORAPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Order #8812 was delivered on 2026-09-20; signed delivery receipt attached."}'reason is required, 5 to 2,000 characters. Returns 200 with the updated case
(status: "appealed", appeal_reason and appealed_at filled in). Only an open case can
be appealed: any other status returns 400.
#Accept a case
curl -X POST "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/accept" \
-H "apikey: $AVIORAPAY_API_KEY"No body. Returns 200 with the updated case (status: "accepted",
money_outcome: "lost"). Only an open case can be accepted: any other status returns
400. Accepting is final.
#Upload evidence
Files go straight to storage through a short-lived signed URL; they never pass through
the API body. Limits: 10 MB per file; types image/png, image/jpeg, image/webp,
image/gif and application/pdf.
Step 1 — request an upload URL. Declare the file name, its exact size in bytes and its type:
curl -X POST "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
-H "apikey: $AVIORAPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file_name": "delivery-receipt.pdf", "file_size": 52341, "mime_type": "application/pdf"}'201 Created:
{
"id": "0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77",
"object": "med_case_evidence_upload",
"upload_url": "https://storage.example.com/…?X-Amz-Signature=…",
"upload_method": "PUT",
"upload_headers": { "Content-Type": "application/pdf", "Content-Length": "52341" },
"expires_in": 900
}Step 2 — PUT the file to upload_url within expires_in seconds (15 minutes), with
exactly the headers in upload_headers. The signature binds the type and the size, so
a different Content-Type or a file of a different size is refused by storage. Do not
send your API key (or any Authorization header) to this URL — the URL itself is the
credential.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @delivery-receipt.pdf(curl --data-binary sends the Content-Length of the file for you; it must match
file_size from step 1.)
Step 3 — confirm the upload. AvioraPay checks that the file really arrived and marks the evidence as uploaded:
curl -X POST "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence/0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77/complete" \
-H "apikey: $AVIORAPAY_API_KEY"200 OK with the evidence object:
{
"id": "0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77",
"object": "med_case_evidence",
"file_name": "delivery-receipt.pdf",
"file_size": 52341,
"mime_type": "application/pdf",
"uploaded_by": "merchant",
"created_at": "2026-09-23T15:10:02.000Z",
"uploaded_at": "2026-09-23T15:10:09.000Z",
"view_url": "https://storage.example.com/…?X-Amz-Signature=…"
}complete is idempotent: calling it again for a confirmed file returns the same evidence.
If the PUT has not happened yet (or the URL expired), it returns 409 with
error.code EVIDENCE_NOT_UPLOADED — request a new URL (step 1) and upload again.
Evidence that was never confirmed does not exist for the API: it is not listed and
does not count in evidence_count.
The official SDKs do the three steps in one call — see SDKs.
#List evidence
curl "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
-H "apikey: $AVIORAPAY_API_KEY"{
"object": "list",
"data": [
{ "id": "0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77", "object": "med_case_evidence", "file_name": "delivery-receipt.pdf", "...": "..." }
]
}Only confirmed evidence is returned, newest first. uploaded_by is merchant (you),
partner or platform (the AvioraPay team). view_url is a signed download link valid
for 5 minutes — fetch the list again for a fresh one.
#Errors
Errors use the standard error shape. The ones specific to MED cases:
| HTTP | error.code | When |
|---|---|---|
400 | invalid_request | Invalid filter (status, type, limit, date), starting_after that is not one of your cases, reason shorter than 5 or longer than 2,000 characters, missing/invalid file_name / file_size / mime_type, file over 10 MB or of a type not accepted, or appeal/accept on a case that is not open. |
404 | not_found | The case (or the evidence) does not exist in your account. |
409 | EVIDENCE_NOT_UPLOADED | complete called before the file reached storage (or after the URL expired). |
409 | EVIDENCE_SIZE_MISMATCH | The stored file's size differs from the declared file_size. |
503 | service_unavailable | Document storage is temporarily unavailable; evidence upload is suspended. Retry later. |
#SDKs
The official SDKs expose the same routes as a medCases resource (med_cases in Python,
MedCases in .NET), plus an uploadEvidence helper that runs the three upload steps:
import { AvioraPayClient } from '@aviorapay/node';
import { readFile } from 'node:fs/promises';
const client = new AvioraPayClient({ apiKey: process.env.AVIORAPAY_API_KEY! });
const { data, has_more, next_cursor } = await client.medCases.list({ status: 'open' });
for (const medCase of data) {
await client.medCases.uploadEvidence(medCase.id, await readFile('delivery-receipt.pdf'), {
fileName: 'delivery-receipt.pdf',
mimeType: 'application/pdf',
});
await client.medCases.appeal(medCase.id, 'Order delivered; signed receipt attached.');
}See Official SDKs for installation and the other languages.
#Transaction metadata fields
A disputed charge may also carry informational blocks in its metadata. They are
informational and non-authoritative: they mirror what was reported about the case, are
read-only, can change on each case event, and their shape may change. Do not build logic
on top of them — use the med_case object above.
#metadata.med — present on a disputed charge
| Field | Meaning |
|---|---|
referenceId | The end_to_end_id (E2E) of the original Pix being disputed. |
status | Case state as reported: created | delivered | closed | canceled. |
result | Ruling outcome, when closed: agreed (lost) | disagreed (won). |
method | Claimed reason: scam | unauthorized | coercion | invasion | other. |
reason | Textual description of the case. |
payerName / payerDocument | Identity of whoever opened the MED, when reported. |
openedAt / updatedAt / closedAt | Case timestamps as reported (open / update / close). |
lastEventAt | When AvioraPay processed the latest event of this case. |
amountMismatch | true when the reported amount diverged from the charge amount. In that case, for safety, no status change is applied and the case goes to manual handling. |
Other keys may appear in this block; ignore the ones you don't recognise.
#metadata.failure — present on an expired / cancelled / failed charge
| Field | Meaning |
|---|---|
reason | Textual failure reason reported by the provider. |
providerErrorCode | Provider code/status (e.g. expired, canceled) — what decided the final state. |
ttlSeconds | Pix QR lifetime in seconds (pix.expires_at − creation), when the charge had an expiry. Absent when it didn't. |
at | When the failure was recorded (ISO 8601). |
#Related
- MED webhook payloads and signatures: Webhooks.
- Voluntary refund (different from MED): see the refund question in Pix and the API reference.