# Events and webhooks

> Every lifecycle event SalesDogg records, how webhooks are signed and retried, and how to verify them.

Every change to a customer's purchases is recorded as an **event** and sent to your **webhook endpoints**. List them with `npx -y @salesdogg/cli events list --app-user-id user_1`.

## Event types

| Type | Meaning |
| --- | --- |
| `initial_purchase` | First paid period of a subscription. |
| `trial_started` | A free trial began. |
| `trial_converted` | The first payment after a trial went through. |
| `renewal` | A new period was paid. |
| `cancellation` | Auto-renew was turned off. Access continues until the paid period ends. |
| `uncancellation` | Auto-renew was turned back on. |
| `billing_issue` | A renewal payment failed. |
| `billing_recovered` | A failed renewal was paid after all. |
| `paused` | The subscription was paused. |
| `resumed` | A paused subscription resumed. |
| `product_change` | The customer moved to another product. |
| `expiration` | The subscription ended and its access was removed. |
| `refund` | A payment was refunded. |
| `non_renewing_purchase` | A one-time purchase. |
| `transfer` | A purchase moved to another customer (restore behavior `transfer`). |
| `grant_created` | Access was granted without a purchase. |
| `grant_revoked` | A grant was revoked. |
| `test` | Sent by `webhooks test`. |

## Register an endpoint

```sh
npx -y @salesdogg/cli webhooks create --url https://example.com/webhooks/salesdogg
npx -y @salesdogg/cli webhooks test <endpoint_id>
```

The response includes the endpoint's signing `secret`, shown once. Endpoints must be public `https` URLs; addresses inside private networks are refused, and redirects are not followed. Pass `--event-types` to receive only some events.

`--format revenuecat` sends RevenueCat's webhook body instead of SalesDogg's event object, for receivers written for RevenueCat; add `--authorization-header` to send a fixed `Authorization` header. Events with no RevenueCat counterpart (`billing_recovered`, grants) are not sent in that format.

## Verify the signature

Each delivery carries a `SalesDogg-Signature` header: `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`, keyed with the endpoint's secret. Verify it against the raw body, before parsing:

```ts
import { verifyWebhook } from "@salesdogg/sdk";

export async function POST(request: Request) {
  const event = await verifyWebhook(
    await request.text(),
    request.headers.get("salesdogg-signature"),
    process.env.SALESDOGG_WEBHOOK_SECRET!,
  ); // throws if the signature is wrong or older than 5 minutes
  // handle event.type
  return new Response(null, { status: 200 });
}
```

## Delivery and retries

Respond with a 2xx status. A failed delivery is retried up to 8 attempts in all, with the wait growing from 30 seconds and capped at 24 hours. `npx -y @salesdogg/cli webhooks deliveries` lists attempts and their results. Deliveries are claimed with row locks, so with several API instances each one is still sent once.

Handle events idempotently by `id`: a retry after a timeout can deliver an event your handler already processed.
