Handle disputes

A dispute is a cardholder's challenge to a charge — they've contacted their issuing bank, the bank pulled the funds back, and you now have a fixed window to either accept the loss or defend the charge with evidence. The card networks set the windows (typically 7-21 days depending on network and dispute reason), and missing the deadline forfeits the case automatically.

This guide is the action playbook. For the underlying state model and the 19-state lifecycle, see the disputes concept.

When a dispute hits

Frame creates a Dispute record and emits a webhook event the moment the network notifies Frame of the chargeback. Subscribe to:

  • charge.dispute.created — a new dispute arrived; deadline starts now.
  • charge.dispute.updated — state transitions (evidence accepted, network decision posted, dispute closed).
// Webhook handler
switch (event.type) {
  case 'charge.dispute.created':
    notifyMerchantOpsTeam(event.data);
    queueEvidenceCollection(event.data.id);
    break;
  case 'charge.dispute.updated':
    if (event.data.status === 'closed_won' || event.data.status === 'closed_lost') {
      finalizeDispute(event.data);
    }
    break;
}

Treat charge.dispute.created as a hard deadline starter. Pipe it into whatever queue your operations team monitors — the worst dispute outcome is "no evidence submitted because nobody saw the notification."

Decide: defend or accept

Before gathering evidence, decide whether the dispute is worth defending. A few clear-cut cases:

  • You shipped what was ordered + can prove delivery → defend. Strong evidence wins these cases. Defend.
  • The customer was correct (you didn't ship, item was defective, charge was unauthorized) → accept. Submitting weak evidence just delays the inevitable + costs operational time. Accept the loss + refund proactively if not already done.
  • Friendly fraud (customer received the goods but disputed anyway) → defend. Strong evidence wins these too; if you let them pass, you train the customer to do it again.

To accept a dispute without submitting evidence, simply don't submit — the deadline passes, the dispute closes as closed_lost, and Frame finalizes the network-side reversal. There's no explicit "accept" call required.

Gather evidence

The evidence you need depends on the dispute reason (product_not_received, product_unacceptable, unrecognized, subscription_canceled, duplicate, fraudulent, etc.). Frame's evidence shape covers the common requirements:

FieldUse forExample
shipping_carrierProduct-not-received cases"UPS"
shipping_dateProduct-not-received cases"2026-05-22"
shipping_tracking_numberProduct-not-received cases"1Z999AA1..."
customer_purchase_ip_addressUnrecognized / fraudulent casesThe IP the customer purchased from
support_descriptionProduct-unacceptable, subscription-canceledNarrative explaining the case
refund_refusal_explanationCases where customer requested a refundWhy you denied or limited the refund
refund_policySubscription / cancellation casesURL or text of your refund policy
access_activity_logDigital-goods casesLogin or download history proving access

You also attach files — receipts, photos, screenshots, terms-of-service PDFs. Files are PDF only, max 2 MB each.

Upload supporting documents

Upload each evidence file to the dispute via the documents endpoint. Each upload is a multipart POST with a document_type indicating what category the file falls into:

curl --request POST \
  --url https://api.framepayments.com/v1/disputes/<dispute_id>/documents \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --form 'document_type=shipping_documentation' \
  --form 'document=@/path/to/proof-of-delivery.pdf'

Valid document_type values:

  • shipping_documentation — proof of delivery, tracking confirmation, signed delivery receipt.
  • customer_signature — signed receipt or proof of customer authorization (digital signature, e-signed terms).
  • customer_communication — emails, chat logs, support tickets showing customer interactions.
  • supporting_file — anything else relevant (terms of service, refund policy, receipt copies).

Upload each file as a separate request. The dispute record accumulates the attached documents; you don't pre-bundle them.

Submit the evidence

Once your documents are uploaded, submit the evidence object on the dispute itself with PATCH /v1/disputes/<id>:

curl --request PATCH \
  --url https://api.framepayments.com/v1/disputes/<dispute_id> \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "evidence": {
      "shipping_carrier": "UPS",
      "shipping_date": "2026-05-22",
      "shipping_tracking_number": "1Z999AA10123456784",
      "customer_purchase_ip_address": "203.0.113.42",
      "support_description": "Customer placed order on 2026-05-20, item shipped 2026-05-22, delivered 2026-05-25 per tracking. Customer did not contact support before initiating the dispute on 2026-06-02.",
      "refund_policy": "https://yourplatform.com/refund-policy"
    }
  }'

