Duplicate event
The provider event ID is recorded. Replays return a duplicate status without granting access twice.
Authenticated API docs
Stripe webhook payloads update entitlements after signature verification, provider event idempotency, and fail-closed product mapping.
Stripe sends events to POST /api/stripe/webhook. This is a provider route, not a customer API route under /api/v1.
{
"received": true,
"status": "processed"
}The handler processes Stripe event objects with an ID, event type, livemode flag, and data object.
{
"id": "evt_example",
"type": "customer.subscription.updated",
"livemode": false,
"data": {
"object": {
"customer": "cus_example",
"subscription": "sub_example",
"items": {
"data": [
{
"price": {
"id": "price_example",
"product": "prod_example",
"unit_amount": 2100
}
}
]
},
"metadata": {
"webot_entitlement_code": "api_agent_top",
"webot_account_id": "account_example"
}
}
}
}Configure STRIPE_WEBHOOK_SECRET in the runtime environment. The route rejects missing, malformed, stale, or mismatched signatures before trusting the payload.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyStripeSignature(payload: string, header: string, signingValue: string) {
const timestamp = header.match(/t=([^,]+)/)?.[1];
const signature = header.match(/v1=([^,]+)/)?.[1];
if (!timestamp || !signature) {
return false;
}
const expected = createHmac("sha256", signingValue)
.update(timestamp + "." + payload, "utf8")
.digest("hex");
return timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
}The provider event ID is recorded. Replays return a duplicate status without granting access twice.
Unsupported but valid event types can be acknowledged as ignored.
Unknown products, missing metadata, mismatched amounts, and unbound customers fail closed.
{
"received": true,
"status": "duplicate"
}After webhook-backed entitlement updates, dashboard callers can confirm access with /api/v1/me, /api/v1/entitlements, and /api/v1/usage.
Checkout success, cancel, session, plan, and payment query parameters are display-only. They may explain UI state, but they never grant API access, dashboard access, or quota.