Part 3 of 7 · Chargeback responder series ~7 min read

How the evidence gets gathered

A rebuttal is only as good as the evidence under it, so this is the part that does the digging. This post is about how the system gathers the four things a card network’s rules actually ask for — what was ordered, that it was delivered, that the customer engaged with it, and what they agreed to — each pulled from the system that really holds it, stored once, and indexed so the next step knows exactly what it has and, just as importantly, what it doesn’t.

Key takeaways

  • One gather Lambda pulls four things, because four things are what a card network’s rules actually weigh.
  • The order record, proof of delivery and tracking, the customer’s own messages, and the agreed policy — each from its real source.
  • Everything gathered is written once to an S3 evidence bucket and indexed, one row per piece, in DynamoDB.
  • The reason code decides which pieces matter most, so the gather is targeted, not a blind sweep.
  • When a piece is missing, the index records it as missing — the system never papers over a gap.

Four things, because that’s what banks weigh

A card network doesn’t want a folder of everything you have; it wants the few specific things that answer the dispute. Across the common reason codes, those few things come down to four. The order record: what was bought, when, for how much, the billing and shipping addresses, and the AVS and CVC results captured at the time of sale — this is what proves a real, authorised purchase happened. The proof of delivery and tracking: the carrier’s confirmation that the goods reached the customer, with a timestamp and a signature or photo where one exists — this answers “I never got it.” The customer’s own messages: the support thread or chat where they asked about, used, or acknowledged the order — a customer who emailed “thanks, it arrived” has undercut their own dispute. And the agreed policy: the refund, shipping, or subscription terms the customer accepted at checkout, with the version and the timestamp of their acceptance — this answers “I was charged after I cancelled” or “they never told me it was final sale.”

Targeted by reason code, not a blind sweep

The dispute’s reason code, read at the front door, tells the gather Lambda which of the four pieces carry the weight. A product-not-received dispute lives or dies on the delivery proof, so that source is pulled first and hardest. A fraudulent / unauthorised dispute turns on the order record — the AVS match, the CVC result, the shipping-to-billing address, the customer’s prior order history. A subscription-cancelled or credit-not-processed dispute turns on the agreed policy and the cancellation timestamp. The gather always collects all four where they exist (a strong packet leans on more than one), but it knows which is decisive, so it spends its effort and its retries where the case will actually be won or lost.

One gather Lambda pulling four evidence sources into S3 and a DynamoDB index In the centre of the AWS account boundary sits a single box, the gather Lambda named cbr-gather, triggered by the SQS queue. On the left, four external source boxes each feed into it with a labelled arrow. Top to bottom: the commerce platform supplying the order record (items, amount, billing and shipping address, AVS and CVC results); the carrier API supplying proof of delivery and tracking (delivery confirmation, timestamp, signature or photo); the support inbox or chat supplying the customer’s own messages; and the policy store supplying the refund and shipping policy the customer agreed to, with version and acceptance timestamp. From cbr-gather two arrows go right. The first writes the raw gathered evidence to an S3 evidence bucket, cbr-evidence, under a per-dispute prefix. The second writes one index row per piece to the DynamoDB evidence index, recording the evidence type, the S3 key, the source, when it was gathered, and a found-or-missing flag. A small callout near the index notes that a missing piece is recorded as missing rather than skipped. A note at the bottom reads: gather is targeted by the reason code, stores each piece once, and never fabricates one that does not exist. Commerce platform order, amount, AVS/CVC, billing + shipping address Carrier API delivery confirmation, timestamp, signature/photo Support inbox / chat the customer’s own messages Policy store agreed refund/shipping, version + acceptance time AWS account cbr-gather SQS-triggered; pulls four sources, targeted by reason code S3 · cbr-evidence raw evidence, stored once per dispute Evidence index one row per piece: type, key, found/missing store index Targeted by the reason code. Each piece stored once. A missing piece is recorded as missing — never invented.
Fig 3. The gather step. One Lambda reads the four real sources, writes each piece once to S3 under a per-dispute prefix, and indexes it in DynamoDB — including, honestly, the pieces it couldn’t find.

Store once, index every piece

Each piece the gather Lambda retrieves is written to the S3 evidence bucket under a prefix keyed by the dispute id — the order JSON, the carrier’s tracking response, the exported message thread, the policy snapshot at the version the customer accepted. Storing the raw source means the packet built later is reproducible: anyone can see exactly what the rebuttal was assembled from. Alongside each S3 write, the Lambda puts one row into the evidence index in DynamoDB: the dispute id, the evidence type, the S3 key, the source it came from, the timestamp it was gathered, and a flag for whether it was found. The index is the manifest the next step reads — it never has to crawl S3 to discover what evidence exists.

Carriers and inboxes are flaky, so gather is built to retry. It runs off the SQS queue with a dead-letter queue behind it: a transient carrier-API error is retried a couple of times, and only a genuinely stuck gather lands in the DLQ with an alarm, where it surfaces to a human instead of silently blocking the case. A delivery confirmation that isn’t available yet (the parcel is still in transit) isn’t an error — it’s recorded as not-yet-available, and the deadline watch will trigger a re-gather closer to the cutoff.

A missing piece is data, not a blank

The single most important rule in this step: when a piece of evidence doesn’t exist, the system records that it doesn’t exist. No delivery confirmation for a digital product. No support thread because the customer never wrote in. No signature because the carrier doesn’t capture one for low-value parcels. Each gap is written to the index as missing, with the reason. This is what keeps the whole system honest downstream: the packet-builder is handed a truthful manifest of what’s there and what isn’t, so it can never be tempted — or asked — to fill a hole with something invented. A gap in the evidence becomes a known fact about the case’s strength, not a silent omission.

Design rules for gathering evidence

  • Four sources, every time: order, delivery proof, customer messages, agreed policy. Those are what banks weigh.
  • Targeted by reason code. The decisive source for this dispute is pulled first and hardest.
  • Store the raw source in S3 once, so the packet built from it is always reproducible.
  • Index one row per piece, so the next step reads a manifest instead of crawling storage.
  • Missing is recorded as missing. A gap is a known fact about the case, never a blank to be filled.
All posts