Skip to content
LIQFYdocs
PTENENEN
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, Liqfy 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. Other transitions that do not come from your own action are described in Case states.

#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. Liqfy 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. A case that the platform registers manually without financial impact debits nothing.
  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.

Repeated or inconsistent reports. If a new report arrives for a charge that already has a case with the same amount (or the same provider ledger reference), or if, together with the charge's other MEDs, it would exceed the charge amount, Liqfy records it for internal review without moving any money, and it does not appear for you — no case, no webhook, no email. If the platform concludes it is an independent MED, the case appears at that moment (with med_case.created) and the amount is held then.

#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. Normally in open; a case the platform registers already with a loss arrives in accepted.
Webhook med_case.updatedThe full med_case object on status changes (appealed, accepted, rejected, closed) and when the platform asks you for evidence (evidence_requested_at). Not on literally every write: it is suppressed for cases the platform registers for individual manual handling in silent mode, and outcome updates (accepted / rejected / closed) are not sent when the platform's outcome-notice setting is off or when whoever decided the case chose not to notify you.
Webhook med.resolvedThe final destination of the money: refunded_to_merchant, closed_without_refund or lost. The same outcome is not sent twice for a case; if the outcome is later corrected (for example from refunded_to_merchant to lost), the new outcome is sent.
Email"MED opened" and "MED resolved" emails to your account's contact. For a case registered without financial impact, "MED opened" says no amount was debited. No "MED resolved" email is sent when the case closes without the amount returning to you (closed_without_refund) — that outcome reaches you through med.resolved and the API.
DashboardDisputes & MED Cases panel, with the amount, the claimed reason, how much of it is held, where the money went, the Pix E2E and the actions below. The search takes a prefix of the case id, of the transaction id or of the case reference, or a full Pix E2E.
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) may not arrive: 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.
acceptedThe loss was accepted without an appeal — by you, or recorded by the platform.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.

Transitions that do not come from your own action:

  • open → appealed when the provider reports the case as under review. In that case appealed_at is filled in, appeal_reason stays null, and the case can no longer be appealed or accepted through the API.
  • open / appealed → accepted when the provider reports the claim as accepted, or when the platform records the loss; appealed → rejected when the appeal is lost or the platform records the loss.
  • closed → accepted when the platform corrects a case that was closed by mistake: the amount is debited again, money_outcome becomes lost and med_case.updated is sent.

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 as soon as the payer's bank or BACEN rules on the case; others take longer to be confirmed, and the case keeps its current status (open or appealed) until it is confirmed. A favorable outcome reported by the provider may first go through a review by the Liqfy team — a withdrawn claim always does — and an unfavorable ruling reported by the provider is also handled by the team before the case changes.

#Deadlines

response_deadline (ISO 8601, UTC) is the deadline to respond to the case. It is filled in when the deadline was informed to Liqfy, 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: at least 5 characters after trimming, at most 2,000 raw characters — the 2,000 cap applies to API-key calls). 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 — where your defense goes. Your appeal, your acceptance and each evidence file whose upload you confirmed are recorded on the Liqfy platform right away, and the case changes immediately. They are also forwarded to the payment provider of the disputed charge, asynchronously, when that provider account supports receiving a MED defense; when it doesn't, they stay recorded only on the Liqfy platform. Only evidence in PDF, PNG, JPEG, WEBP or GIF up to 10 MB is forwarded. Forwarding does not decide the case: the official ruling reaches Liqfy 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
Authorization: Bearer lq_live_...

The canonical auth header is Authorization: Bearer <key>. The legacy apikey: <key> form still works as compatibility, but new integrations should use Authorization: Bearer.

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 same id without the med_ prefix also works).

#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",
  "charge_metadata": { "order_id": "PED-10442" },
  "customer_name": "Maria Silva",
  "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_….
