Op gebruik gebaseerde facturering met Stripe: een praktisch datamodel
Op gebruik gebaseerde facturering met Stripe vereist schone eventopslag en reconciliatie. Leer een eenvoudig schema, webhook-flow, backfills en oplossingen voor dubbele telling.

Wat je echt bouwt (en waarom het faalt)
Op gebruik gebaseerde facturering klinkt simpel: meet wat een klant gebruikte, vermenigvuldig met een prijs en factureer aan het einde van de periode. In de praktijk bouw je een klein boekhoudsysteem. Het moet correct blijven, ook als data laat, dubbel of helemaal niet binnenkomt.
De meeste fouten gebeuren niet tijdens checkout of in het dashboard. Ze gebeuren in het metering-datamodel. Als je niet vol vertrouwen kunt beantwoorden: “Welke gebruiksgebeurtenissen zijn meegeteld voor deze factuur, en waarom?”, dan ga je uiteindelijk te veel, te weinig factureren of vertrouwen verliezen.
Gebruikfacturering faalt meestal op voorspelbare manieren: gebeurtenissen ontbreken na een storing, retries maken duplicaten, late gebeurtenissen verschijnen nadat totalen zijn berekend, of verschillende systemen komen niet overeen en je kunt het verschil niet reconciliëren.
Stripe is uitstekend in prijsstelling, facturen, belastingen en incasso. Maar Stripe kent je ruwe gebruik niet tenzij jij het stuurt. Dat dwingt een beslissing over de bron van waarheid: is Stripe het grootboek, of is jouw database het grootboek dat Stripe weerspiegelt?
Voor de meeste teams is de veiligste verdeling:
- Je database is de bron van waarheid voor ruwe gebruiksgebeurtenissen en hun lifecycle.
- Stripe is de bron van waarheid voor wat daadwerkelijk gefactureerd en betaald is.
Voorbeeld: je trackt “API-calls.” Elke call genereert een usage event met een stabiele unieke sleutel. Bij het factureren tel je alleen in aanmerking komende events op die nog niet gefactureerd zijn, en maak je een Stripe invoice item aan of update je die. Als ingestie retries optreden of een webhook twee keer binnenkomt, maken idempotentie-regels de duplicaat onschadelijk.
Beslissingen die je moet nemen voordat je tabellen ontwerpt
Voordat je tabellen maakt, leg de definities vast die later bepalen of facturering uitlegbaar blijft. De meeste “mystery invoice bugs” komen van onduidelijke regels, niet van slechte SQL.
Begin met de eenheid waar je voor factureert. Kies iets dat makkelijk te meten is en moeilijk te betwisten. “API-calls” wordt ingewikkeld met retries, batch-requests en fouten. “Minuten” wordt lastig met overlap. “GB” heeft een duidelijke basis nodig (GB vs GiB) en een meetmethode (gemiddelde vs piek).
Bepaal vervolgens grenzen. Je systeem moet precies weten bij welk venster een event hoort. Wordt gebruik per uur, per dag, per facturatieperiode of per klantactie geteld? Als een klant halverwege de maand upgrade, splits je dan het venster of pas je één prijs op de hele maand toe? Deze keuzes bepalen hoe je events groepeert en hoe je totalen uitlegt.
Beslis ook wie welke feiten bezit. Een veelvoorkomend patroon met Stripe is: jouw app bezit ruwe events en afgeleide totalen, terwijl Stripe facturen en betalingsstatussen bezit. Dat werkt het beste als je geschiedenis niet stil bewerkt. Je legt correcties vast als nieuwe entries en bewaart het origineel.
Een korte set niet-onderhandelbare punten helpt je schema eerlijk te houden:
- Traceerbaarheid: elke gefactureerde eenheid is herleidbaar naar opgeslagen events.
- Auditbaarheid: je kunt maanden later antwoord geven op “waarom is dit in rekening gebracht?”.
- Omkeerbaarheid: fouten worden opgelost met expliciete correcties.
- Idempotentie: dezelfde input kan niet twee keer worden meegeteld.
- Duidelijk eigenaarschap: één systeem bezit elk feit (gebruik, prijs, facturering).
Voorbeeld: als je factureert voor “verzonden berichten”, bepaal of retries meetellen, of mislukte afleveringen meetellen en welke timestamp doorslaggevend is (clienttijd vs servertijd). Schrijf het op en codeer het in eventvelden en validatie, niet alleen in iemands hoofd.
Een eenvoudig datamodel voor usage events
Op gebruik gebaseerde facturering is het makkelijkst wanneer je gebruik als boekhouding behandelt: ruwe feiten zijn append-only en totalen zijn afgeleid. Die ene keuze voorkomt de meeste geschillen omdat je altijd kunt uitleggen waar een getal vandaan komt.
Een praktisch startpunt gebruikt vijf kern-tabellen (namen kunnen variëren):
- customer: interne customer id, Stripe customer id, status, basismetadata.
- subscription: interne subscription id, Stripe subscription id, verwachte plan/prijzen, start-/eindtijdstempels.
- meter: wat je meet (API calls, seats, opslag GB-uren). Inclusief een stabiele meter key, eenheid en hoe het aggregreert (sum, max, unique).
- usage_event: één rij per gemeten actie. Sla customer_id, subscription_id (indien bekend), meter_id, quantity, occurred_at (wanneer het gebeurde), received_at (wanneer je het ingeslikt hebt), source (app, batch import, partner) en een stabiele externe sleutel voor dedupe op.
- usage_aggregate: afgeleide totalen, meestal per customer + meter + tijdvenster (dag of uur) en facturatieperiode. Sla opgetelde quantity plus een versie of last_event_received_at op om herberekening te ondersteunen.
Houd usage_event onveranderlijk. Als je later een fout ontdekt, schrijf een compenserend event (bijvoorbeeld -3 seats voor een annulering) in plaats van geschiedenis te wijzigen.
Bewaar raw events voor audits en disputen. Als je ze niet eeuwig kunt bewaren, bewaar ze minstens zo lang als je billing lookback window plus je refund/dispute window.
Houd afgeleide totalen gescheiden. Aggregaten zijn snel voor facturen en dashboards, maar wegwerpbaar. Je moet usage_aggregate altijd kunnen herbouwen uit usage_event, ook na een backfill.
Idempotentie en event lifecycle-staten
Usage-data is noisy. Clients retryen requests, queues leveren duplicaten en Stripe webhooks kunnen in de verkeerde volgorde aankomen. Als je database niet kan bewijzen “dit usage event is al eens meegeteld”, ga je uiteindelijk dubbel factureren.
Geef elk usage event een stabiel, deterministisch event_id en handhaaf uniciteit daarop. Vertrouw niet alleen op een auto-increment id als enige identifier. Een goede event_id is afgeleid van de businessactie, zoals customer_id + meter + source_record_id (of customer_id + meter + timestamp_bucket + sequence). Als dezelfde actie opnieuw wordt gestuurd, levert dat hetzelfde event_id op en wordt de insert een veilige no-op.
Idempotentie moet elk ingest-pad dekken, niet alleen je openbare API. SDK-calls, batch imports, worker-jobs en webhook-processors worden allemaal herhaald. Gebruik één regel: als de input herhaald kan worden, heeft het een idempotency key die in je database staat en gecontroleerd wordt voordat totalen veranderen.
Een eenvoudig lifecycle-state model maakt retries veilig en support makkelijker. Maak het expliciet en bewaar een reden wanneer iets faalt:
received: opgeslagen, nog niet gecontroleerdvalidated: voldoet aan schema, customer, meter en tijdvenster regelsposted: meegeteld in facturatieperiode-totalenrejected: permanent genegeerd (met een reden-code)
Voorbeeld: je worker crasht na validatie maar vóór posting. Bij retry vindt hij hetzelfde event_id in staat validated en gaat dan door naar posted zonder een tweede event te maken.
Voor Stripe webhooks gebruik je hetzelfde patroon: sla de Stripe event.id op en markeer het maar één keer verwerkt, zodat dubbele afleveringen onschadelijk zijn.
Stap-voor-stap: ingest van metering events end-to-end
Behandel elk metering event als geld: valideer het, sla het origineel op en derivatieer totalen van die bron van waarheid. Dat houdt facturering voorspelbaar wanneer systemen retryen of data laat sturen.
Een betrouwbare ingest-flow
Valideer elk binnenkomend event voordat je enige totalen aanraakt. Vereis minimaal: een stabiele customer-identifier, een meter-naam, een numerieke quantity, een timestamp en een unieke event key voor idempotentie.
Sla eerst het raw event op, ook als je later wilt aggregeren. Dat raw record is wat je opnieuw zult verwerken, auditen en gebruiken om fouten te herstellen zonder te raden.
Een betrouwbare flow ziet er zo uit:
- Accepteer het event, valideer verplichte velden, normaliseer eenheden (bijv. seconden vs minuten).
- Insert een raw usage_event-rij met de event key als unieke constraint.
- Agregeer in een bucket (dagelijks of per facturatieperiode) door de event quantity toe te passen.
- Als je gebruik naar Stripe rapporteert, leg dan vast wat je stuurde (meter, quantity, periode en Stripe response identifiers).
- Log anomalieën (geweigerd events, eenheidsconversies, late aankomsten) voor audits.
Houd aggregatie herhaalbaar. Een veelgebruikte aanpak is: insert het raw event in één transactie en zet daarna een job in de wachtrij om buckets bij te werken. Als de job twee keer draait, moet hij detecteren dat het raw event al toegepast is.
Wanneer een klant vraagt waarom ze 12.430 API-calls zijn gefactureerd, moet je het exacte set raw events kunnen tonen die in dat facturatievenster zijn opgenomen.
Reconciliatie van Stripe webhooks met je database
Webhooks zijn het ontvangstbewijs voor wat Stripe daadwerkelijk deed. Jouw app kan concepten creëren en gebruik pushen, maar de factuurstatus wordt pas echt wanneer Stripe dat meldt.
De meeste teams focussen op een klein aantal webhooktypes die factureringsuitkomsten beïnvloeden:
invoice.created,invoice.finalized,invoice.paid,invoice.payment_failedcustomer.subscription.created,customer.subscription.updated,customer.subscription.deletedcheckout.session.completed(als je abonnementen via Checkout start)
Sla elke webhook die je ontvangt op. Bewaar de raw payload plus wat je opmerkte bij ontvangst: Stripe event.id, event.created, je signature verification resultaat en je server receive-timestamp. Die geschiedenis is belangrijk bij debuggen of beantwoorden van “waarom ben ik in rekening gebracht?”.
Een solide, idempotent reconciliatiepatroon ziet er zo uit:
- Insert de webhook in een
stripe_webhook_eventstabel met een unieke constraint opevent_id. - Als de insert faalt, is het een retry. Stop.
- Verifieer de signature en registreer pass/fail.
- Verwerk het event door je interne records op te zoeken via Stripe IDs (customer, subscription, invoice).
- Pas de state-change alleen toe als het vooruit beweegt.
Out-of-order levering is normaal. Gebruik een “max state wins”-regel plus timestamps: beweeg een record nooit terug.
Voorbeeld: je ontvangt invoice.paid voor invoice in_123, maar je interne invoicerij bestaat nog niet. Maak een rij aan gemarkeerd als “gezien van Stripe” en koppel die later aan het juiste account via de Stripe customer ID. Dat houdt je grootboek consequent zonder dubbele verwerking.
Van usage-totalen naar invoice-regels
Ruwe usage omzetten naar factuurregels gaat vooral over timing en grenzen. Bepaal of je totalen in realtime nodig hebt (dashboards, bestedingsalerts) of alleen bij facturatie. Veel teams doen beide: events continu schrijven en invoice-klare totalen berekenen in een geplande job.
Stem je gebruiksvenster af op Stripe’s billing period. Raad niet op kalendermaanden. Gebruik de subscription item’s huidige billing period start en end, en tel alleen events op waarvan de timestamps binnen dat venster vallen. Sla timestamps op in UTC en maak het billing window ook UTC.
Houd geschiedenis onveranderlijk. Als je later een fout vindt, bewerk dan geen oude events of herschrijf eerdere totalen. Maak een adjustment-record dat verwijst naar het originele venster en hoeveelheid toevoegt of aftrekt. Dat is makkelijker te auditen en uit te leggen.
Wijzigingen van plannen en proratie zijn plekken waar traceerbaarheid vaak verloren gaat. Als een klant halverwege de cyclus van plan verandert, splits dan gebruik in sub-vensters die overeenkomen met de actieve prijsrange. Je factuur kan twee usage-regels bevatten (of één regel plus een correctie), elk gekoppeld aan een specifieke prijs en tijdsrange.
Een praktische flow:
- Haal het facturatievenster uit Stripe period start en end.
- Agregeer in aanmerking komende usage events tot een usage-totaal voor dat venster en die prijs.
- Genereer invoice line items uit het usage-totaal plus eventuele correcties.
- Sla een calculation run id op zodat je de cijfers later kunt reproduceren.
Backfills en late data zonder vertrouwen te breken
Late gebruiksdata is normaal. Devices gaan offline, batch-jobs lopen uit, partners sturen bestanden opnieuw en logs worden na een storing opnieuw afgespeeld. De kern is backfills als correctiewerk te behandelen, niet als een manier om “de cijfers passend te maken”.
Wees expliciet over waar backfills vandaan mogen komen (app-logs, warehouse exports, partner-systemen). Leg de bron vast op elk event zodat je kunt uitleggen waarom het laat aankwam.
Bij backfill bewaar je twee timestamps: wanneer het gebeurde (de tijd waarvoor je wilt factureren) en wanneer je het ingeslikt hebt. Tag het event als backfilled, maar overschrijf de geschiedenis niet.
Geef de voorkeur aan het herbouwen van totalen uit ruwe events boven het toepassen van deltas op de huidige aggregatietabel. Replays zijn hoe je herstelt van bugs zonder te gokken. Als je pijplijn idempotent is, kun je een dag, een week of een volledige facturatieperiode opnieuw draaien en dezelfde totalen krijgen.
Zodra een factuur bestaat, moeten correcties een duidelijke policy volgen:
- Als de factuur nog niet gefinaliseerd is, herbereken en update totalen vóór finalisatie.
- Als hij gefinaliseerd is en ondergefactureerd, maak een add-on invoice (of voeg een nieuw invoice item toe) met een duidelijke omschrijving.
- Als hij gefinaliseerd is en overgefactureerd, maak een credit note en verwijs naar de originele factuur.
- Verplaats gebruik niet naar een andere periode om een correctie te vermijden.
- Bewaar een korte reden voor de correctie (partner resend, vertraagde loglevering, bugfix).
Voorbeeld: een partner stuurt missende events voor 28-29 januari op 3 februari. Je voegt ze in met occurred_at in januari, ingested_at in februari en een backfill source “partner”. De januari-factuur is al betaald, dus je maakt een kleine add-on invoice voor de missende units, met de reden opgeslagen naast het reconciliatierecord.
Veelvoorkomende fouten die dubbele telling veroorzaken
Dubbele telling gebeurt wanneer een systeem “een bericht kwam aan” interpreteert als “de actie gebeurde”. Met retries, vertraagde webhooks en backfills moet je de klantactie scheiden van je verwerking.
De gebruikelijke boosdoeners:
- Retries behandeld als nieuw gebruik. Als elk event geen stabiel actie-id draagt (request_id, message_id) en je database geen uniciteit afdwingt, tel je dubbel.
- Eventtijd vermengd met verwerkingstijd. Rapporteren op ingest-tijd in plaats van occurred-tijd zorgt dat late events in het verkeerde venster landen en later opnieuw tijdens replays worden meegeteld.
- Ruwe events verwijderd of overschreven. Als je alleen een lopend totaal bewaart, kun je niet bewijzen wat er gebeurde en kan reprocessing totalen vergroten.
- Webhookvolgorde verondersteld. Webhooks kunnen worden gedupliceerd, out of order of gedeeltelijke staten vertegenwoordigen. Reconcileer op Stripe object IDs en gebruik een “already processed” guard.
- Annuleringen, refunds en credits niet expliciet gemodelleerd. Als je alleen gebruik optelt en nooit negatieve correcties opneemt, kom je in de verleiding om totalen te “fixen” met imports en dubbel te tellen.
Voorbeeld: je logt “10 API-calls” en geeft later een credit voor 2 calls vanwege een storing. Als je vervolgens de hele dag opnieuw inleest en ook de credit toepast, ziet de klant 18 calls (10 + 10 - 2) in plaats van 8.
Snelle checklist vóór livegang
Voordat je op echte klanten overgaat met gebruiksgebaseerde facturering, loop een laatste check uit op de basisdingen die dure factureringsfouten voorkomen. De meeste fouten zijn geen “Stripe-problemen”. Het zijn data-problemen: duplicaten, missende dagen en stille retries.
Houd de checklist kort en afdwingbaar:
- Dwing uniciteit af op usage events (bijv. unieke constraint op
event_id) en kies één id-strategie. - Sla elke webhook op, verifieer de signature en verwerk idempotent.
- Behandel ruwe usage als onveranderlijk. Corrigeer met adjustments (positief of negatief), niet met edits.
- Draai een dagelijkse reconciliatiejob die interne totalen (per customer, per meter, per dag) vergelijkt met de Stripe billing status.
- Voeg alerts toe voor gaten en anomalieën: missende dagen, negatieve totalen, plotselinge pieken of een groot verschil tussen “events ingeslikt” en “events gefactureerd”.
Een eenvoudige test: kies één klant, herstart ingestie voor de laatste 7 dagen en bevestig dat totalen niet veranderen. Als ze dat wel doen, heb je nog een idempotentie- of backfill-probleem.
Voorbeeldscenario: een realistische maand gebruik en facturen
Een klein supportteam rekent $0,10 per gesprek dat wordt afgehandeld. Ze verkopen het als gebruiksgebaseerde facturering met Stripe, maar vertrouwen groeit door wat er gebeurt als data rommelig is.
Op 1 maart begint de klant een nieuwe billingperiode. Elke keer dat een agent een gesprek sluit, zendt je app een usage event:
event_id: een stabiele UUID uit je appcustomer_idensubscription_item_idquantity: 1 gesprekoccurred_at: het sluitingstijdstipingested_at: wanneer je het voor het eerst zag
Op 3 maart probeert een background worker na een timeout opnieuw en zend hetzelfde gesprek nogmaals. Omdat event_id uniek is, wordt de tweede insert een no-op en veranderen totalen niet.
Halverwege de maand stuurt Stripe webhooks voor een invoice preview en later de gefinaliseerde factuur. Je webhook-handler slaat stripe_event_id, type en received_at op en markeert het pas verwerkt nadat je database-transactie committed is. Als de webhook twee keer binnenkomt, wordt de tweede aflevering genegeerd omdat stripe_event_id al bestaat.
Op 18 maart importeer je een late batch van een mobiele client die offline was. Het bevat 35 gesprekken van 17 maart. Die events hebben oudere occurred_at waardes, maar zijn nog geldig. Je systeem voegt ze in, herberekent dagelijkse totalen voor 17 maart en het extra gebruik wordt meegenomen op de volgende factuur omdat het nog binnen de open billing-periode valt.
Op 22 maart ontdek je dat één gesprek dubbel is vastgelegd door een bug die twee verschillende event_id waarden genereerde. In plaats van geschiedenis te verwijderen, schrijf je een adjustment-event met quantity = -1 en een reden zoals “duplicate detected.” Dat houdt het auditspoor intact en maakt de factuurwijziging uitlegbaar.
Volgende stappen: implementeer, monitor en iteratief verbeteren
Begin klein: één meter, één plan, één klantsegment dat je goed begrijpt. Het doel is eenvoudige consistentie — jouw cijfers moeten maand na maand met Stripe overeenkomen, zonder verrassingen.
Bouw klein, versterk daarna
Een praktisch eerste rollout:
- Definieer één event-shape (wat telt, in welke eenheid, op welke tijd).
- Sla elk event op met een unieke idempotency key en een duidelijke status.
- Agregeer naar dagelijkse (of uurlijkse) totalen zodat facturen uitlegbaar zijn.
- Reconcileer periodiek met Stripe webhooks, niet alleen realtime.
- Na facturatie behandel de periode als gesloten en rout laat binnenkomende events via een adjustment-pad.
Zelfs met no-code kun je sterke data-integriteit behouden als je invalide staten onmogelijk maakt: handhaaf unieke constraints voor idempotency keys, vereis foreign keys naar customer en subscription en vermijd het updaten van geaccepteerde raw events.
Monitoring die je later redt
Voeg vroeg audit-schermen toe. Ze verdienen zich terug de eerste keer dat iemand vraagt: “Waarom is mijn rekening deze maand hoger?” Handige weergaven zijn: zoeken op events per klant en periode, per-periode totalen per dag zien, webhook-verwerkingsstatus volgen en backfills en correcties inzien met wie/wanneer/waarom.
Als je dit implementeert met AppMaster (appmaster.io), past het model natuurlijk: definieer raw events, aggregaten en correcties in de Data Designer en gebruik Business Processes voor idempotente ingestie, geplande aggregatie en webhook-reconciliatie. Je krijgt nog steeds een echt grootboek en een auditspoor, zonder alle plumbing zelf te hoeven schrijven.
Wanneer je eerste meter stabiel is, voeg dan de volgende toe. Houd dezelfde lifecycle-regels, dezelfde audittools en dezelfde gewoonte: verander één ding tegelijk en verifieer end-to-end.
FAQ
Behandel het als een klein grootboek. Het moeilijke deel is niet het afrekenen van de kaart; het is het behouden van een nauwkeurige, verklaarbare registratie van wat er is meegeteld, ook wanneer gebeurtenissen laat aankomen, dubbel binnenkomen of moeten worden gecorrigeerd.
Een veilige standaard is: jouw database is de bron van waarheid voor ruwe gebruiksgebeurtenissen en hun status, en Stripe is de bron van waarheid voor facturen en betaalresultaten. Die verdeelde verantwoordelijkheid houdt facturering traceerbaar terwijl Stripe prijzen, belasting en incasso afhandelt.
Maak het stabiel en deterministisch zodat retries hetzelfde ID opleveren. Vaak is het afgeleid van de werkelijke businessactie, zoals een customer id plus een meter key plus een bronrecord-id; dan wordt een dubbele verzending een onschadelijke no-op in plaats van extra gebruik.
Bewerk of verwijder geen geaccepteerde gebruiksgebeurtenissen. Leg een compenserend correctie-event vast (inclusief negatieve hoeveelheden indien nodig) en laat het originele event intact zodat je later de geschiedenis kunt verklaren.
Houd ruwe gebruiksgebeurtenissen append-only, en sla aggregaten apart op als afgeleide data die je kunt herbouwen. Aggregaten zijn er voor snelheid en rapportage; ruwe events zijn er voor audits, disputen en het herbouwen van totalen na bugs of backfills.
Sla minstens twee timestamps op: wanneer het plaatsvond en wanneer je het hebt geïngest. Noteer ook de bron. Als de factuur nog niet is gefinaliseerd, herbereken dan vóór finalisatie; als hij al is gefinaliseerd, behandel het als een duidelijke correctie (extra charge of credit) in plaats van het stilletjes verschuiven van gebruik naar een andere periode.
Sla elke webhook-payload op die je ontvangt en handhaaf idempotente verwerking met Stripe’s event id als unieke sleutel. Webhooks worden vaak gedupliceerd of in verkeerde volgorde afgeleverd, dus je handler moet alleen state changes toepassen die records vooruit brengen.
Gebruik de billing period start en end van Stripe voor de window en split usage wanneer de actieve prijs verandert. Het doel is dat elke factuurregel gekoppeld kan worden aan een specifieke tijdsrange en prijs zodat totalen uitlegbaar blijven.
Sla een calculation run-id of soortgelijke metadata op zodat je de totalen kunt reproduceren. Als het opnieuw draaien van ingestie voor hetzelfde venster totalen verandert, heb je waarschijnlijk een idempotentie- of lifecycle-state probleem.
Modelleer de ruwe usage events, aggregaten, correcties en webhook-inbox tabellen in de Data Designer, en implementeer ingestie en reconciliatie in Business Processes met uniqueness constraints voor idempotentie. Je kunt een auditabel grootboek en geplande reconciliatie opzetten zonder alle plumbing handmatig te schrijven.


