Prompt
What’s the best way to handle inbound webhooks from Stripe, Shopify, and GitHub?
Latest observation
The best approach is to treat all webhooks as untrusted, retryable, at-least-once events and build a small, shared ingestion pipeline around that idea.
Recommended pattern
1. Receive fast, verify, and enqueue
For each provider:
- Verify the signature immediately
- Parse minimally
- Store the raw payload + headers
- Enqueue a job/event
- Return 2xx quickly
Don’t do business logic directly in the HTTP handler. Webhook providers retry on slow failures, so your handler should be thin.
2. Make processing idempotent
Assume duplicates will happen.
Use a deduplication key such as:
- Stripe:
event.id - GitHub:
X-GitHub-Deliveryheader - Shopify:
X-Shopify-Webhook-Idheader, or your own combination if unavailable
Persist a “seen” record with a unique constraint. If you receive the same event again, acknowledge it and skip processing.
3. Separate transport from business logic
Normalize each provider’s payload into your internal event model, something like:
{
"provider": "stripe",
"event_id": "evt_123",
"type": "payment_succeeded",
"occurred_at": "2026-01-01T12:00:00Z",
"raw": { ... }
}
Then route that to domain handlers:
payment_succeededorder_createdissue_opened- etc.
This keeps Stripe/Shopify/GitHub specifics out of your app logic.
4. Use a queue and retries
Webhook providers already retry delivery, but your internal processing should also have retries for transient failures.
Use a queue with:
- retry policy
- dead-letter queue
- visibility into failures
Examples:
- SQS + worker
- RabbitMQ
- Kafka
- Celery / Sidekiq / BullMQ / Hangfire, etc.
5. Keep raw payloads for audit/debugging
Store:
- raw request body
- headers
- provider
- received timestamp
- signature verification status
- processing status
- error details
This makes debugging much easier when someone asks, “Why didn’t that order sync?”
6. Use provider-specific verification correctly
Stripe
- Verify with the raw request body
- Use the Stripe signing secret and timestamp tolerance
- Prefer Stripe’s official SDK verification
Shopify
- Verify HMAC signature using the shared secret
- Must also use the raw body
- Be careful with request body parsing/encoding
GitHub
- Verify
X-Hub-Signature-256using the webhook secret - Use constant-time comparison
- Also validate the event type from headers
Practical architecture
A solid setup looks like this:
-
Webhook endpoint
- provider-specific route
- signature verification
- persist raw event
- return 200
-
Ingestion table
- event id
- provider
- headers
- raw payload
- received_at
- status
-
Queue
- pushes event id or ingestion record id
-
Worker
- fetches event
- deduplicates
- transforms to internal event
- runs domain logic
- marks processed/failed
-
Monitoring
- metrics for success/failure/retry counts
- alert on dead-letter queue growth
Important gotchas
- Do not trust the event order
- Do not assume exactly-once delivery
- Do not perform slow API calls in the webhook request
- Do not parse the body before verifying signatures
- Do not treat retries as errors if you already processed the event
If you want the simplest good solution
Use this rule set:
- Verify signature
- Save raw event
- Deduplicate by provider event ID
- Enqueue work
- Respond 200 immediately
- Process asynchronously with retries
If you want, I can also give you:
- a unified webhook handler design,
- provider-specific verification examples in Node/Python/Ruby/Go, or
- a database schema for storing and deduping webhook events.