> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lavendly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook receiver

> Stripe-to-Lavendly webhook ingest. Verifies signature, dedupes, credits the user.

This endpoint is called by **Stripe**, not by your client. It receives
events for the user-account configured as `STRIPE_WEBHOOK_SECRET`.

### Required Stripe configuration

In your Stripe dashboard → Developers → Webhooks, add:

* **Endpoint URL**: `https://your-domain/api/v1/stripe/webhook`
* **Events to send**:
  * `checkout.session.completed`
  * `customer.subscription.deleted`
  * `customer.subscription.paused`
  * `invoice.payment_failed`
* Copy the **Signing secret** into `STRIPE_WEBHOOK_SECRET`.

### What gets granted on `checkout.session.completed`

The session carries `metadata.lavendly_kind` and either
`lavendly_plan` (subscription) or `lavendly_credits` (pack). The
handler dispatches:

| Kind           | Action                           |
| -------------- | -------------------------------- |
| `subscription` | `setUserPlan(user_id, plan)`     |
| `credit_pack`  | `topUpCredits(user_id, credits)` |

### Dedupe

Stripe delivers events at least once. We persist a marker per event
id at `/data/storage/_billing/_events/<event_id>.json`; a re-delivery
returns `{ "received": true, "deduped": true }` without re-running
the handler.

### Errors

| Status | Meaning                                                      |
| ------ | ------------------------------------------------------------ |
| 400    | Missing or invalid signature → Stripe stops retrying.        |
| 200    | Processed (or deduped).                                      |
| 500    | Handler crashed, Stripe will retry with exponential backoff. |
| 503    | `STRIPE_SECRET_KEY` not configured, Stripe will retry.       |
