← Blog

E-commerce

Stripe dispute payload: drie velden bepalen vechten of refund

Elke Stripe dispute payload heeft twintig velden. Drie ervan bepalen of je vecht of refundt. De rest is context die je pas leest nadat de beslissing is genomen.

Jacob Molkenboer· Oprichter · A Brand New Company· 6 jun 2026· 6 min
Pakketje van kraftpapier met linnen touw, drie messing labels met groen lint en rode lakzegel op ivoren tafel.

Een Stripe dispute webhook komt binnen om 02:14. De founder opent het dashboard, ziet "Fraudulent" staan en stuurt het door naar support met één woord: "Vechten." Twee weken later is de chargeback definitief, het geld is weg, en het evidence-formulier is nooit ingediend omdat niemand het deadline-veld heeft gelezen. Zo verliezen kleine e-commerce teams het vaakst disputes die ze hadden kunnen winnen.

Het Stripe dispute object heeft ruim twintig velden. Drie ervan bepalen of je vecht, refundt of niets doet. De andere twintig zijn context. De payload lezen als een payments engineer betekent dat je eerst naar die drie gaat, in volgorde, voordat je ook maar één e-mailthread opent.

De payload, uitgekleed

Strip de metadata eruit en een echte dispute ziet er zo uit:

{
  "id": "dp_1Oz...",
  "object": "dispute",
  "amount": 14900,
  "currency": "eur",
  "reason": "product_not_received",
  "status": "needs_response",
  "is_charge_refundable": true,
  "evidence_details": {
    "due_by": 1717977599,
    "has_evidence": false,
    "past_due": false,
    "submission_count": 0
  },
  "charge": "ch_3Oz..."
}

Lees in deze volgorde: reason, is_charge_refundable, evidence_details.due_by. Alles wat daarna komt is afgeleid van deze drie.

Veld één: reason

Het reason-veld is de vertaling die Stripe maakt van de reason code van het kaartnetwerk. Visa, Mastercard en Amex hebben elk hun eigen dispute-taxonomie. Stripe perst die samen in een kleinere set: fraudulent, product_not_received, product_unacceptable, subscription_canceled, duplicate, credit_not_processed, general, plus nog een paar.

De vertaling klopt meestal, maar de echte code van het netwerk zit in network_reason_code en vertelt soms een ander verhaal. Een Visa-code 13.1 (merchandise/services not received) en een code 10.4 (card-absent fraud) zien er allebei slecht uit in het dashboard, maar het bewijs dat je indient is totaal anders.

De reason bepaalt welk bewijs Stripe accepteert:

  • fraudulent: AVS-match, CVC-match, IP-adres, device fingerprint, eerdere succesvolle aankopen van dezelfde klant.
  • product_not_received: verzendbewijs, tracking number met afleverbevestiging, communicatielogboek.
  • subscription_canceled: opzegvoorwaarden, bewijs dat de klant de dienst na de opzegdatum nog gebruikte, je algemene voorwaarden.

Stuur je verzendbewijs in bij een fraudulent-dispute, dan verlies je. Stuur je AVS-data in bij product_not_received, dan verlies je. Match het bewijs aan de reason, niet aan je onderbuikgevoel over wat er gebeurd is.

Veld twee: is_charge_refundable

is_charge_refundable is een boolean die stilletjes je opties bepaalt.

Als die true is, kun je de originele charge nog steeds refunden. De dispute wordt teruggedraaid, je betaalt de chargeback-fee, maar je vermijdt dat de dispute meetelt voor je win rate. Voor kleine disputes waarbij de klant toch gaat winnen, is dit meestal de juiste keuze.

Als die false is, houdt het netwerk het geld al vast. Refunden kan niet meer. Je kunt alleen nog bewijs indienen en wachten op de uitspraak van het netwerk. De keuze om te vechten is geen keuze meer, ze wordt opgelegd.

De omslag van true naar false gaat snel, soms op het moment dat de dispute wordt aangemaakt, soms een paar uur later. Zie je is_charge_refundable: true en is de reden fraudulent, refund dan direct. Friendly fraud bevechten verdient zelden de support-uren terug, en Stripe's eigen richtlijnen over het meten van dispute outcomes zijn er eerlijk over hoe weinig van die winsten daadwerkelijk bij de merchant landen.

Let op

Als is_charge_refundable false is, dien dan geen gedeeltelijke refund in in de hoop dat de dispute daarmee opgelost is. Gedeeltelijke refunds op disputed charges geven reconciliatieproblemen en trekken de dispute niet in.

Veld drie: evidence_details.due_by

evidence_details.due_by is een Unix timestamp. Het is de enige deadline die telt. Niet de e-mail uit het dashboard, niet de Slack-herinnering die je hebt gezet, niet de SLA van je support tool.

De deadline wordt bepaald door het kaartnetwerk, niet door Stripe. Visa geeft je tot 30 dagen, Mastercard meestal 45, maar Stripe trekt daar zelf ongeveer een week vanaf om tijd te hebben voor de review van je inzending. Een Mastercard-dispute die het netwerk op dag 45 plant, staat dus op ongeveer dag 38 in de payload.

