Webhooks
Receive inbound webhook calls on your FlareX app — verify signatures, handle retries, idempotency.
Updated
Webhooks are how external services tell your app that something happened — Stripe pings you when a payment succeeds, GitHub pings you when someone pushes, Discord pings you when a message comes in (slash commands, interactions). The pattern is consistent across providers; the details differ.
The basic flow
- Provider POSTs JSON to a URL on your app.
- Your app verifies the signature so it knows the call is real.
- Your app extracts the event type + payload, does the work, returns 200.
- If you return a non-2xx, the provider retries with backoff.
Step 1: Pick the URL
Convention: one path per provider. So /webhooks/stripe, /webhooks/github, /webhooks/discord. Easy to grep, easy to rate-limit per source.
For your FlareX app:
https://<your-app>-<hex>.flarex.app/webhooks/<provider>
Step 2: Configure the provider
Each provider has a webhook UI:
- Stripe: Dashboard → Developers → Webhooks → Add endpoint
- GitHub: repo Settings → Webhooks → Add webhook
- Discord (incoming): channel settings → Integrations → Webhooks → New
- Cloudflare / Resend / Twilio / Vercel: similar dashboard flows
Pick the events you care about. Don't subscribe to "all events" — you'll waste compute parsing things you ignore.
Step 3: Add the signing secret
Every provider that does webhooks signs them. The signing secret is shown once, when you create the endpoint.
Add it to Secrets:
STRIPE_WEBHOOK_SECRET=whsec_...
GITHUB_WEBHOOK_SECRET=...
DISCORD_PUBLIC_KEY=...
Step 4: Verify the signature
Different providers, different schemes:
Just tell FlareX the provider and it wires the right verifier:
Add a Stripe webhook handler at /webhooks/stripe. Use
STRIPE_WEBHOOK_SECRET from Secrets to verify signatures with the
Stripe SDK. Handle checkout.session.completed and invoice.paid.
Step 5: Handle idempotency
Webhooks deliver at least once, sometimes twice or three times for the same event. Don't process duplicates:
const event = stripe.webhooks.constructEvent(...);
// Skip if we've already processed this event id
const seen = await prisma.processedWebhookEvent.findUnique({
where: { id: event.id },
});
if (seen) return reply.send({ idempotent: true });
await prisma.processedWebhookEvent.create({ data: { id: event.id } });
// ... do the work
The unique constraint on id doubles as the idempotency lock — even concurrent duplicates will fail the second insert.
Step 6: Return 200 fast
Providers expect 200 within a few seconds. If your handler does slow work (calls an LLM, sends email, runs a long query), don't block the response — enqueue a job and return 200 immediately:
app.post('/webhooks/stripe', async (req, reply) => {
const event = verifyAndParse(req);
await queue.add('stripe-event', event); // background job
reply.send({ received: true }); // returns immediately
});
The worker picks up the job and does the actual work.
Testing webhooks locally
The provider needs a public URL — your FlareX app already has one. So the simplest dev loop is:
- Deploy a "staging" version of the app with
STAGING_=true. - Point a separate webhook endpoint in the provider at it.
- Tail logs while you trigger events on the provider's side.
For Stripe specifically, the Stripe CLI can replay events against any URL.
Common gotchas
| Symptom | Likely cause |
|---|---|
| Signature verification always fails | Body is being JSON-parsed before verification — need the raw body |
| First event works, repeats fail | No idempotency → unique key collision in DB |
| Provider keeps retrying the same event | Handler is returning 5xx (or timing out beyond the provider's window) |
| Discord deactivated my interactions endpoint | Missed the { type: 1 } ping reply within 3s |
What's next
- Stripe walkthrough — full Checkout + subscription webhook flow
- Discord webhooks — incoming, not interactions
- Build an API service — when your app is the webhook handler
- Secrets — webhook signing keys