Pular para o conteúdo
MOONIPdocs
IntegraçãoManual
PTEN
Ir para o painel

Disputas e MED (contestação Pix)

Um pagamento Pix pode ser contestado depois de pago. No Pix, o canal para isso é o MED (Mecanismo Especial de Devolução) do BACEN: o pagador pede ao banco dele a devolução de uma transação que alega ter sido fraude, golpe ou erro. Quando um MED é aberto contra uma cobrança sua, a Moonip abre um caso MED para ele, retém o valor da sua conta enquanto o caso corre e te avisa — por webhook, por e-mail e no painel — para que você não descubra o prejuízo só no fim do mês.

Isto é diferente de um estorno (refund), que é uma devolução que você decide fazer. O MED é iniciado pelo pagador, do lado do banco dele.

Tudo nesta página está disponível pela API (/v1/med/cases), pelos SDKs oficiais (recurso medCases) e pelo painel Disputas e casos MED do seu dashboard. Os três leem e alteram o mesmo caso.

#O ciclo de vida, de relance

text
                 cobrança paid
                      │
        MED aberto pelo banco do pagador
                      │
                      ▼
   med_case "open"  ── valor retido da sua conta,
                      │  cobrança vira "disputed", estorno bloqueado
          ┌───────────┴──────────────┐
          ▼                          ▼
    você aceita               você contesta (+ evidências)
          │                          │
          ▼                          ▼
     "accepted"                 "appealed"
  (perda assumida)                   │
                         ┌───────────┴───────────┐
                         ▼                       ▼
                    "rejected"               "closed"
                  (MED mantido,          (a seu favor, ou
                  perda assumida)      encerrado sem devolução)

Um caso também pode ir direto de open para closed — por exemplo quando o pagador desiste da reclamação ou a decisão sai a seu favor antes de você responder.

#O que acontece quando um MED abre

  1. Um caso MED é criado (med_case, id med_…) ligado à cobrança contestada (charge_id, ch_…). Ele começa em open.
  2. A Moonip retém o valor da sua conta. O valor contestado é debitado do seu saldo operacional (pode ficar negativo) e fica retido enquanto o caso corre. Se o caso fechar a seu favor, ele volta; se você perder, ele já estava reservado.
  3. A cobrança vira disputed. O status público da cobrança (vocabulário de status) passa de paid para disputed. Uma cobrança disputed não pode ser estornada — o estorno duplicaria a devolução (o seu estorno + o MED executado pelo banco). Se o caso fechar a seu favor, a cobrança volta para paid.
  4. Você é avisado, por todos os canais abaixo.

#Como você é avisado

CanalO que chega
Webhook med_case.createdO objeto med_case completo quando o caso abre, com response_deadline quando o prazo é conhecido.
Webhook med_case.updatedO objeto med_case completo nas mudanças de status (appealed, accepted, rejected, closed) e quando a plataforma te pede evidências (evidence_requested_at). Não em toda escrita literalmente: ele é suprimido em casos de tratamento manual individual, e nas atualizações de desfecho quando a política de notificação da conta está desligada.
Webhook med.resolvedO destino final do dinheiro: refunded_to_merchant, closed_without_refund ou lost.
E-mailE-mails "MED aberto" e "MED resolvido" para o contato da sua conta.
PainelDisputas e casos MED, com o valor, o motivo alegado e as ações abaixo.
APIGET /v1/med/cases e GET /v1/med/cases/{id} — sempre atualizados.

Corpos e assinaturas dos três eventos estão em Webhooks.

Nenhum webhook charge.* sai por causa de um MED. A cobrança foi paga; MED não é falha de cobrança, nem estorno — charge.failed e charge.refunded não são enviados quando um MED abre, é ganho ou é perdido. Assine med_case.created, med_case.updated e med.resolved. Relendo a cobrança (GET /v1/charges/{id}) você vê status: "disputed" enquanto o caso está aberto.

Webhook é sinal; a API é a fonte da verdade. Os avisos de desfecho (med.resolved, e med_case.updated para accepted / rejected / closed) podem não chegar: um evento de caso MED cuja publicação falha não é reenviado. Reconcilie com GET /v1/med/cases (por exemplo uma vez por dia, filtrando por created_after) e considere status + money_outcome da API como o estado final.

#Estados do caso

statusSignificadoEfeito no dinheiro
openMED recém-aberto. Aguardando você aceitar ou contestar.Valor retido.
appealedVocê contestou (enviou sua defesa); aguardando o desfecho.Valor segue retido até o desfecho.
acceptedVocê aceitou a perda sem contestar.Valor segue debitado (perda assumida).
rejectedA contestação não prosperou — o MED foi mantido.Valor segue debitado (perda definitiva).
closedO caso foi encerrado. Leia money_outcome para saber para onde foi o dinheiro.Devolvido a você, ou não — veja abaixo.

