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

# Event types

> Every event Boble can send, what it means for your business, and a full payload example for each.

This page is the canonical reference for the events Boble emits. For each one you'll find:

* **When it fires** — in plain language, with the underlying transition.
* **What it means** — what just happened to your business.
* **What you can do with it** — concrete ideas creators use today.
* **Full payload** — a real example, ready to paste into your handler.

All `user_promotion.*` events share the same nested objects (`user`, `promotion`). Only the top-level `type`, the `status`, and the timing fields change between transitions. The shared envelope is documented in [Payload structure](/webhooks/payload-structure).

***

## `user_promotion.access_granted`

A buyer just gained access to one of your promotions.

### When it fires

* A buyer completed a one-off paid checkout.
* A buyer started a subscription (Stripe `customer.subscription.created`).
* A buyer claimed a free promotion.
* You granted access manually from your dashboard.

### What it means

Someone is now an **active member** of this promotion. They can consume the content, attend the calls, or whatever the promotion unlocks. This is the moment your business gained a new customer for this product.

### What you can do with it

* Send a "🎉 New sale" notification to your team in Discord or Slack with the buyer's name, email, and the promotion title.
* Add the buyer to a Mailchimp / Brevo / ConvertKit list, tagged with the promotion they just bought, to enroll them into your onboarding sequence.
* Grant them a role in your Discord server, an access in a Notion workspace, or any other resource gated behind the promotion.
* Append a new row to a Google Sheet for your sales log.

### Payload

```json theme={null}
{
  "id": "0d6d31a3-2b9e-4d9f-9a51-3b9c3f8e6c10",
  "type": "user_promotion.access_granted",
  "api_version": "2026-01-01",
  "created": 1717497600,
  "livemode": true,
  "data": {
    "object": {
      "id": "f3e4c9e2-31a8-4d1c-9c4a-e8d2b3a17a44",
      "user_id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
      "promotion_id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
      "promotion_payment_id": "b9d8c7e6-5a4b-3c2d-1e0f-9a8b7c6d5e4f",
      "status": "active",
      "start_date": "2026-06-04T12:00:00.000Z",
      "end_date": null,
      "user": {
        "id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
        "email": "lea.martin@example.com",
        "firstname": "Léa",
        "lastname": "Martin",
        "username": "lea.m",
        "avatar": "https://cdn.boble.app/uploads/users/2c47.../avatar.jpg",
        "preferred_language": "fr",
        "created_at": "2026-04-12T09:31:00.000Z"
      },
      "promotion": {
        "id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
        "title": "Pro membership",
        "type": "subscription",
        "currency": "EUR",
        "amount": 19
      }
    }
  }
}
```

### Fields worth highlighting

* `status` is `active`.
* `start_date` is the exact moment access began.
* `end_date` is `null` for open-ended subscriptions and ongoing access.
* `promotion.type` tells you whether this was a `free` claim, a `one_time` purchase, or a `subscription` start.
* `promotion_payment_id` is `null` for free promotions and manual grants.

***

## `user_promotion.canceled`

A buyer's access has been canceled.

### When it fires

* The buyer canceled their subscription from their account.
* You canceled their access manually from your dashboard.
* Stripe reported the subscription as deleted (`customer.subscription.deleted`).

### What it means

This buyer **no longer has access** to the promotion. For a subscription, this is the equivalent of "they unsubscribed". For a one-off, this is a manual revoke on your side.

### What you can do with it

* Drop a quiet note in your team's Discord/Slack so you know someone churned and can reach out if you want.
* Remove the buyer's tag in your email tool, or move them to a "former customer" segment for a win-back sequence.
* Revoke the role you granted them on your Discord server, or remove their Notion access.
* Update your churn dashboard.

### Payload

```json theme={null}
{
  "id": "1b6e22c8-9d11-4f0e-8b2a-7c5d9e0f1a23",
  "type": "user_promotion.canceled",
  "api_version": "2026-01-01",
  "created": 1719312800,
  "livemode": true,
  "data": {
    "object": {
      "id": "f3e4c9e2-31a8-4d1c-9c4a-e8d2b3a17a44",
      "user_id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
      "promotion_id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
      "promotion_payment_id": "b9d8c7e6-5a4b-3c2d-1e0f-9a8b7c6d5e4f",
      "status": "canceled",
      "start_date": "2026-06-04T12:00:00.000Z",
      "end_date": "2026-06-25T08:13:20.000Z",
      "user": {
        "id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
        "email": "lea.martin@example.com",
        "firstname": "Léa",
        "lastname": "Martin",
        "username": "lea.m",
        "avatar": "https://cdn.boble.app/uploads/users/2c47.../avatar.jpg",
        "preferred_language": "fr",
        "created_at": "2026-04-12T09:31:00.000Z"
      },
      "promotion": {
        "id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
        "title": "Pro membership",
        "type": "subscription",
        "currency": "EUR",
        "amount": 19
      }
    }
  }
}
```

### Fields worth highlighting

* `status` is `canceled`.
* `end_date` is set to the exact moment access ended.
* Same `id` as the original `access_granted` — this is the same `user_promotion`, just transitioned.

***

## `user_promotion.expired`

A subscription term ended and was not renewed.

### When it fires

* A subscription that had been stuck in `past_due` was finally given up on (Stripe stopped trying to charge it).
* A fixed-term access reached its end date.

### What it means

