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

Auth: No bearer authStripe signature verified

POST /api/webhooks/stripe

This 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 webhooks

Verification 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 verify

Handled Events

EventEffect on the team
checkout.session.completedLinks the Stripe subscription and customer to the team; marks the subscription active.
customer.subscription.created, customer.subscription.updatedResolves 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.deletedReverts the team to the Free plan and resets usage.
invoice.paidAdvances the billing period using the paid invoice's line item.
invoice.payment_failedMarks 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 — ignored

If 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" }
    }
  }
}

Next Steps