Webhooks
Stripe notifies Xagent of billing events so a team's plan and usage stay in sync — a subscription starting, renewing, changing, or being cancelled. This is server-to-server: your integration does not call these endpoints, but self-hosters must configure them.
Endpoint
POST /api/webhooks/stripeThis endpoint is public — it is not protected by a bearer token. Its authenticity is guaranteed by the Stripe signature instead (below), so only Stripe can deliver valid events.
Signature Verification
Each request carries a stripe-signature header. Xagent verifies it against the STRIPE_WEBHOOK_SECRET before processing. Configure both Stripe credentials on the deployment (see Environment Variables):
STRIPE_SECRET_KEY=sk_live_... # calling the Stripe API
STRIPE_WEBHOOK_SECRET=whsec_... # verifying inbound webhooksVerification failures are rejected before any handler runs:
HTTP/1.1 400 Bad Request
{ "detail": "Invalid payload" } # body could not be parsed
HTTP/1.1 400 Bad Request
{ "detail": "Invalid signature" } # stripe-signature did not verifyHandled Events
| Event | Effect on the team |
|---|---|
checkout.session.completed | Links the Stripe subscription and customer to the team; marks the subscription active. |
customer.subscription.created, customer.subscription.updated | Resolves the plan from the Stripe price id; updates status, current period, and cancel-at-period-end. Resets usage counters when the plan changes. |
customer.subscription.deleted | Reverts the team to the Free plan and resets usage. |
invoice.paid | Advances the billing period using the paid invoice's line item. |
invoice.payment_failed | Marks the subscription past_due. |
Unhandled events
Only the events above change state. Note that invoice.payment_succeeded is not handled — period advancement keys off invoice.paid. Any other event type is accepted with 200 OK and ignored.
Idempotency & Retries
Each event is recorded by its Stripe event.id. A redelivery of an already-processed event is a no-op:
HTTP/1.1 200 OK
{ "status": "ok" } # processed for the first time
HTTP/1.1 200 OK
{ "status": "already_processed" } # duplicate event id — ignoredIf a handler raises, the error is re-raised so Stripe retries delivery later with its standard backoff — make sure the endpoint is reachable during deploys.
Example Payload
A checkout.session.completed event. The team_id is carried in metadata as a string:
{
"id": "evt_1NabcXYZ",
"type": "checkout.session.completed",
"data": {
"object": {
"subscription": "sub_1NabcSub",
"customer": "cus_1NabcCus",
"metadata": { "team_id": "1" }
}
}
}