money_outcome é null enquanto o caso está open ou appealed e, depois, diz o destino final do dinheiro:

money_outcomeSignificado
refunded_to_merchantO valor retido voltou para o seu saldo (decisão a seu favor, ou reclamação retirada).
closed_without_refundO caso foi encerrado, mas o valor não voltou para o seu saldo.
lostO caso terminou em perda (accepted ou rejected); o valor segue debitado.

closed sozinho não quer dizer que você ganhou. Leia sempre money_outcome.

Alguns desfechos são aplicados assim que o banco do pagador ou o BACEN decide o caso; outros levam mais tempo para ser confirmados, e o caso mantém o status atual até a confirmação.

#Prazos

response_deadline (ISO 8601, UTC) é o prazo para responder ao caso. Ele vem preenchido quando o prazo foi informado à Moonip, e null quando não. Vai no med_case.created e em toda leitura. Planeje contestar (com suas evidências) bem antes dele — a decisão não espera por você.

#Sua defesa: contestar, aceitar e evidências

Enquanto o caso está open, você escolhe um de dois caminhos:

  • Contestar (POST /v1/med/cases/{id}/appeal) — você contesta o MED com uma justificativa escrita (reason: pelo menos 5 caracteres depois do trim, no máximo 2.000 caracteres brutos — o teto de 2.000 vale para chamadas com chave de API). O caso vai para appealed.
  • Aceitar (POST /v1/med/cases/{id}/accept) — você desiste de contestar. O caso vai para accepted e o valor segue debitado.

Evidências (nota fiscal, comprovante de entrega, conversas com o comprador — qualquer coisa que mostre que a cobrança foi legítima) são anexadas pelo upload em três passos abaixo. Você pode anexar antes ou depois de contestar; anexe antes do prazo.

Importante — o que a defesa é, e o que não é. Sua contestação e suas evidências ficam registradas na plataforma Moonip e são usadas para conduzir o caso. Elas não são encaminhadas automaticamente ao provedor de pagamento, ao banco do pagador nem ao BACEN: hoje nenhum provedor oferece à Moonip um canal para enviar a defesa de um MED. Contestar, portanto, não é um envio formal ao BACEN. A decisão oficial chega à Moonip pelo canal do MED e aparece no caso (status, money_outcome) e nos webhooks acima.

#API

Todas as rotas usam a sua chave de API e só enxergam os seus casos. O caminho canônico é /v1/med/cases; /v1/infractions é um alias que atende as mesmas rotas.

text
Authorization: Bearer ak_live_...

O cabeçalho canônico de autenticação é Authorization: Bearer <chave>. A forma legada apikey: <chave> continua funcionando como compatibilidade, mas integrações novas devem usar Authorization: Bearer.

MétodoCaminhoO que faz
GET/v1/med/casesLista seus casos MED (paginação por cursor).
GET/v1/med/cases/{id}Consulta um caso.
POST/v1/med/cases/{id}/appealContesta um caso open.
POST/v1/med/cases/{id}/acceptAceita um caso open.
GET/v1/med/cases/{id}/evidenceLista as evidências do caso.
POST/v1/med/cases/{id}/evidencePede uma URL de upload para uma nova evidência.
POST/v1/med/cases/{id}/evidence/{evidence_id}/completeConfirma o upload de uma evidência.

O {id} do caso aceita o id med_….

#O objeto med_case

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": "Pagador relata golpe",
  "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"
}
CampoDescrição
idId do caso, med_….
objectSempre med_case.
typemed para um MED Pix. Outros tipos de disputa (chargeback, dispute, fraud) usam o mesmo objeto.
statusopen, appealed, accepted, rejected ou closed (estados).
amountValor contestado, na menor unidade da moeda (centavos para BRL).
currencyCódigo ISO 4217, ex.: BRL.
charge_idA cobrança contestada, ch_….
charge_metadataA metadata que você mandou ao criar a cobrança contestada — leia o id do seu pedido direto no caso (charge_metadata.order_id no exemplo acima), sem um GET /v1/charges/{id} por caso. {} quando a cobrança não tem metadata; null quando não foi possível ler naquele momento (aí use GET /v1/charges/{charge_id}).
customer_nameO nome do cliente que você mandou ao criar a cobrança, ou null.
reasonO motivo alegado, como aparece no seu painel. Pode ser null.
response_deadlinePrazo para responder, ou null quando desconhecido (prazos).
evidence_requested_atQuando a plataforma te pediu evidências, ou null.
appeal_reasonA justificativa que você enviou ao contestar, ou null.
appealed_atQuando você contestou, ou null.
evidence_countQuantidade de evidências com upload confirmado.
money_outcomerefunded_to_merchant, closed_without_refund, lost, ou null enquanto não há desfecho.
opened_atQuando o MED foi aberto.
closed_atQuando o caso chegou a um estado final, ou null.
created_at / updated_atDatas do registro (ISO 8601, UTC).

