Skip to main content
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.

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

{
  "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

{
  "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

{
  "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.
This event is fired only on the transition into past_due. A successful recovery does not fire suspended again — it fires access_granted.

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

{
  "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 suspendedsuspended 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

{
  "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.