The buyer **lost access** because their subscription effectively ran out. This is different from `canceled`: the buyer didn't choose to cancel, the access simply timed out (most often after several failed payment attempts).

### What you can do with it

* Trigger a different email sequence than for `canceled` — typically a "your subscription expired, here is the link to resubscribe" reminder rather than a win-back.
* Remove their access in external tools, exactly like for `canceled`.
* Log the expiration in your dashboard so you can monitor your dunning success rate.

### Payload

```json theme={null}
{
  "id": "2c7f33d9-ae22-501f-9c3b-8d6e0f1b2c34",
  "type": "user_promotion.expired",
  "api_version": "2026-01-01",
  "created": 1721904000,
  "livemode": true,
  "data": {
    "object": {
      "id": "f3e4c9e2-31a8-4d1c-9c4a-e8d2b3a17a44",
      "user_id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
      "promotion_id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
      "promotion_payment_id": "b9d8c7e6-5a4b-3c2d-1e0f-9a8b7c6d5e4f",
      "status": "expired",
      "start_date": "2026-06-04T12:00:00.000Z",
      "end_date": "2026-07-25T12:00:00.000Z",
      "user": {
        "id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
        "email": "lea.martin@example.com",
        "firstname": "Léa",
        "lastname": "Martin",
        "username": "lea.m",
        "avatar": "https://cdn.boble.app/uploads/users/2c47.../avatar.jpg",
        "preferred_language": "fr",
        "created_at": "2026-04-12T09:31:00.000Z"
      },
      "promotion": {
        "id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
        "title": "Pro membership",
        "type": "subscription",
        "currency": "EUR",
        "amount": 19
      }
    }
  }
}
```

### Fields worth highlighting

* `status` is `expired`.
* `end_date` is the moment access actually ended (typically after the dunning window closed).

***

## `user_promotion.suspended`

A recurring payment failed and the buyer's access is paused.

### When it fires

* A subscription invoice could not be charged. Stripe reports the subscription as `past_due` and Boble suspends access.

### What it means

The buyer **still exists** as a customer, but their access is **temporarily blocked** until the payment succeeds. This is your dunning window — most of the time, payments recover on the second or third attempt and you'll receive a fresh `access_granted` once that happens. If they never recover, you'll eventually receive an `expired`.

<Note>
  This event is fired only on the transition into `past_due`. A successful recovery does not fire `suspended` again — it fires `access_granted`.
</Note>

### What you can do with it

* Email the buyer with a link to update their payment method. This is one of the highest-ROI things you can wire on a webhook.
* Pause their access in external tools, but **don't revoke it permanently** — they will most likely come back.
* Add them to a "dunning" segment in your CRM for follow-up.

### Payload

```json theme={null}
{
  "id": "3d8044ea-bf33-612a-ad4c-9e7f1a2c3d45",
  "type": "user_promotion.suspended",
  "api_version": "2026-01-01",
  "created": 1720008400,
  "livemode": true,
  "data": {
    "object": {
      "id": "f3e4c9e2-31a8-4d1c-9c4a-e8d2b3a17a44",
      "user_id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
      "promotion_id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
      "promotion_payment_id": "b9d8c7e6-5a4b-3c2d-1e0f-9a8b7c6d5e4f",
      "status": "past_due",
      "start_date": "2026-06-04T12:00:00.000Z",
      "end_date": null,
      "user": {
        "id": "2c47a8d4-5e3a-4b1c-9f8d-71a2b8c3d4e5",
        "email": "lea.martin@example.com",
        "firstname": "Léa",
        "lastname": "Martin",
        "username": "lea.m",
        "avatar": "https://cdn.boble.app/uploads/users/2c47.../avatar.jpg",
        "preferred_language": "fr",
        "created_at": "2026-04-12T09:31:00.000Z"
      },
      "promotion": {
        "id": "6a5c1b3e-2f8d-4a9b-9c7d-8e1f2a3b4c5d",
        "title": "Pro membership",
        "type": "subscription",
        "currency": "EUR",
        "amount": 19
      }
    }
  }
}
```

### Fields worth highlighting

* `status` is `past_due` (not `suspended` — `suspended` is the event name, the underlying status is `past_due`).
* `end_date` stays `null` because the access is paused, not ended.

***

## `webhook.test`

A test event you triggered manually from your dashboard.

### When it fires

You clicked the "Send test event" button (or hit the test API). The test event is dispatched **even if your endpoint is disabled**, so you can verify connectivity before flipping the switch on.

### What it means

Nothing happened on a real buyer. This is a smoke test to confirm that:

* Your endpoint is reachable.
* Your handler accepts a Boble webhook delivery.
* Your signature verification logic works.

### What you can do with it

* Use it after deploying changes to your handler, to confirm everything still works without waiting for a real sale.
* Use it once at integration time to validate your verification code.

### Payload

```json theme={null}
{
  "id": "8c00aabb-ccdd-eeff-1122-334455667788",
  "type": "webhook.test",
  "api_version": "2026-01-01",
  "created": 1717497600,
  "livemode": false,
  "data": {
    "object": {
      "message": "This is a test event",
      "endpoint_id": "8c12e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
      "sent_at": "2026-06-04T12:00:00.000Z"
    }
  }
}
```

### Fields worth highlighting

* `data.object` is intentionally minimal — no buyer, no promotion, just a marker that this is a test.
* `livemode` is `false` outside production.
* A test delivery still appears in your event log so you can inspect what your endpoint replied.
