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
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
- A MED case is created (
med_case, idmed_…) linked to the disputed charge (charge_id,ch_…). It starts inopen. - 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.
- 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.
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
| 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. Normally in open; a case the platform registers already with a loss arrives in accepted. |
Webhook med_case.updated | The 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.resolved | The 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. |
"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. | |
| Dashboard | Disputes & 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. |
| 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) may not arrive: 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 | The loss was accepted without an appeal — by you, or recorded by the platform. | 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. |
Transitions that do not come from your own action:
open→appealedwhen the provider reports the case as under review. In that caseappealed_atis filled in,appeal_reasonstaysnull, and the case can no longer be appealed or accepted through the API.open/appealed→acceptedwhen the provider reports the claim as accepted, or when the platform records the loss;appealed→rejectedwhen the appeal is lost or the platform records the loss.closed→acceptedwhen the platform corrects a case that was closed by mistake: the amount is debited again,money_outcomebecomeslostandmed_case.updatedis sent.
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 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 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 — 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.
Authorization: Bearer lq_live_...The canonical auth header is
Authorization: Bearer <key>. The legacyapikey: <key>form still works as compatibility, but new integrations should useAuthorization: Bearer.
| 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 same id without the med_ prefix also works).
#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",
"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"
}| 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_…. |
charge_metadata | The 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_name | The customer name you sent when creating the charge, or null. |
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 (also null when the case moved to appealed by a provider update). |
appealed_at | When the case moved to 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://api.liqfy.com.br/v1/med/cases?status=open&limit=25" \
-H "Authorization: Bearer $LIQFY_API_KEY"| Query parameter | Description |
|---|---|
limit | Integer from 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. Max 140 characters. |
status | open, appealed, accepted, rejected or closed (case-insensitive). |
type | med, chargeback, dispute or fraud (case-insensitive). Without it, every type is returned. |
charge_id | Only the cases of this charge (ch_…). Max 140 characters. |
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; 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
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
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
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:
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:
{
"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. Liqfy checks that the file really arrived and marks the evidence as uploaded:
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:
{
"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
curl "https://api.liqfy.com.br/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
-H "Authorization: Bearer $LIQFY_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 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:
| HTTP | error.code | When |
|---|---|---|
400 | invalid_request | Invalid 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). |
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 { 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
| Field | Meaning |
|---|---|
caseId | Id 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. |
referenceId | The end_to_end_id (E2E) of the original Pix being disputed. |
status | Case state as reported: created | delivered | closed | canceled (claim withdrawn). |
result | Ruling 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. |
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 Liqfy 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.