Submitting flips the dispute into the evidence_submitted state and queues it for representment. Frame packages your evidence + uploaded documents and sends them to the card network on your behalf. You're done with the active work; the network decision comes back asynchronously over the following days or weeks (network-dependent).

There's no separate "submit" endpoint — submitting the evidence object via PATCH is the submission. Once submitted, you can no longer modify evidence on the dispute. If you realize you need to add something after submitting, contact Frame support before the deadline; otherwise the case is locked.

Track the outcome

The dispute transitions through submittedawaiting_network_decisionclosed_won or closed_lost. On the terminal transition, charge.dispute.updated fires with the final status.

  • closed_won — the network ruled in your favor. The disputed funds return to your platform; the chargeback fee may or may not be refunded (network-dependent).
  • closed_lost — the network ruled in the cardholder's favor. The funds stay with the cardholder; the chargeback fee is not refunded.

Read the disputes concept for the full 19-state model — there are intermediate states for evidence rejection, do-not-contest declarations, and representment retries that the action-oriented summary above abstracts away.

Operational patterns

Internal SLA before the network deadline. Card-network deadlines are typically 7-21 days. Build internal SLA of (network deadline minus 2-3 days) to leave buffer for last-minute issues — document quality problems, missing tracking numbers, escalations. Pipe the deadline into your team's task tracker.

Template your evidence. For platforms with high dispute volume (subscriptions, marketplaces), evidence shape is repetitive. Build a template per dispute reason — product_not_received evidence has the same shape every time. Auto-fill from your order data; have ops review + send.

Track win rate. Disputed transactions ÷ won disputes is a leading indicator of fraud-prevention effectiveness, evidence quality, or customer-experience issues depending on the trend direction. Plumb it into your dashboard alongside chargeback rate.

Don't refund post-dispute. Once a chargeback hits, refunding doesn't withdraw the dispute — you end up paying the customer twice (the disputed funds via the network reversal + the refund). Refund proactively before disputes happen, or accept the dispute and let the network reversal settle the customer side.

Gotchas

Symptom: you uploaded documents but the dispute is still in received state. Why: uploading documents doesn't submit the dispute — the documents accumulate against the dispute record but the case isn't representment-ready until you PATCH the evidence object. Fix: call PATCH /v1/disputes/<id> with the evidence object after uploading. That's the submission step.

Symptom: a document upload failed with a size error. Why: file is over 2 MB or isn't a PDF. Fix: compress the PDF, or split a large document into smaller PDFs uploaded as separate supporting_file entries. The 2 MB cap is per-file, not per-dispute.

Symptom: the dispute closed as closed_lost even though you submitted strong evidence. Why: network decisions are not transparent — issuers weigh evidence against the cardholder's claim using rules the network doesn't publish. Strong evidence loses more often for fraud-related reason codes (fraudulent, unrecognized) where the issuer often sides with the cardholder by policy. Fix: not all disputes are winnable. For high-volume disputes of this shape, look upstream — Sonar tuning, 3DS adoption, billing-descriptor clarity (most "unrecognized" disputes come from confusing descriptors).

Symptom: you missed the deadline and the dispute closed as closed_lost. Why: no evidence submitted within the network window. Fix: this is final; the case can't be reopened. For the future, wire charge.dispute.created into a fast-alert path (Slack, PagerDuty, anything that doesn't rely on someone checking email).

Next steps