charge_metadataThe metadata you sent when creating the disputed charge — read your own order id straight from the case (charge_metadata.order_id in the example above), no GET /v1/charges/{id} per case. {} when the charge has no metadata; null when it could not be read at that moment (then use GET /v1/charges/{charge_id}).
customer_nameThe customer name you sent when creating the charge, or null.
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 (also null when the case moved to appealed by a provider update).
appealed_atWhen the case moved to 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://api.liqfy.com.br/v1/med/cases?status=open&limit=25" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
Query parameterDescription
limitInteger from 1 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. Max 140 characters.
statusopen, appealed, accepted, rejected or closed (case-insensitive).
typemed, chargeback, dispute or fraud (case-insensitive). Without it, every type is returned.
charge_idOnly the cases of this charge (ch_…). Max 140 characters.
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; next_cursor is null on the last page. An unknown value for status, type or limit, a malformed date, a starting_after that is not one of your cases, or a starting_after/charge_id longer than 140 characters returns 400 — the seven documented filters are never silently ignored. The extra parameters q, types and page belong to the dashboard and are silently ignored under an API key.

#Retrieve a case

bash
curl "https://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10" \
  -H "Authorization: Bearer $LIQFY_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://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/appeal" \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Order #8812 was delivered on 2026-09-20; signed delivery receipt attached."}'

reason is required: at least 5 characters after trimming, at most 2,000 raw characters (the 2,000 cap is enforced with a 400 only for API-key calls; in the dashboard, longer text is cut to the first 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. A case opened for individual manual handling also returns 400 while still open — those cases are decided directly by the platform and take no defense. A case that does not exist in your account returns 404.

#Accept a case

bash
curl -X POST "https://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/accept" \
  -H "Authorization: Bearer $LIQFY_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 — and, like appeal, a case under individual manual handling returns 400 even while open. A case that does not exist in your account returns 404. 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/jpg, image/webp, image/gif and application/pdf.

Step 1 — request an upload URL. Declare the file name, its exact size in bytes (a JSON number) and its type. The camelCase forms fileName, fileSize and mimeType are also accepted:

bash
curl -X POST "https://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
  -H "Authorization: Bearer $LIQFY_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. Liqfy checks that the file really arrived and marks the evidence as uploaded:

bash
curl -X POST "https://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence/0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77/complete" \
  -H "Authorization: Bearer $LIQFY_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. It is the only idempotent route of this surface — the others take no Idempotency-Key and are not deduplicated. 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, does not count in evidence_count and is not forwarded to the provider.

The official SDKs do the three steps in one call — see SDKs.

#List evidence

bash
curl "https://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
  -H "Authorization: Bearer $LIQFY_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 Liqfy team). view_url is a signed download link valid for 5 minutes — fetch the list again for a fresh one. It is nullable: null when document storage is off for the account.

#Errors

Errors use the standard error shape: an error object (type, code, message, and param / details when applicable) plus request_id, which is also sent in the X-Request-Id header. The ones specific to MED cases:

HTTPerror.codeWhen
400invalid_requestInvalid filter (status, type, limit, date), starting_after that is not one of your cases, starting_after/charge_id over 140 characters, 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 (or is under individual manual handling).
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 { LiqfyClient } from '@liqfy/node';
import { readFile } from 'node:fs/promises';

const client = new LiqfyClient({ apiKey: process.env.LIQFY_API_KEY!, baseUrl: 'https://api.liqfy.com.br' });

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
caseIdId of the MED case (med_…) — the same id as GET /v1/med/cases/{id} and the med_case webhooks. Stable for the life of the case; use it to correlate the charge with its case. May be briefly absent right after the case opens, and is absent on cases opened before this field existed.
referenceIdThe end_to_end_id (E2E) of the original Pix being disputed.
statusCase state as reported: created | delivered | closed | canceled (claim withdrawn).
resultRuling outcome as reported, when closed: agreed (against you) | disagreed (reported in your favor). disagreed alone does not prove the money came back — read money_outcome on the case.
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 Liqfy 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.