Skip to content
Integrations

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

  1. Provider POSTs JSON to a URL on your app.
  2. Your app verifies the signature so it knows the call is real.
  3. Your app extracts the event type + payload, does the work, returns 200.
  4. 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:

  1. Deploy a "staging" version of the app with STAGING_=true.
  2. Point a separate webhook endpoint in the provider at it.
  3. 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

SymptomLikely cause
Signature verification always failsBody is being JSON-parsed before verification — need the raw body
First event works, repeats failNo idempotency → unique key collision in DB
Provider keeps retrying the same eventHandler is returning 5xx (or timing out beyond the provider's window)
Discord deactivated my interactions endpointMissed the { type: 1 } ping reply within 3s

What's next

Webhooks · FlareX