---
title: "Stripe billing webhook (server-to-server)"
method: POST
path: "/api/webhooks/stripe"
tags: ["Infrastructure"]
---

# Stripe billing webhook (server-to-server)

`POST /api/webhooks/stripe`

## Request

- Base URL: `https://www.apier.no`
- URL: `https://www.apier.no/api/webhooks/stripe`
- Auth: none required

Stripe-signed webhook for billing lifecycle events. Handled events: checkout.session.completed (links the Stripe customer/subscription ids to the consumer, or — for sessions marked metadata.kind=credit_topup — grants prepaid credits exactly once), customer.subscription.created and customer.subscription.updated (map the subscription's price and status to the consumer's billing tier via the shared decision logic, including the past_due grace window), customer.subscription.deleted (downgrades the consumer to the free tier and sends a cancellation-confirmation email), charge.dispute.created (Sentry alert only — never a billing-state mutation), invoice.paid (sends a receipt email), and invoice.payment_failed (sends a payment-failed email; tier changes remain owned by the subscription events and the shared tier-decision logic). Lifecycle emails are exactly-once via a billing email ledger claim, so redeliveries and reconciliation re-runs can never re-send one. Called ONLY by Stripe and authenticated by the Stripe-Signature header — it is NOT a public or agent-facing surface, requires no Apier API key, and is intentionally outside the /api/v1 namespace (so the API-key auth model, response-provenance, and scope rules do not apply). A missing or invalid signature returns 400; the request body is never logged or echoed. Deliveries are idempotent at the database level (the Stripe event id is a primary key): a duplicate of a processed event is acknowledged with 200 and does no further work, while a delivery whose earlier run failed after the claim is re-run before acknowledging.

## Headers

- `Stripe-Signature` string, required

## Request body

- object: Opaque Stripe Event object (see the Stripe API reference). Apier verifies the signature, then acts only on a small allowlist of subscription event types; all other types are acknowledged and ignored.

## Response `200`

Event received and acknowledged — handled, ignored (unhandled type), or skipped as an already-processed duplicate. Stripe treats any 2xx as a successful delivery.

- object
  - `received` true, required: Always true once the signature is verified and the event is recorded.

## Other responses

- `400`: The Stripe-Signature header was missing or failed verification. The request body and the underlying error are never logged or echoed.
- `503`: Stripe is not configured in this environment (the secret key / webhook signing secret are unset). Expected outside real production, where these env vars are required.

---

[API](https://skmtc.dev/apier/apis/apier-no-api.md) · [All operations](https://skmtc.dev/apier/apis/apier-no-api/llms.txt) · [OpenAPI document](https://skmtc.dev/apier/apis/apier-no-api/revisions/9ec19bc87311?raw)
