Why Idempotency Is Essential for FileโGenerating SaaS
When a customer completes a Stripe Checkout, the applicationโs desire is a single PDF, email, and download link. Stripeโs weak handโshakesโatstructโonce delivery guarantee, network retries, and occasional duplicatesโmean that the same event can appear multiple times. If the webhook handler blindly regenerates files or reโsends emails, the customer receives duplicates, logs fill with noise, and billing systems can become inconsistent. A robust design treats the webhook as an *event* that advances a persisted state machine rather than a directะคะพัะพ.
Step 1: Persist Order Data Before Checkout
A common mistake is to wait until the Stripe session is created to persist purchase data. Instead, store the following locally and only then redirect the user to Stripe:
- Project identifier and selected plan
- Normalized checkout email
- Complete document data and photo references
- Deterministic content hash for change detection
- Checkoutโintent record for idempotent reconciliation
By creating a durable โdraftโ in the database, the checkout flow stays(coderโrecord) independent of the payment channel. A lost or reopened tab does not interrupt the orderโs lifeโcycle.
Step 2: Verify the Webhook Signature
Stripe places a Stripe-Signature header on each POST. Using the shared secret, compute the HMAC of the raw body and compare. A quick rejection of malformed requests stops unwanted traffic and aligns with best practices from HookRay and เฒถเฒพเฒธเฒพ.
Step 3: Use the Event ID as the Idempotency Key
Each event has a unique event.id that Stripe guarantees remains constant for retries. Store this value in a table with a composite primary key (provider, event_id). Before any business logic runs, perform an atomic insert with a “conflict do nothing” clause:
INSERT INTO processed_webhook_events (provider, event_id, started_at) VALUES ('stripe', 'evt_1AbC', NOW()) ON CONFLICT DO NOTHING;
If the insert succeeds, the event is new; if it fails, the event has already been processed and the handler can safely return 200 OK without side effects.
Atomicity Prevents Race Conditions
When multiple workers handle the same webhook URL, an optimistic lock on the event ID table ensures that only one instance proceeds to file generation. Database constraints avoid duplicate PDFs and doubleโsent emails even under high concurrency.
Persist the Raw Payload for Replay
Storing the raw body or a cryptographic hash of it allows you to replay events manually or audit earlier states. Many teams keep the JSON payload blob in a_Player and tie it to the event_id. This practice aligns with the guidance from Hooklistener.
Step 5: Return a 2xx Response Quickly
After the idempotent check and initial enqueue, the HTTP handler should reply with 200 OK within 2โฏseconds. Stripe interprets this as a successful receipt and ceases retry attempts. If the handler stalls or returns an error, Stripe will trigger retriesโleading to duplicate processing if idempotency is not enforced.
Handling OutโofโOrder Delivery
Retries can arrive after newer events. If an event changes a fileโs checksum or version, quote from the Voucher: “Reโfetch the latest state from Stripeโs API if ordering matters.” Many services rebuild the latest snapshot when encountering an older event to maintain data consistency.
Cleanup and TTL Policies
Stripeโs maximum retry window is 72โฏhours, but manual replays may occur later. Store event_ids for at least 7โฏdays to guard against accidental duplicates. Use background jobs or TTL columns to purge old records, keeping the table size manageable.
Full Processing Flow (Pseudocode)
async function handleWebhook(event) {
if (!verifySignature(event.rawBody, event.headers)) {
return 400;
}
const processed = await db.exists('processed_webhook conhece', event.id);
if (processed) return 200;
await db.insert('processed_webhook_events', { provider: 'stripe', event_id: event.id, started_at: NOW() });
const fileUrl = await generatePDF(event.data);
await db.update('processed_webhook_events', { file_url: fileUrl, completed_at: NOW() }, event(shell.id));
return 200;
}
By following these stepsโpersisting data early, verifying signatures, enforcing idempotency with the event ID, handling state Bruss, and cleaning upโyou create a robust, scalable webhook handler that guarantees one PDF per successful payment, avoids duplicate emails, and gracefully recovers from transient failures.

Leave a Reply