Prompt
I'm building a webhook listener for Stripe and GitHub — how should I structure it?
Latest observation
Oct 5, 2026
OpenAI APIWeb search: off
A good webhook listener structure is:
-
One public endpoint per provider
- e.g.
/webhooks/stripeand/webhooks/github - Keep them separate because they have different auth, signing, payloads, and retry behavior.
- e.g.
-
Verify authenticity before doing anything
- Stripe: verify the
Stripe-Signatureheader with your endpoint secret. - GitHub: verify the
X-Hub-Signature-256HMAC with your webhook secret. - Do this on the raw request body before parsing/mutating it.
- Stripe: verify the
-
Parse minimally, route by event type
- Read the event type first:
- Stripe:
event.type - GitHub:
X-GitHub-Eventheader
- Stripe:
- Dispatch to handlers like:
handleStripeInvoicePaidhandleGitHubPullRequestOpened
- Read the event type first:
-
Acknowledge fast
- Return
2xxas soon as the event is validated and queued/recorded. - Don’t do long DB work or API calls inline if you can avoid it.
- Webhook providers retry on timeouts/non-2xx.
- Return
-
Make processing idempotent
- Store a unique event ID and ignore duplicates.
- Stripe includes an event
id. - GitHub delivery has an
X-GitHub-DeliveryUUID. - Assume retries and duplicate deliveries will happen.
-
Use a queue/background worker
- Webhook endpoint:
- validate
- persist event
- enqueue job
- return
200
- Worker:
- performs business logic
- retries safely
- logs failures
- Webhook endpoint:
-
Separate “ingestion” from “business logic”
- Keep the HTTP layer thin.
- Put provider-specific normalization in adapter code.
- Put app logic in service classes/functions.
-
Log enough for debugging
- provider, delivery ID, event type, signature result, processing result
- avoid logging secrets or full sensitive payloads if not necessary
Suggested structure
webhooks/
stripe/
controller
verifier
parser
handlers/
github/
controller
verifier
parser
handlers/
common/
event_store
queue
idempotency
logging
Or in a simpler code organization:
src/
routes/
stripeWebhook.ts
githubWebhook.ts
services/
stripe/
verify.ts
handleEvent.ts
github/
verify.ts
handleEvent.ts
jobs/
processWebhook.ts
db/
webhookEvents.ts
Processing flow
- Receive request
- Capture raw body
- Verify signature
- Extract provider event ID + type
- Check idempotency store
- Persist event record
- Enqueue worker job
- Return
200 OK - Worker processes event
- Update status / retry on failure
Important provider-specific notes
Stripe
- Must use the raw body exactly as received for signature verification.
- Event types can be many; only handle the ones you need.
- Stripe retries for a long time, so idempotency is critical.
- Good practice: use Stripe object IDs too, not just event IDs, when updating domain objects.
GitHub
- Verify with
X-Hub-Signature-256. - Use
X-GitHub-Eventto route. - GitHub may send
pingevents for setup health checks. - Delivery IDs are useful for deduplication.
Minimal pseudocode
POST /webhooks/:provider
raw = readRawBody(request)
if provider == "stripe":
verifyStripeSignature(raw, headers)
event = parseStripeEvent(raw)
eventId = event.id
eventType = event.type
if provider == "github":
verifyGitHubSignature(raw, headers)
eventType = header["X-GitHub-Event"]
eventId = header["X-GitHub-Delivery"]
event = parseJson(raw)
if alreadyProcessed(eventId):
return 200
storeEvent(eventId, provider, eventType, raw)
enqueue("processWebhook", {provider, eventId})
return 200
Common pitfalls
- Parsing JSON before verifying signatures
- Doing slow work before responding
- Not handling duplicates
- Not storing the raw payload
- Using a single generic handler for both providers without normalization
- Logging secrets or full PII unnecessarily
If you want, I can also give you:
- a Node/Express example,
- a Python/FastAPI example,
- or a production-ready architecture with queue + DB schema.