Skip to content
AVIORAPAYdocs
PTEN
Go to dashboard

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

text
                 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

  1. A MED case is created (med_case, id med_…) linked to the disputed charge (charge_id, ch_…). It starts in open.
  2. 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.
  3. The charge becomes disputed. The public charge status (status vocabulary) moves from paid to disputed. A disputed charge 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 to paid.
  4. You are notified, through every channel below.

#How you are notified

ChannelWhat you get
Webhook med_case.createdThe full med_case object when the case opens, including response_deadline when the deadline is known.
Webhook med_case.updatedThe 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.resolvedThe final destination of the money: refunded_to_merchant, closed_without_refund or lost.
Email"MED opened" and "MED resolved" emails to your account's contact.
DashboardDisputes & MED Cases panel, with the amount, the claimed reason and the actions below.
APIGET /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.failed and charge.refunded are not sent when a MED opens, is won or is lost. Subscribe to med_case.created, med_case.updated and med.resolved. Re-reading the charge (GET /v1/charges/{id}) shows status: "disputed" while the case is open.

Webhooks are a signal; the API is the source of truth. Outcome notifications (med.resolved, and med_case.updated for accepted / 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 with GET /v1/med/cases (for example once a day, filtered by created_after) and treat status + money_outcome from the API as final.

#Case states

statusMeaningMoney effect
openMED just opened. Waiting for you to accept or appeal.Amount held.
appealedYou appealed (sent your defense); waiting for the outcome.Amount stays held until the outcome.
acceptedYou accepted the loss without appealing.Amount stays debited (loss taken).
rejectedThe appeal did not prevail — the MED was upheld.Amount stays debited (definitive loss).
closedThe 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_outcomeMeaning
refunded_to_merchantThe held amount was returned to your balance (ruling in your favor, or claim withdrawn).
closed_without_refundThe case was closed, but the amount was not returned to your balance.
lostThe case ended as a loss (accepted or rejected); the amount stays debited.

closed alone does not mean you won. Always read money_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 to appealed.
  • Accept (POST /v1/med/cases/{id}/accept) — you give up contesting. The case moves to accepted and 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.

text
apikey: ak_live_...
MethodPathWhat it does
GET/v1/med/casesList your MED cases (cursor pagination).
GET/v1/med/cases/{id}Retrieve one case.
POST/v1/med/cases/{id}/appealAppeal an open case.
POST/v1/med/cases/{id}/acceptAccept an open case.
GET/v1/med/cases/{id}/evidenceList the case's evidence files.
POST/v1/med/cases/{id}/evidenceRequest an upload URL for a new evidence file.
POST/v1/med/cases/{id}/evidence/{evidence_id}/completeConfirm the upload of an evidence file.

The case {id} accepts the med_… id.

#The med_case object

json
{
  "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"
}
FieldDescription
idCase id, med_….
objectAlways med_case.
typemed for a Pix MED. Other dispute types (chargeback, dispute, fraud) use the same object.
statusopen, appealed, accepted, rejected or closed (states).
amountDisputed amount, in the currency's minor unit (cents for BRL).
currencyISO 4217 code, e.g. BRL.
charge_idThe disputed charge, ch_….
reasonThe claimed reason, as shown in your dashboard. May be null.
response_deadlineDeadline to respond, or null when unknown (deadlines).
evidence_requested_atWhen the platform asked you for evidence, or null.
appeal_reasonThe justification you sent when appealing, or null.
appealed_atWhen you appealed, or null.
evidence_countNumber of evidence files whose upload was confirmed.
money_outcomerefunded_to_merchant, closed_without_refund, lost, or null while undecided.
opened_atWhen the MED was opened.
closed_atWhen the case reached a final state, or null.
created_at / updated_atRecord timestamps (ISO 8601, UTC).

#List cases

bash
curl "https://aviorapay.app/v1/med/cases?status=open&limit=25" \
  -H "apikey: $AVIORAPAY_API_KEY"
Query parameterDescription
limit1 to 100. Default 25.
starting_afterA med_… id: returns the cases after it (the previous page's next_cursor). Must be one of your cases.
statusopen, appealed, accepted, rejected or closed.
typemed, chargeback, dispute or fraud. Without it, every type is returned.
charge_idOnly the cases of this charge (ch_…).
created_after / created_beforeISO 8601 date-times (exclusive bounds).
json
{
  "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

bash
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

bash
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

bash
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:

bash
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:

json
{
  "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.

bash
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:

bash
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:

json
{
  "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

bash
curl "https://aviorapay.app/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
  -H "apikey: $AVIORAPAY_API_KEY"
json
{
  "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:

HTTPerror.codeWhen
400invalid_requestInvalid 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.
404not_foundThe case (or the evidence) does not exist in your account.
409EVIDENCE_NOT_UPLOADEDcomplete called before the file reached storage (or after the URL expired).
409EVIDENCE_SIZE_MISMATCHThe stored file's size differs from the declared file_size.
503service_unavailableDocument 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:

ts
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

FieldMeaning
referenceIdThe end_to_end_id (E2E) of the original Pix being disputed.
statusCase state as reported: created | delivered | closed | canceled.
resultRuling outcome, when closed: agreed (lost) | disagreed (won).
methodClaimed reason: scam | unauthorized | coercion | invasion | other.
reasonTextual description of the case.
payerName / payerDocumentIdentity of whoever opened the MED, when reported.
openedAt / updatedAt / closedAtCase timestamps as reported (open / update / close).
lastEventAtWhen AvioraPay processed the latest event of this case.
amountMismatchtrue 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

FieldMeaning
reasonTextual failure reason reported by the provider.
providerErrorCodeProvider code/status (e.g. expired, canceled) — what decided the final state.
ttlSecondsPix QR lifetime in seconds (pix.expires_at − creation), when the charge had an expiry. Absent when it didn't.
atWhen the failure was recorded (ISO 8601).
  • MED webhook payloads and signatures: Webhooks.
  • Voluntary refund (different from MED): see the refund question in Pix and the API reference.