All articles
webhooksfraudidempotencyarchitecture

Idempotent Conversion Webhooks That Reject Fraud

Yulia Kovalenko
· 5 min read

A conversion webhook is the single point where money gets attributed. When Stripe fires a checkout event, or your own backend posts a signup, that payload decides which affiliate earns a commission. Get the ingestion wrong and you pay the same commission twice, or you pay a fraudster, or you drop a legitimate conversion that a partner is counting on. None of these failures are loud. They surface weeks later as a reconciliation discrepancy nobody can explain.

The fix is not a bigger queue or a retry loop. It is treating the webhook endpoint as an idempotent, authenticated, fraud-aware boundary from the first line of code. Here is how we build that boundary at Argus Grape, and how you can reason about your own.

Dedup on event id, not payload

Stripe sends an event id like evt_1Q2x on every delivery, and it will deliver the same event more than once: a network blip, a slow 200, a manual replay. The wrong instinct is to hash the payload body and dedup on that, because two distinct conversions can carry identical bodies and one conversion can arrive re-serialized so it hashes differently. Dedup on the provider event id instead. Persist it before you do any work. We write it to a unique-constrained column inside the same transaction that records the conversion, and if the insert hits a duplicate key we return 200 immediately and touch nothing else. For our internal webhook, which has no native event id, we require the caller to send a UUID in an Idempotency-Key header and treat it identically.

Verify the signature before you parse

Signature verification comes first, before JSON parsing, before any database touch. Stripe signs the raw request body with a timestamped HMAC; you recompute it with your endpoint secret and compare in constant time. If you parse the body into an object first, you have already let an unauthenticated payload steer your code, and re-serializing for verification will not match the bytes that were signed.

A few rules we hold to without exception:

  • Compare signatures with a constant-time function, never with string equality, to close the timing side channel.
  • Reject any request whose signature timestamp is outside a five-minute window, which kills naive replay attempts.
  • Read the raw body exactly once, before any middleware can consume or rewrite it.
  • Scope each endpoint secret to one source, so a leaked internal key cannot forge a Stripe event.

Auto-reject fraud at ingestion

A signed, deduplicated event can still be fraudulent. The conversion may trace back to a click we already flagged, an affiliate under a silent shadow-ban, or a device fingerprint with a risk score past threshold. The webhook handler is the right place to make that call, because once a commission lands in the referral tree it ripples up every parent level and becomes expensive to claw back.

So we score before we credit. The handler joins the conversion to its originating click and the affiliate's standing, runs the same risk model the click pipeline uses, and either credits the commission, holds it for review, or rejects it outright. A shadow-banned affiliate still gets a clean 200, exactly as they expect. Nothing in the response reveals that the conversion was quarantined, which denies the fraudster the feedback loop they need to adapt.

Return 200 for anything you successfully recorded a decision about. Reserve non-2xx for failures the sender should retry, never for fraud you intend to silently drop.

Make the contract explicit

Idempotency is a contract, and contracts have to be written down. Document which header carries the dedup key, how long you remember it, what each status code means, and what a held-for-review conversion looks like on reconciliation. When a partner replays a week of events to backfill, they should be able to predict the outcome to the cent. Our full payload schema, signature scheme, and retry semantics live in the webhooks reference, and the rejection codes map one-to-one to the fraud signals described there. Build the endpoint this way and webhooks stop being a source of mystery double-pays. Verify, dedup, score, then credit, in that order, every time.

Last updated May 13, 2026.