Skip to main content
The /webhook route is the Stripe webhook receiver. Stripe sends signed POST requests to this endpoint when subscription-related events occur. The route verifies the signature before processing the event payload.

POST /webhook

Request

Content-Type: application/json (raw text — do not pre-parse)
stripe-signature
string
required
The signature header added by Stripe to every webhook request. Used to verify that the request originated from Stripe and has not been tampered with. The value is a comma-separated string of timestamp and HMAC signatures.
The request body must be read as raw text before signature verification. Parsing the body as JSON before calling stripe.webhooks.constructEvent will invalidate the signature check.

Webhook source

export const action = async ({ request }: LoaderFunctionArgs) => {
  let event;
  const body = await request.text();
  const stripeSignature = request.headers.get("stripe-signature") as string;
  const webhookSecret = "whsec_...";

  if (!stripeSignature || !webhookSecret) {
    return data({ error: "Webhook configuration error." }, { status: 400 });
  }

  try {
    event = stripe.webhooks.constructEvent(body, stripeSignature, webhookSecret);
  } catch (err) {
    return Response.json(null, { status: 400 });
  }

  const dataObject = event.data.object;
  switch (event.type) {
    case "invoice.paid":               break;
    case "invoice.payment_failed":     break;
    case "customer.subscription.deleted": break;
    default:                           break;
  }

  return Response.json({ "data": "webhook" }, { status: 200 });
};

Signature verification

The route uses stripe.webhooks.constructEvent to verify the request:
event = stripe.webhooks.constructEvent(body, stripeSignature, webhookSecret);
  • body — the raw request body read with request.text().
  • stripeSignature — the value of the stripe-signature header.
  • webhookSecret — the webhook signing secret from the Stripe dashboard (format: whsec_...).
If verification fails, Stripe’s SDK throws an error which is caught and results in a 400 response.

Handled event types

Event typeDescription
invoice.paidFired when an invoice is successfully paid. Use this to activate or extend access for the subscriber.
invoice.payment_failedFired when an invoice payment attempt fails. Use this to notify the customer or restrict access.
customer.subscription.deletedFired when a subscription is cancelled or expires. Use this to revoke access and update the customer’s status.

Responses

StatusConditionBody
400stripe-signature header is missing, webhook secret is not configured, or signature verification fails.null or { "error": "Webhook configuration error." }
200Event received, signature verified, and event processed.{ "data": "webhook" }
Always read the request body with request.text() before passing it to stripe.webhooks.constructEvent. Calling request.json() first will mutate the body stream and cause signature verification to fail, resulting in a 400 error for every event.
Store your webhook signing secret (whsec_...) in an environment variable. Never hard-code it in source code or commit it to version control.
Register this endpoint in the Stripe dashboard under Developers → Webhooks. For local development, use the Stripe CLI to forward events: stripe listen --forward-to localhost:5173/webhook.
Stripe retries failed webhook deliveries (non-2xx responses) for up to 72 hours with exponential backoff. Ensure your handler is idempotent — the same event may be delivered more than once.

Build docs developers (and LLMs) love