#Listar casos

bash
curl "https://api.moonip.io/v1/med/cases?status=open&limit=25" \
  -H "Authorization: Bearer $MOONIP_API_KEY"
ParâmetroDescrição
limitDe 1 a 100. Padrão 25.
starting_afterUm id med_…: devolve os casos depois dele (o next_cursor da página anterior). Precisa ser um caso seu. Máximo de 140 caracteres.
statusopen, appealed, accepted, rejected ou closed.
typemed, chargeback, dispute ou fraud. Sem ele, vêm todos os tipos.
charge_idSó os casos desta cobrança (ch_…). Máximo de 140 caracteres.
created_after / created_beforeData-hora ISO 8601 (limites exclusivos).
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"
}

Os casos vêm do mais novo para o mais antigo (created_at decrescente). Enquanto has_more for true, passe next_cursor como starting_after para ler a próxima página. Valor desconhecido em status, type ou limit, data malformada, ou starting_after/charge_id com mais de 140 caracteres devolve 400 — os sete filtros documentados nunca são ignorados em silêncio. Os parâmetros extras q, types e page são do painel e são ignorados em silêncio com uma chave de API.

#Consultar um caso

bash
curl "https://api.moonip.io/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10" \
  -H "Authorization: Bearer $MOONIP_API_KEY"

Devolve o objeto med_case, ou 404 se o caso não existe na sua conta.

#Contestar um caso

bash
curl -X POST "https://api.moonip.io/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/appeal" \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Pedido #8812 entregue em 20/09/2026; comprovante de entrega assinado em anexo."}'

reason é obrigatório: pelo menos 5 caracteres depois do trim, no máximo 2.000 caracteres brutos (o teto de 2.000 é aplicado com 400 só em chamadas com chave de API). Devolve 200 com o caso atualizado (status: "appealed", com appeal_reason e appealed_at preenchidos). Só um caso open pode ser contestado: qualquer outro status devolve 400. Um caso aberto para tratamento manual individual também devolve 400 mesmo estando open — esses casos são decididos diretamente pela plataforma e não aceitam defesa.

#Aceitar um caso

bash
curl -X POST "https://api.moonip.io/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/accept" \
  -H "Authorization: Bearer $MOONIP_API_KEY"

Sem corpo. Devolve 200 com o caso atualizado (status: "accepted", money_outcome: "lost"). Só um caso open pode ser aceito: qualquer outro status devolve 400 — e, como na contestação, um caso em tratamento manual individual devolve 400 mesmo estando open. Aceitar é definitivo.

#Enviar evidências

O arquivo vai direto para o armazenamento, por uma URL assinada de vida curta; ele nunca passa pelo corpo da API. Limites: 10 MB por arquivo; tipos image/png, image/jpeg, image/jpg, image/webp, image/gif e application/pdf.

Passo 1 — peça uma URL de upload. Declare o nome do arquivo, o tamanho exato em bytes e o tipo:

bash
curl -X POST "https://api.moonip.io/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "comprovante-entrega.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
}

Passo 2 — faça o PUT do arquivo em upload_url em até expires_in segundos (15 minutos), com exatamente os cabeçalhos de upload_headers. A assinatura amarra o tipo e o tamanho: outro Content-Type, ou um arquivo de outro tamanho, é recusado pelo armazenamento. Não mande sua chave de API (nem cabeçalho Authorization) para essa URL — a própria URL é a credencial.

bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @comprovante-entrega.pdf

(O curl --data-binary já envia o Content-Length do arquivo; ele precisa bater com o file_size do passo 1.)

Passo 3 — confirme o upload. A Moonip confere que o arquivo chegou de fato e marca a evidência como enviada:

bash
curl -X POST "https://api.moonip.io/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence/0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77/complete" \
  -H "Authorization: Bearer $MOONIP_API_KEY"

200 OK com o objeto da evidência:

json
{
  "id": "0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77",
  "object": "med_case_evidence",
  "file_name": "comprovante-entrega.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=…"
}

O complete é idempotente: chamar de novo para um arquivo já confirmado devolve a mesma evidência. Ele é a única rota idempotente desta superfície — as outras não aceitam Idempotency-Key e não deduplicam. Se o PUT ainda não aconteceu (ou a URL expirou), ele devolve 409 com error.code EVIDENCE_NOT_UPLOADED — peça uma URL nova (passo 1) e envie de novo. Evidência nunca confirmada não existe para a API: não é listada e não conta em evidence_count.