Trek vandaag af van due_by. Heb je minder dan 72 uur, laat dan alles vallen. Heb je meer dan twee weken, plan het bewijswerk dan in maar stel de inzending niet uit tot binnen één week voor de deadline. Stripe stuurt af en toe bewijs terug als incompleet en dan heb je tijd nodig voor een nieuwe inzending.

const dueBy = new Date(dispute.evidence_details.due_by * 1000);
const hoursLeft = (dueBy.getTime() - Date.now()) / 36e5;

let priority;
if (hoursLeft < 72) priority = 'urgent';
else if (hoursLeft < 168) priority = 'soon';
else priority = 'scheduled';

De flag past_due: true is terminaal. Zie je die staan, dan is het venster gesloten en heeft het netwerk in het voordeel van de klant beslist. Er is geen beroepspad binnen de Stripe API.

De beslisboom, in gewone code

Zodra je de drie velden hebt gelezen, comprimeert de beslissing tot twintig regels:

function decide(dispute) {
  const { reason, is_charge_refundable, amount, evidence_details } = dispute;

  if (evidence_details.past_due) return 'already_lost';

  if (reason === 'fraudulent' && is_charge_refundable) {
    return amount < 10000 ? 'refund' : 'fight';
  }

  if (reason === 'duplicate') {
    return 'fight'; // Stripe heeft hier meestal de bonnetjes
  }

  if (reason === 'product_not_received') {
    return hasShippingProof(dispute.charge) ? 'fight' : 'refund';
  }

  if (reason === 'subscription_canceled') {
    return hasUsageAfterCancellation(dispute.charge) ? 'fight' : 'refund';
  }

  return 'review_manually';
}

Dit is niet het laatste woord. Elke dispute heeft context. Maar het haalt de cognitieve last weg om elke keer opnieuw patronen te herkennen, en de meeste disputes belanden in minder dan een seconde bij een standaardactie.

De kleinste bruikbare dispute handler

Draai je een e-commerce site op Stripe en heb je nog geen geautomatiseerde dispute-respons, dan is het kleinste bruikbare wat je vandaag kunt bouwen een webhook die drie dingen doet zodra hij vuurt op charge.dispute.created:

  1. Stuur een Slack-bericht met de drie velden vertaald naar gewone taal.
  2. Bereken hoursLeft uit due_by en geef het een kleurcode.
  3. Zoek de oorspronkelijke order op en hang er verzend- of gebruiksdata aan.

Alleen dat al dicht het gat tussen "dispute is binnen" en "mens onderneemt iets". De meeste kleine teams verliezen disputes niet omdat ze moeilijk te bevechten zijn, maar omdat de payload vier dagen in een mailbox blijft hangen die niemand opent.

Toen we de dispute handler bouwden voor een Nederlandse e-commerce klant, liepen we er steeds tegenaan dat network_reason_code en reason vaak genoeg met elkaar oneens waren dat een standaardtemplate op basis van Stripe's reason herhaaldelijk het verkeerde bewijs aanleverde. We zijn de network code uiteindelijk eerst gaan lezen en gebruiken Stripe's reason als fallback, het soort patroon dat thuishoort in ons werk rond procesautomatisering.

Pak één echte dispute payload uit je Stripe dashboard, plak hem in een JSON viewer en lees die drie velden in volgorde. Begint de huidige workflow van je team daar niet, dan heb je vóór maandag een fix van vijf minuten te doen.

Kern

Lees eerst reason, is_charge_refundable en evidence_details.due_by. De andere twintig velden zijn context voor een beslissing die deze drie al hebben genomen.

FAQ

Wat betekent het concreet als is_charge_refundable false is?

Het geld wordt vastgehouden door het kaartnetwerk en je kunt de originele charge niet meer refunden. Je enige route is bewijs indienen via de evidence flow van Stripe en wachten op de uitspraak van het netwerk.

Is network_reason_code betrouwbaarder dan het reason-veld van Stripe?

Vaak wel. De network code is wat het kaartnetwerk daadwerkelijk heeft gestuurd. Het reason-veld van Stripe is een vertaling. Spreken ze elkaar tegen, dan landt bewijs gekozen op de network code meestal beter.

Hoe snel slaat is_charge_refundable om van true naar false?

Meestal binnen een paar uur na het ontstaan van de dispute, soms direct. Behandel een true-waarde als een venster dat aan het sluiten is, niet als een stabiele staat, en refund nog binnen het uur als dat de gekozen actie is.

Kan ik nog in beroep nadat past_due true is geworden?

Niet binnen de Stripe API. Zodra past_due true is, is het venster dicht en heeft het netwerk al beslist. Het geld is weg en de chargeback telt mee voor je win rate.

e-commerceintegrationsoperationsworkflowautomation

Iets bouwen?

Start een project