Radondocs

Webhooks

One signature-verified handler for every provider's events. payments.webhooks.handle() verifies and normalizes any provider's payload into a single event schema.

A webhook is an HTTP request a provider sends to your server when something happens — a redirect payment completes, a dispute opens, a subscription renews. Every provider signs and shapes these differently. payments.webhooks.handle() verifies the signature and collapses them into one NormalizedEvent, so you write a single handler.

The handler

app/api/webhooks/route.ts (Next.js)
import { payments } from "@/lib/payments";

export async function POST(req: Request) {
  const event = await payments.webhooks.handle({
    body: await req.text(),        // RAW body — see the warning below
    headers: Object.fromEntries(req.headers),
  });

  switch (event.type) {
    case "charge.succeeded":
      await fulfillOrder(event.data.metadata.orderId);
      break;
    case "refund.succeeded":
      await markRefunded(event.data.id);
      break;
    case "subscription.canceled":
      await revokeAccess(event.data.customer.email);
      break;
  }

  return new Response("ok"); // 2xx tells the provider you received it
}

You must pass the RAW body

Signatures are computed over the exact bytes the provider sent. Pass the raw string/Buffer (await req.text(), or req.rawBody on Node frameworks) — never JSON.parsed-then-re-serialized data, or verification will fail every time. In Next.js route handlers, req.text() gives you the raw body.

The normalized event

typestring

The normalized event name, e.g. charge.succeeded, charge.failed, refund.succeeded, subscription.updated, subscription.canceled.

dataChargeResult | RefundResult | SubscriptionResult

The normalized object the event is about (same shapes as the API returns).

providerstring

Which provider sent it.

rawunknown

The original, untouched provider payload.

Configuring the signing secret

Set each provider's webhook signing secret via webhookSecret (or its RADON_<PROVIDER>_WEBHOOK_SECRET env var). See each provider's page for exactly which secret to use.

const payments = new RadonPayments({
  providers: {
    stripe: { webhookSecret: process.env.RADON_STRIPE_WEBHOOK_SECRET },
  },
});

With multiple providers, name the sender

Radon infers the provider from the signature only when exactly one is configured. With several, pass { provider } so it knows which verifier to use:

await payments.webhooks.handle({ body, headers }, { provider: "stripe" });

Otherwise it throws InvalidConfigError.

Provider-specific behavior

Return the right status

Returning a 2xx response tells the provider the event was received. If your handler throws, respond with a 4xx/5xx so the provider retries. Radon throws WebhookSignatureError when verification fails — treat that as a 400.

Next steps

On this page