Os SDKs oficiais fazem os três passos numa chamada só — veja SDKs.

#Listar evidências

bash
curl "https://api.moonip.io/v1/med/cases/med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10/evidence" \
  -H "Authorization: Bearer $MOONIP_API_KEY"
json
{
  "object": "list",
  "data": [
    { "id": "0b8e6f52-7c3d-4e1a-9f40-2d6c8a1b5e77", "object": "med_case_evidence", "file_name": "comprovante-entrega.pdf", "...": "..." }
  ]
}

Só vêm as evidências confirmadas, da mais nova para a mais antiga. uploaded_by é merchant (você), partner ou platform (a equipe da Moonip). view_url é um link assinado de download válido por 5 minutos — liste de novo para obter um link novo. Ele é anulável: null quando o armazenamento de documentos está desligado para a conta.

#Erros

Os erros seguem o formato padrão. Os específicos de casos MED:

HTTPerror.codeQuando
400invalid_requestFiltro inválido (status, type, limit, data), starting_after que não é um caso seu, reason com menos de 5 ou mais de 2.000 caracteres, file_name / file_size / mime_type ausente ou inválido, arquivo acima de 10 MB ou de tipo não aceito, ou contestar/aceitar um caso que não está open.
404not_foundO caso (ou a evidência) não existe na sua conta.
409EVIDENCE_NOT_UPLOADEDcomplete chamado antes de o arquivo chegar ao armazenamento (ou depois de a URL expirar).
409EVIDENCE_SIZE_MISMATCHO tamanho do arquivo armazenado difere do file_size declarado.
503service_unavailableO armazenamento de documentos está indisponível; o envio de evidências fica suspenso. Tente mais tarde.

#SDKs

Os SDKs oficiais expõem as mesmas rotas como o recurso medCases (med_cases no Python, MedCases no .NET), mais um atalho uploadEvidence que faz os três passos do upload:

ts
import { MoonipClient } from '@moonip/node';
import { readFile } from 'node:fs/promises';

const client = new MoonipClient({ apiKey: process.env.MOONIP_API_KEY!, baseUrl: 'https://api.moonip.io' });

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('comprovante-entrega.pdf'), {
    fileName: 'comprovante-entrega.pdf',
    mimeType: 'application/pdf',
  });
  await client.medCases.appeal(medCase.id, 'Pedido entregue; comprovante assinado em anexo.');
}

Veja SDKs oficiais para a instalação e as outras linguagens.

#Campos de metadata da transação

Uma cobrança em disputa também pode trazer blocos informativos no metadata. Eles são informativos e não autoritativos: espelham o que foi informado sobre o caso, são somente leitura, podem mudar a cada evento do caso e o formato pode mudar. Não construa lógica em cima deles — use o objeto med_case acima.

#metadata.med — presente numa cobrança em disputa

CampoSignificado
caseIdId do caso MED (med_…) — o mesmo de GET /v1/med/cases/{id} e dos webhooks med_case. Estável durante toda a vida do caso; use-o para ligar a cobrança ao caso. Pode faltar por instantes logo depois da abertura, e falta nos casos abertos antes de este campo existir.
referenceIdO end_to_end_id (E2E) do Pix original contestado.
statusEstado do caso como informado: created | delivered | closed | canceled.
resultResultado do julgamento, quando fechado: agreed (perdido) | disagreed (ganho).
methodMotivo alegado: scam | unauthorized | coercion | invasion | other.
reasonDescrição textual do caso.
payerName / payerDocumentIdentidade de quem abriu o MED, quando informada.
openedAt / updatedAt / closedAtDatas do caso como informadas (abertura / atualização / fechamento).
lastEventAtQuando a Moonip processou o evento mais recente deste caso.
amountMismatchtrue quando o valor informado divergiu do valor da cobrança. Nesse caso, por segurança, nenhuma mudança de status é aplicada e o caso vai para tratamento manual.

Outras chaves podem aparecer neste bloco; ignore as que você não reconhece.

#metadata.failure — presente numa cobrança expired / cancelled / failed

CampoSignificado
reasonMotivo textual da falha informado pelo provedor.
providerErrorCodeCódigo/status do provedor (ex.: expired, canceled) — o que decidiu o estado final.
ttlSecondsTempo de vida do QR Pix em segundos (pix.expires_at − criação), quando a cobrança tinha validade. Ausente quando não tinha.
atQuando a falha foi registrada (ISO 8601).

#Relacionados

  • Corpos e assinaturas dos webhooks de MED: Webhooks.
  • Estorno voluntário (diferente do MED): veja a pergunta sobre estorno em Pix e a referência da API.