BackstagePass

Opening the door…

Automations

Connect BackstagePass to your tools

Pick how you’ll connect.

Give this to your AI agent

Paste it into your agent. It will ask you for your key.

Connect to my BackstagePass account.

1. Read https://backstagepass.com/developers/llms.txt, then https://backstagepass.com/developers/llms-full.txt.
2. Requests are described in https://backstagepass.com/developers/openapi.json. Webhook bodies are described in https://backstagepass.com/developers/webhooks.schema.json.
3. Base URL: https://backstagepass.com. Send my API key in the header Authorization: Bearer <key>. Ask me for the key. Never guess it.
4. Start with GET /api/v1/me and show me the handle it returns. The People calls (POST /api/v1/people and its revoke, block and unblock) change my list: ask me before each one.
5. On 401, ask me for a new key. On 429, wait 60 seconds, then try again.
6. You cannot add a webhook yourself. Tell me the exact URL and moments to add in Studio > Automations > APIs & Webhooks, then ask me for that row's signing secret.
7. Follow the numbered steps at https://backstagepass.com/help/how/automations#agent-steps.
Contents
For creators

Connect in 3 steps

No code. Your tool gives you a web address. BackstagePass sends each moment there.

  1. Copy your tool’s webhook address

    Zapier

    1. In Zapier, make a new Zap.
    2. Trigger: search Webhooks by Zapier. Event: Catch Hook. Press Continue.
    3. Copy the webhook URL Zapier shows.
    4. Once a real moment has arrived, press Test trigger to see its fields.

    If the Zap is off, Zapier answers 404 and BackstagePass counts a failed delivery.

    Make

    1. In Make, create a new scenario.
    2. Add the Webhooks app, module Custom webhook.
    3. Press Add, give it a name, press Save.
    4. Copy the address Make shows. Make waits for the first request to learn the fields.

    n8n

    1. Add a Webhook node.
    2. Set HTTP Method to POST. It starts on GET.
    3. Copy the Production URL, not the Test URL.
    4. Turn the workflow on. Until then, nothing reaches the Production URL.
    5. Each arrival shows under Executions, with its fields.

    Pabbly

    1. In Pabbly Connect, make a new workflow.
    2. For the trigger app, choose Webhook by Pabbly.
    3. Copy the webhook URL it shows.
  2. Paste it in BackstagePass

    1. Open Studio → Automations → APIs & Webhooks.
    2. On Webhooks, paste the URL.
    3. Every moment starts ticked. Untick the ones you don't need.
    4. Press Add the webhook.
  3. Pick what happens

    In your tool, add the step that does the work: a contact, a post, a row. Use a recipe below.

What can happen

Each tick box in Studio, in plain words.

Tick box in StudioWhat happenedCarries a fanName in your tool
Follow confirmationSomeone joined your listYeswelcome
Live scheduledYou booked a liveNoseat_reserved
Going liveYou went liveNogoing_live
15 minutes beforeYour live starts in 15 minutesNoreminder_15m
60 minutes beforeYour live starts in an hourNoreminder_60m
1 day beforeYour live is a day awayNoreminder_1d
Live rescheduledYou moved a liveNolive_rescheduled
Live cancelledYou cancelled a liveNolive_canceled
Seat reservedA fan RSVP'd to a liveYescounted_in
Encore publishedYour replay is readyNoencore_published
New pass createdYou put a Pass on saleNonew_pass
Pass expiredA fan's Pass endedYeswin_back
Joined backstageA fan bought a PassYesjoined_backstage
Bought a ticketA fan got a ticketYesticket_bought
Became inactiveA fan went quiet for 60 daysYesgone_quiet
New transactionA fan paid youYesnew_transaction

Recipes

Every new Pass holder into my CRM

Tick
Joined backstage only.
Then
Your CRM's create or update contact step. Use the fan's email, name and phone.

The moment does not say which Pass yet.

Field names
FieldUse it for
fan.emailEmail
fan.nameName
fan.phonePhone
fan.idMatch on this to avoid duplicates

Post Going Live in my Discord or Slack

Tick
Going live only.
Then
Discord or Slack send channel message. Put the live's title and its room link in the message.
Field names
FieldUse it for
stream.titleThe live's title
stream.room_urlThe link fans tap

Log every transaction to a Google Sheet

Tick
New transaction only.
Then
Google Sheets add a row. One column per field below.
Field names
FieldUse it for
transaction.occurred_atWhen
transaction.kindsubscription, ticket, tip or drop
transaction.amount_centsAmount, in cents
transaction.amount_after_fees_centsAfter fees, in cents. Empty when not reported
fan.nameWho
fan.emailTheir email

Send my Welcome through my email tool

Tick
Follow confirmation only.
Then
Your email tool's add subscriber step, with the fan's email. Skip it when the email is empty.

Turn off Follow confirmation in Automations so fans don't get two welcomes. The webhook keeps sending.

Field names
FieldUse it for
fan.emailEmail
fan.nameName

Export my audience to a spreadsheet

Once
Studio → Followers → Export. Pick who to include. The file opens in Google Sheets or Excel.
Every week
Needs your API key and a scheduled step in your tool.
How, with the API key

your tool's scheduled HTTP request step (Zapier: Webhooks by Zapier GET; Make: HTTP → Make a request). Address https://backstagepass.com/api/v1/audience/export, header Authorization: Bearer <your API key>. Then add the rows to your sheet.

curl https://backstagepass.com/api/v1/audience/export \
  -H "Authorization: Bearer $BSP_API_KEY" \
  -o audience.csv

Add and remove people from your tools

These go the other way: your tool calls BackstagePass. They need your API key (Studio → Automations → APIs & Webhooks → API access → Generate new key) and, to add to a Pass, its Pass ID (Studio → Passes, the Copy button beside Pass ID).

The POST step in your tool

Zapier

  1. Add an action: search Webhooks by Zapier. Event: POST. Press Continue.
  2. URL: the address above.
  3. Payload Type: Json.
  4. Data: one line per field in the body below. Map the email from your trigger.
  5. Headers: Authorization = Bearer <your API key>.
  6. Press Test step. 201 or 200 means it worked.

Make

  1. Add the HTTP app, module Make a request.
  2. URL: the address above. Method: POST.
  3. Headers: name Authorization, value Bearer <your API key>.
  4. Body: JSON (application/json). Paste the body below and map the email from your trigger.
  5. Press Run once. 201 or 200 means it worked.

n8n

  1. Add an HTTP Request node. Method: POST. URL: the address above.
  2. Turn on Send Headers: Authorization = Bearer <your API key>.
  3. Turn on Send Body. Body Content Type: JSON. Specify Body: Using JSON. Paste the body below and map the email.
  4. Run the step. 201 or 200 means it worked.

Add a buyer from my website or newsletter to a Pass

When
Your shop, form or newsletter tool's own trigger, such as new order or new subscriber.
Then
An HTTP POST to https://backstagepass.com/api/v1/people with the buyer's email, your Pass ID and "attested": true.
They get
They show on Followers as Invited, with the Pass waiting for them. It is theirs when they sign in with that email. No email goes from you before that, except the welcome letter when welcome is true.

Body

{
  "email": "<the buyer's email>",
  "pass_id": "pass_3f9a0c1e7b2d4a68",
  "welcome": true,
  "attested": true
}

attested: true is your word that they opted in to hear from you by email.

For a Pass that ends, add "expires_at": "2026-12-31T23:59:00Z".

Sending the same email again is safe: it is still one person.

Remove someone

When
Your tool's own refund, cancel or unsubscribe trigger.
Then
An HTTP POST to https://backstagepass.com/api/v1/people/revoke with their email. Add pass_id to take one Pass only.
One Pass
A grant ends now. A paid membership stops renewing and runs to the end of its paid term.
Everything
The Followers tab's Revoke. Someone still Invited has the invite withdrawn.
For good
POST https://backstagepass.com/api/v1/people/block with their email. Only for someone who has signed in.

Body

{
  "email": "<their email>",
  "pass_id": "pass_3f9a0c1e7b2d4a68"
}

Nothing is refunded. Refunds are made one charge at a time on Transactions.

Good to know

  1. Your tool does not check our signature. Keep the webhook URL private, like a password.
  2. Send a ping proves the link. It carries no fan, so your tool learns the fan fields from the first real moment.
  3. A real moment is real: booking a live tells your followers. A follow from a second account is a safe one to start with.
  4. A webhook sends even when that moment's notification is off in Automations. Became inactive is the one exception.
  5. Moments about one fan carry their name, email and phone. Email or phone can be empty.
  6. Money is in cents. 2700 is 27.00.
  7. 8 failed deliveries in a row pause the webhook. Fix your tool, press Send a ping, then Turn back on.
For developers

Quickstart

Prove the key, then prove a signed webhook lands. About five minutes.

  1. Get a key

    Studio → Automations → APIs & Webhooks → API access → Generate new key. Account owner only.

  2. Call the API

    export BSP_API_KEY='paste-your-key-here'
    curl https://backstagepass.com/api/v1/me \
      -H "Authorization: Bearer $BSP_API_KEY"

    Expect 200:

    {
      "handle": "maya",
      "display_name": "Maya Rivers",
      "plan": "free"
    }

    401 {"error":"unauthorized"} means the key is wrong, replaced or disabled.

  3. Catch a signed webhook

    1. Open webhook.site and copy Your unique URL.
    2. Studio → Automations → APIs & Webhooks → Webhooks: paste it, press Add the webhook. Copy the row's signing secret.
    3. Press Send a ping on the row.
    4. webhook.site shows one POST with X-Backstagepass-Event: ping, X-Backstagepass-Signature and this body:
    {
      "event": "ping",
      "creator": {
        "handle": "maya"
      },
      "occurred_at": "2026-10-24T01:00:03Z"
    }
  4. Check the signature

    Paste the raw body, header and secret into a verifier below. Then build your receiver from the reference.

Key, auth, errors

Who can
The account owner only. Teammates see the card but not the key.
Format
40 letters and digits. One key per account.
Replace
Press Generate new key again. The old key stops working at once.
Disable
Press Disable key. Every call with it gets 401 at once.
Scope
Your account's data only. The People calls are the only writes.
Base URL
https://backstagepass.com
Auth header
Authorization: Bearer <api-key>
Methods
GET, and POST for the People calls. A POST body is JSON (Content-Type: application/json).
Responses
The object itself, no envelope. /api/v1/audience/export returns CSV. Times are ISO 8601, UTC.
Rate limit
120 requests per minute per key. Over that, 429 until the minute passes. No rate-limit headers.
Version
In the path: /api/v1. No version header. This is the only version.
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
404{"error":"not found"}People calls: the email is not on your list.
422{"error":"<plain words>"}People calls: the request cannot be done as sent. The sentence says why.
429{"error":"rate limited"}Over 120 requests in a minute.

Endpoints

GET/api/v1/meYour creator profile

No parameters.

curl https://backstagepass.com/api/v1/me \
  -H "Authorization: Bearer $BSP_API_KEY"
Example response
{
  "handle": "maya",
  "display_name": "Maya Rivers",
  "plan": "free"
}
FieldTypeMeaning
handlestringYour handle: backstagepass.com/<handle>.
display_namestringYour name as fans see it.
planstringInternal plan identifier.
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
429{"error":"rate limited"}Over 120 requests in a minute.

GET/api/v1/audienceYour audience, as counts

Counts only, no people. The reachable numbers are the same ones the Followers page and the composer show.

No parameters.

curl https://backstagepass.com/api/v1/audience \
  -H "Authorization: Bearer $BSP_API_KEY"
Example response
{
  "fans": 1240,
  "push_reachable": 610,
  "email_reachable": 980,
  "imported_awaiting_first_sign_in": 75,
  "text_reachable": 0,
  "imported": 300,
  "imported_claimed": 120,
  "generated_at": "2026-10-27T18:04:05Z"
}
FieldTypeMeaning
fansintegerPeople who follow you.
push_reachableintegerFollowers a push notification can reach now.
email_reachableintegerFollowers an email can reach now. Not imported contacts who have not signed in.
imported_awaiting_first_sign_inintegerImported contacts eligible for email who have not signed in yet.
text_reachableintegerFollowers a text message can reach.
importedintegerContacts you imported.
imported_claimedintegerImported contacts who have since signed in.
generated_atstringWhen this answer was computed (UTC).
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
429{"error":"rate limited"}Over 120 requests in a minute.

GET/api/v1/audience/exportEvery person, as a CSV file

One row per person: every follower, then imported contacts who have not signed in. The same columns as Export on Followers.

No parameters.

curl https://backstagepass.com/api/v1/audience/export \
  -H "Authorization: Bearer $BSP_API_KEY" \
  -o audience.csv
Content-Type
text/csv; charset=utf-8
Encoding
UTF-8, starting with a byte order mark (BOM). Strip it if your parser does not.
Transfer
Streamed. No Content-Length, no Content-Disposition.
Columns, in order

name, email, phone, active_access, expired_access, active_names, expired_names, lifetime_spend, spend_passes, spend_tickets, spend_drops, eligible_lives, showed_up, showed_up_%, encores_started, encore_started_%, total_watch_time, live_minutes, encore_minutes, reachable_push, reachable_email, reachable_sms, sms_consent, email_sent, email_opened, email_open_rate, push_sent, push_delivered, push_open_rate, last_attended, lives_ago, last_sign_in, joined, source, blocked, tags, notes

StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
429{"error":"rate limited"}Over 120 requests in a minute.

GET/api/v1/streamsYour ended lives and their notifications

Your 50 most recently ended lives, newest first, each with the notifications sent for it. A live that was taken down is left out. No fan appears in this payload. No pagination.

No parameters.

curl https://backstagepass.com/api/v1/streams \
  -H "Authorization: Bearer $BSP_API_KEY"
Example response
{
  "streams": [
    {
      "title": "Friday soundcheck",
      "slug": "friday-soundcheck",
      "started_at": "2026-10-24T01:00:00Z",
      "ended_at": "2026-10-24T02:10:00Z",
      "peak_in_room": 99,
      "hearts": 412,
      "cut_retention": {
        "before": 90,
        "after": 72,
        "pct": 80
      },
      "broadcasts": [
        {
          "trigger": "go_live",
          "sent_at": "2026-10-24T01:00:12Z",
          "receipt": {
            "push": {
              "sent": 1,
              "delivered": 0,
              "opened": 0,
              "no_transport": 0,
              "suppressed_quiet_hours": 0,
              "suppressed_never_activated": 0,
              "suppressed_no_consent": 0,
              "suppressed_muted": 0,
              "suppressed_signal_tier": 0,
              "suppressed_digest": 0,
              "suppressed_plan_cap": 0,
              "suppressed_announcement_type": 0,
              "suppressed_frequency_cap": 0,
              "suppressed_push_primary": 0,
              "failed": 0
            }
          },
          "walked_in_10m": {
            "push": 1
          }
        }
      ]
    }
  ],
  "generated_at": "2026-10-27T18:04:05Z"
}
FieldTypeMeaning
streams[].titlestringThe live's title.
streams[].slugstringThe live's page: backstagepass.com/<handle>/s/<slug>.
streams[].started_at / ended_atstring|nullUTC.
streams[].peak_in_roomintegerMost people in the room at once.
streams[].heartsintegerReactions sent during the live.
streams[].cut_retentionobject|nullRoom count at the cut (before), 10 to 14 minutes later (after), and after/before as a rounded percent (pct). null if the live was never cut or there are no samples.
streams[].broadcasts[].triggerstringWhat sent the notification, e.g. go_live, stream_reminder, went_backstage.
streams[].broadcasts[].sent_atstringUTC.
streams[].broadcasts[].receiptobjectCounts per channel (push, email, sms, whatsapp). A channel with all-zero counts is left out.
streams[].broadcasts[].walked_in_10mobjectPer channel: people who entered the room within 10 minutes of the notification reaching them. Channels with nobody are left out.
generated_atstringWhen this answer was computed (UTC).
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
429{"error":"rate limited"}Over 120 requests in a minute.

People and Passes

One person on your list, keyed by email: add, find, revoke, block and unblock. Each call does what Studio’s Followers page does for one person.

The Invited rule

  1. Adding someone works like Studio → Followers → Add people: they are Invited. No account and no follow are created, and the Passes are queued on their invite.
  2. The one email they can get before signing in is the welcome letter, only when welcome is true. It goes once ever per person.
  3. When they sign in (with a code to that address, or Apple/Google), they become a follower and every queued Pass is granted, carrying its until.
  4. Apart from the welcome letter, nobody gets email from you until they have signed in at least once.
  5. Nothing new fires: the API's add fires exactly what Studio's Add people fires.

Pass IDs

Format
pass_ plus 16 lowercase letters and digits. The integer id never appears in the API.
In Studio
Studio → Passes: each Pass prints its Pass ID with a Copy button.
By API
GET /api/v1/passes, below.

Bodies and errors

Bodies
JSON in (Content-Type: application/json), JSON out.
Errors
Always {"error": "<plain words>"}.
Rate limit
The same 120 requests a minute per key.
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
404{"error":"not found"}The email is not on your list. An account with nothing to do with you is a 404 too.
422{"error":"<plain words>"}The request cannot be done as sent. Each call below lists its sentences.
429{"error":"rate limited"}Over 120 requests in a minute.

GET/api/v1/passesEvery Pass you have, with its public id

Every Pass on your account, oldest first, with the shelf’s state word and every price it is sold at.

No parameters.

curl "https://backstagepass.com/api/v1/passes" \
  -H "Authorization: Bearer $BSP_API_KEY"
Example response
{
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "status": "published",
      "prices": [
        {
          "cadence": "monthly",
          "price_cents": 900
        },
        {
          "cadence": "yearly",
          "price_cents": 9000
        }
      ]
    }
  ],
  "generated_at": "2026-10-29T12:00:00Z"
}
FieldTypeMeaning
passes[].pass_idstringThe Pass's public id.
passes[].namestringThe Pass's name.
passes[].statusstringThe word the Pass shelf uses: published, link_only, archived or draft.
passes[].prices[].cadencestringmonthly, quarterly, half_yearly, yearly or lifetime.
passes[].prices[].price_centsinteger|nullThe price at that cadence, in cents.
generated_atstringWhen this answer was computed (UTC).
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
429{"error":"rate limited"}Over 120 requests in a minute.

The person object

Every People call returns this shape. An Invited person, who has not signed in since you added them, carries three more fields and holds nothing yet.

Signed in
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": "1p",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "grant",
      "until": "2026-12-31T23:59:00Z",
      "active": true
    }
  ]
}
Invited
{
  "status": "invited",
  "id": null,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": null,
  "follower": false,
  "invited": true,
  "revoked_at": null,
  "blocked": false,
  "access": "Invited",
  "source": "API",
  "passes": [],
  "invited_at": "2026-10-29T12:00:00Z",
  "welcome_sent": true,
  "queued_passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "until": "2026-12-31T23:59:00Z"
    }
  ]
}
FieldMeaning
statusinvited: added, and not signed in since. signed_in: has signed in and is on your list. withdrawn: only in the answer to the revoke that withdrew the invite.
idThe person's account id. null for somebody who has never signed in.
followertrue when they follow you and the follow has not been revoked.
invitedtrue when status is invited.
revoked_atWhen you revoked them (the follow row stays on your list).
blockedtrue when you have blocked them. A block closes every door.
accessThe Followers page's Access chip, word for word: Blocked, Revoked, a count such as 1p · 2t, Follower, or Invited. null for a withdrawn invite.
sourceThe Followers page's Source column: API, Manual, Organic, or a tracking link's label.
passesOne entry per Pass of yours they hold or used to hold. Entries that have run out are included with active: false.
passes[].kindgrant (you gave it) or subscription (they bought it).
passes[].untilWhen it ends: the grant's expiry, or the membership's cancel date. null means no end is scheduled (a permanent grant, or a membership that renews).
invited_atInvited only. When you added them.
welcome_sentInvited only. Whether the welcome letter has gone.
queued_passesInvited only. The Passes waiting for them, each with the until its grant will carry when they sign in (null for no end).

POST/api/v1/peopleAdd one person, as Add people does

Studio → Followers → Add people for one person. It adds them as Invited (see the Invited rule). It is idempotent on email.

curl https://backstagepass.com/api/v1/people \
  -H "Authorization: Bearer $BSP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "pass_id": "pass_3f9a0c1e7b2d4a68",
  "expires_at": "2026-12-31T23:59:00Z",
  "welcome": true,
  "attested": true
}'
FieldRequiredNotes
emailyesThe key. Case and surrounding spaces are ignored.
attestedyes, trueThe sheet's Confirmation box: you confirm this person consented to be added to your audience and to get email from you. Anything but true adds nobody.
namenoUp to 120 characters. If the person already has an account, their own name wins.
phonenoDigits with country_code ("US", "GB" or "+44"), or E.164 on its own (+14155550132). It is stored on their contact row and is never a sign-in.
country_codenoReads phone, as the sheet's country picker does.
pass_idnoQueues this Pass for them.
pass_idsnoSeveral Passes at once, as the sheet's picker takes them. May be sent beside pass_id.
expires_atnoISO 8601 time in the future. Requires a Pass. It is the end of every Pass in this request. Leave it out for a grant with no end.
welcomeno, default falsetrue sends the welcome letter, once ever per person, exactly as the sheet's "Send a welcome email now" box does.

201 when the person was not on your list, 200 when they were (Invited already, or a follower). The body is the person object.

Example response: 201, welcome on
{
  "status": "invited",
  "id": null,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": null,
  "follower": false,
  "invited": true,
  "revoked_at": null,
  "blocked": false,
  "access": "Invited",
  "source": "API",
  "passes": [],
  "invited_at": "2026-10-29T12:00:00Z",
  "welcome_sent": true,
  "queued_passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "until": "2026-12-31T23:59:00Z"
    }
  ]
}
Example response: 201, welcome off

Send "welcome": false, or leave it out. No email goes.

{
  "status": "invited",
  "id": null,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": null,
  "follower": false,
  "invited": true,
  "revoked_at": null,
  "blocked": false,
  "access": "Invited",
  "source": "API",
  "passes": [],
  "invited_at": "2026-10-29T12:00:00Z",
  "welcome_sent": false,
  "queued_passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "until": "2026-12-31T23:59:00Z"
    }
  ]
}
Example response: 200, already a follower
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": "1p",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "grant",
      "until": "2026-12-31T23:59:00Z",
      "active": true
    }
  ]
}

How it behaves

Not on your list
An Invited row (Source API on the Followers page, the Invited chip), with the Passes queued. Nobody is followed, and no email goes except the welcome when asked for.
Already Invited
The same row. New Passes are added to the queue. For a Pass already queued, until becomes this request's expires_at (none means no end).
Already a follower
They signed in long ago, so the Pass is granted now, as the sheet grants it. A Pass they already hold has its end set to this request's expires_at.
An account that does not follow you
Invited, like everybody else. Being on a list is not a relationship until they sign in.
An invite you withdrew
Posting the address again makes it Invited again on the same row. The welcome is still once ever, so a second letter never goes.
422 errorWhen
Adding people needs your word first — confirm everyone here opted in to hear from you by email. Nobody was added.attested was not true.
email is required
That email address cannot be read
That phone number cannot be read — send it with its country code, like +14155550132
No pass with that pass_id on your accountAn unknown id, another creator's Pass, or an archived Pass.
expires_at needs a pass_id
expires_at must be an ISO 8601 time, like 2026-12-31T23:59:00Z
expires_at is in the past
That is your own account
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
429{"error":"rate limited"}Over 120 requests in a minute.

GET/api/v1/people?email=One person on your list, by email

200: the person object, for an Invited person or a signed-in one. 404 {"error":"not found"} when the email is neither (a withdrawn invite included).

curl "https://backstagepass.com/api/v1/people?email=ada@example.com" \
  -H "Authorization: Bearer $BSP_API_KEY"
Example response: signed in
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": "1p",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "grant",
      "until": "2026-12-31T23:59:00Z",
      "active": true
    }
  ]
}
Example response: Invited
{
  "status": "invited",
  "id": null,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": null,
  "follower": false,
  "invited": true,
  "revoked_at": null,
  "blocked": false,
  "access": "Invited",
  "source": "API",
  "passes": [],
  "invited_at": "2026-10-29T12:00:00Z",
  "welcome_sent": true,
  "queued_passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "until": "2026-12-31T23:59:00Z"
    }
  ]
}
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
404{"error":"not found"}The email is not on your list. An account with nothing to do with you is a 404 too.
429{"error":"rate limited"}Over 120 requests in a minute.

POST/api/v1/people/revokeRevoke one Pass, or everything

curl https://backstagepass.com/api/v1/people/revoke \
  -H "Authorization: Bearer $BSP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com",
  "pass_id": "pass_3f9a0c1e7b2d4a68",
  "mode": "at_period_end"
}'
FieldRequiredNotes
emailyes
pass_idnoLeave it out to revoke everything.
modeno, default at_period_endOnly applies to a paid membership on that Pass. at_period_end stops renewal: access runs to the end of the paid term and the card is never charged again. immediate closes the door now and stops billing at period end.
Invited, with pass_id
That Pass comes off their queue, with its until. Nothing was granted yet, so nothing else changes.
Invited, without pass_id
The invite is withdrawn. Every queued Pass goes, they leave the Invited list, and signing in later makes them nothing on your list. The answer has status: "withdrawn", and a GET is then a 404. Studio's Followers page has no button for this; the API's withdraw is the one way to take an invite back. The record is kept, so the welcome stays once ever and an unsubscribe they made from it still holds.
Signed in, with pass_id
A grant of that Pass ends now. A membership to that Pass is cancelled at Whop as Studio's bulk Cancel does it (at_period_end), or revoked as the Followers Revoke does it (immediate), and the member gets the same email. Nothing is refunded. Refunds are made one charge at a time on Transactions.
Signed in, without pass_id
This is the Followers tab's Revoke for this one person. Paid memberships are revoked through the bulk revoke path, and every grant and ticket of yours ends. The follow row stays on your list stamped Revoked, so access becomes Revoked. They can re-follow or re-buy on their own; block them to stop that.

200: the person object plus what happened.

Example response: one Pass, a membership at period end
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": "1p",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "subscription",
      "until": "2026-11-30T00:00:00Z",
      "active": true
    }
  ],
  "revoked": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "subscription",
      "until": "2026-11-30T00:00:00Z"
    }
  ],
  "skipped": []
}
Example response: everything
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": false,
  "invited": false,
  "revoked_at": "2026-10-29T12:00:00Z",
  "blocked": false,
  "access": "Revoked",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "subscription",
      "until": "2026-11-30T00:00:00Z",
      "active": false
    },
    {
      "pass_id": "pass_9b0e2f6c1a3d5e70",
      "name": "Back Room",
      "kind": "grant",
      "until": "2026-10-29T12:00:00Z",
      "active": false
    }
  ],
  "revoked": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "subscription",
      "until": "2026-11-30T00:00:00Z"
    },
    {
      "pass_id": "pass_9b0e2f6c1a3d5e70",
      "name": "Back Room",
      "kind": "grant",
      "until": "2026-10-29T12:00:00Z"
    },
    {
      "name": "Tuesday coaching call",
      "kind": "ticket"
    },
    {
      "kind": "follow"
    }
  ],
  "skipped": []
}
Example response: an invite withdrawn
{
  "status": "withdrawn",
  "id": null,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": null,
  "follower": false,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": null,
  "source": "API",
  "passes": [],
  "revoked": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "queued"
    },
    {
      "kind": "invite"
    }
  ],
  "skipped": []
}
Example response: a lifetime seat skipped
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": "1p",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "subscription",
      "until": null,
      "active": true
    }
  ],
  "revoked": [],
  "skipped": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "subscription",
      "reason": "lifetime seat — nothing renews"
    }
  ]
}

revoked[].kind is queued (a Pass taken off an invite), invite (the invite withdrawn), grant, subscription, ticket or follow. skipped[].reason uses the bulk tool's own words:

skipped[].reasonWhen
already winding downA cancel was already scheduled. The API never cancels twice.
lifetime seat — nothing renewsA one-time seat has no renewal to stop in at_period_end mode. Use immediate to close its door.
not active, already canceled, already paused
the payment service refused: …Whop said no, so the membership is still live there.
holds nothing on this pass
already revoked, not a followerOnly when revoking a signed-in person without pass_id.
422 errorWhen
email is required
mode must be at_period_end or immediate
No pass with that pass_id on your account
One bulk act at a time — the last one hasn't finished.A Studio bulk act is still running and this person holds a paid membership.
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
404{"error":"not found"}The email is not on your list. An account with nothing to do with you is a 404 too.
429{"error":"rate limited"}Over 120 requests in a minute.

POST/api/v1/people/blockBlock one person

curl https://backstagepass.com/api/v1/people/block \
  -H "Authorization: Bearer $BSP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com"
}'

200: the person object, with blocked: true. Idempotent.

A block is the Followers tab's Block. They cannot enter a live (open ones included), watch an encore or buy. Nothing they paid for is cancelled or refunded, so unblocking brings every door back as it was. The block is recorded as made "from the API".

Example response
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": true,
  "access": "Blocked",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "grant",
      "until": "2026-12-31T23:59:00Z",
      "active": true
    }
  ]
}
422 errorWhen
not signed in yet — withdraw the invite insteadSomebody you invited who has no account yet.
email is required

A block belongs to an account, and Studio's Invited row has no Block either, so there is nothing to block until they sign in; withdraw the invite to stop them becoming a follower. An Invited person who already has an account (they signed in before you added them) can be blocked.

StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
404{"error":"not found"}The email is not on your list. An account with nothing to do with you is a 404 too.
429{"error":"rate limited"}Over 120 requests in a minute.

POST/api/v1/people/unblockUnblock one person

curl https://backstagepass.com/api/v1/people/unblock \
  -H "Authorization: Bearer $BSP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com"
}'

200: the person object, with blocked: false. Idempotent. Every door comes back as it was.

Example response
{
  "status": "signed_in",
  "id": 4812,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "phone": "+14155550132",
  "follower": true,
  "invited": false,
  "revoked_at": null,
  "blocked": false,
  "access": "1p",
  "source": "API",
  "passes": [
    {
      "pass_id": "pass_3f9a0c1e7b2d4a68",
      "name": "Front Row",
      "kind": "grant",
      "until": "2026-12-31T23:59:00Z",
      "active": true
    }
  ]
}
422 errorWhen
not signed in yet — withdraw the invite insteadSomebody you invited who has no account yet.
email is required
StatusBodyWhen
401{"error":"unauthorized"}No key, unknown key, or disabled key.
404{"error":"not found"}The email is not on your list. An account with nothing to do with you is a 404 too.
429{"error":"rate limited"}Over 120 requests in a minute.

Webhooks

  1. You add a URL and pick the moments it should hear.
  2. When a moment happens, BackstagePass sends one POST with a JSON body to every URL subscribed to it.
  3. Your server checks the signature and answers 2xx.

A webhook fires whether or not that moment’s fan notification is on in Automations. The one exception is gone_quiet, which only exists while the Became inactive automation is on. Breakout and after-party rooms never send.

Add an endpoint

  1. Open Studio → Automations → APIs & Webhooks.
  2. On Webhooks, paste the URL.
  3. Every moment starts ticked. Untick the ones you don't need.
  4. Press Add the webhook.
URL rules
https://, a real host, no username or password in the URL, up to 2048 characters, public address (private and local networks are refused).
Moments
At least one.
Duplicates
One row per URL per account.
Secret
40 letters and digits, made when the endpoint is added. It cannot be changed; delete and re-add the URL for a new one.
Row buttons
Send a ping, Turn back on (when paused), Delete.
Who can
The owner and teammates with studio access.
By API
Not available. Endpoints are managed in Studio only.

A refused URL answers That doesn’t look like a URL we can POST to. No moment ticked answers Pick at least one moment.

Events

The picker offers these, in this order. Fan events carry the one person the moment is about.

Wire nameCard labelFires whenFanObjectsHow often
welcomeFollow confirmationSomeone follows you.FanfanOnce per new follow.
seat_reservedLive scheduledYou book a live for a future time.NonestreamOnce per live.
going_liveGoing liveYou go live (GO).NonestreamOnce per live.
reminder_15m15 minutes beforeA scheduled live is 15 minutes out.NonestreamOnce per live.
reminder_60m60 minutes beforeA scheduled live is one hour out.NonestreamOnce per live.
reminder_1d1 day beforeA scheduled live is one day out.NonestreamOnce per live.
live_rescheduledLive rescheduledYou move a scheduled live to a new time.NonestreamOnce per move.
live_canceledLive cancelledYou cancel a scheduled live.NonestreamOnce per live.
counted_inSeat reservedOne fan counts themselves in (RSVP) to a live.Fanfan, streamOnce per RSVP.
encore_publishedEncore publishedA live's replay is ready to watch.NonestreamNot deduplicated.
new_passNew pass createdA Pass goes on sale (listed).NonepassOnce per Pass.
win_backPass expiredA fan's Pass ends after a cancel (not a pause).FanfanEach time it happens.
joined_backstageJoined backstageA fan buys a Pass.FanfanOnce per purchase.
ticket_boughtBought a ticketA fan gets a ticket to a live.Fanfan, streamOnce per ticket.
gone_quietBecame inactiveDaily sweep: a fan's last activity crossed 60 days. Only while the Became inactive automation is on.FanfanAt most once per fan per 90 days.
new_transactionNew transactionA fan pays you: Pass charge, ticket, tip or drop.Fanfan, transactionOnce per payment.

Two names read crossed: seat_reserved is the Live scheduled moment (you booked a live), and counted_in is the Seat reserved moment (one fan RSVP’d). Match on the wire name.

Payloads

Every body has event, occurred_at (UTC) and creator.handle. Then, depending on the event:

FieldTypeMeaning
fanobjectid (integer), name (string), email (string|null), phone (string|null, E.164). All four keys always present.
streamobjecttitle, slug, status (scheduled|live|ended|canceled), started_at, ended_at, room_url.
stream.replay_urlstringreplay.ready only. Same address as room_url.
passobjectid, name.
transactionobjectid, kind (subscription|ticket|tip|drop), amount_cents, amount_after_fees_cents (null = not reported, never zero), occurred_at.

Key order in the body is not meaningful. A scheduled live’s start time is not in the payload.

welcome · Follow confirmation

Someone follows you. Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "welcome",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
seat_reserved · Live scheduled

You book a live for a future time. Carries no fan.

{
  "event": "seat_reserved",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
going_live · Going live

You go live (GO). Carries no fan.

{
  "event": "going_live",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "live",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
reminder_15m · 15 minutes before

A scheduled live is 15 minutes out. Carries no fan.

{
  "event": "reminder_15m",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
reminder_60m · 60 minutes before

A scheduled live is one hour out. Carries no fan.

{
  "event": "reminder_60m",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
reminder_1d · 1 day before

A scheduled live is one day out. Carries no fan.

{
  "event": "reminder_1d",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
live_rescheduled · Live rescheduled

You move a scheduled live to a new time. Carries no fan.

{
  "event": "live_rescheduled",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
live_canceled · Live cancelled

You cancel a scheduled live. Carries no fan.

{
  "event": "live_canceled",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "canceled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
counted_in · Seat reserved

One fan counts themselves in (RSVP) to a live. Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "counted_in",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
encore_published · Encore published

A live's replay is ready to watch. Carries no fan.

{
  "event": "encore_published",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "ended",
    "ended_at": "2026-10-24T02:10:00Z",
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
new_pass · New pass created

A Pass goes on sale (listed). Carries no fan.

{
  "pass": {
    "id": 312,
    "name": "Gold"
  },
  "event": "new_pass",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
win_back · Pass expired

A fan's Pass ends after a cancel (not a pause). Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "win_back",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
joined_backstage · Joined backstage

A fan buys a Pass. Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "joined_backstage",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
ticket_bought · Bought a ticket

A fan gets a ticket to a live. Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "ticket_bought",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "scheduled",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": null
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
gone_quiet · Became inactive

Daily sweep: a fan's last activity crossed 60 days. Only while the Became inactive automation is on. Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "gone_quiet",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
new_transaction · New transaction

A fan pays you: Pass charge, ticket, tip or drop. Carries the fan.

{
  "fan": {
    "id": 4821,
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+15551234567"
  },
  "event": "new_transaction",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z",
  "transaction": {
    "id": 90211,
    "kind": "ticket",
    "occurred_at": "2026-10-24T00:58:41Z",
    "amount_cents": 2700,
    "amount_after_fees_cents": null
  }
}
stream.live · Went live (legacy)

At GO, beside going_live.

{
  "event": "stream.live",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "live",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
stream.cut · Went backstage (legacy)

Every social feed is cut (CUT THE SOCIALS).

{
  "event": "stream.cut",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "live",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
stream.restored · Feeds restored (legacy)

The social feeds come back after a cut.

{
  "event": "stream.restored",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "live",
    "ended_at": null,
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
stream.ended · Show ended (legacy)

The live ends (END). Also fires for a live that was taken down.

{
  "event": "stream.ended",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "ended",
    "ended_at": "2026-10-24T02:10:00Z",
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
replay.ready · Replay ready (legacy)

With encore_published.

{
  "event": "replay.ready",
  "stream": {
    "slug": "friday-soundcheck",
    "title": "Friday soundcheck",
    "status": "ended",
    "ended_at": "2026-10-24T02:10:00Z",
    "room_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "replay_url": "https://backstagepass.com/maya/s/friday-soundcheck",
    "started_at": "2026-10-24T01:00:00Z"
  },
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}
ping · Send a ping

Sent when you press Send a ping on the card. Every endpoint can receive it; it is not on the picker.

{
  "event": "ping",
  "creator": {
    "handle": "maya"
  },
  "occurred_at": "2026-10-24T01:00:03Z"
}

Verify the signature

HeaderTypeMeaning
Content-Typestringapplication/json
X-Backstagepass-EventstringThe event wire name, same as the body's event.
X-Backstagepass-DeliverystringThis delivery's id. The same on every retry. Use it to deduplicate.
X-Backstagepass-Signaturestringt=<unix seconds>,v1=<hex HMAC-SHA256>
  1. Read the raw request body as bytes. Do not parse and re-serialize it.
  2. Split the header on , and = to get t and v1.
  3. Compute HMAC-SHA256(secret, t + "." + raw_body) as lowercase hex, with the signing secret from the row.
  4. Compare it to v1 in constant time.
  5. Recommended: reject a t more than a few minutes from your clock. Each retry is signed fresh, so a retry passes.

Worked example

Secret
Xk3n9QpL2vR7tY1wZ8bC4dF6gH0jM5sA2eU7iO9q
t
1792803605
Raw body
{"event":"ping","creator":{"handle":"maya"},"occurred_at":"2026-10-24T01:00:03Z"}
Signed string
1792803605.{"event":"ping","creator":{"handle":"maya"},"occurred_at":"2026-10-24T01:00:03Z"}
Header
t=1792803605,v1=1e4bdaf501c8215c92763979b19eddeae854819028cda18cc826631964440a04

Node

import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.BSP_WEBHOOK_SECRET; // from the card
const TOLERANCE_SECONDS = 300;                 // your choice

function verify(rawBody, header) {
  const parts = Object.fromEntries(
    String(header || "").split(",").map((kv) => kv.split("=", 2))
  );
  const t = Number(parts.t);
  const v1 = parts.v1 || "";
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();
// Keep the raw bytes: the signature is over the body exactly as sent.
app.post("/hooks/backstagepass", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("X-Backstagepass-Signature"))) return res.sendStatus(400);
  const deliveryId = req.get("X-Backstagepass-Delivery");
  // if (alreadyProcessed(deliveryId)) return res.sendStatus(200);
  res.sendStatus(200);                      // answer fast, work after
  const event = JSON.parse(raw);
  // queue(event, deliveryId);
});

app.listen(3000);

Ruby

require "openssl"
require "json"
require "rack/utils"

SECRET = ENV.fetch("BSP_WEBHOOK_SECRET") # from the card
TOLERANCE_SECONDS = 300                  # your choice

def verify(raw_body, header)
  parts = header.to_s.split(",").to_h { |kv| kv.split("=", 2) }
  t = parts["t"].to_i
  v1 = parts["v1"].to_s
  return false if t.zero? || v1.empty?
  return false if (Time.now.to_i - t).abs > TOLERANCE_SECONDS
  expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{t}.#{raw_body}")
  Rack::Utils.secure_compare(expected, v1)
end

# Rails controller
class BackstagepassHooksController < ActionController::API
  def create
    raw = request.raw_post # the bytes as sent, never params.to_json
    return head :bad_request unless verify(raw, request.headers["X-Backstagepass-Signature"])
    delivery_id = request.headers["X-Backstagepass-Delivery"]
    # return head :ok if already_processed?(delivery_id)
    event = JSON.parse(raw)
    # HandleBackstagepassEventJob.perform_later(event, delivery_id)
    head :ok
  end
end

Python

import hashlib
import hmac
import json
import os
import time

from flask import Flask, request

SECRET = os.environ["BSP_WEBHOOK_SECRET"]  # from the card
TOLERANCE_SECONDS = 300                    # your choice

def verify(raw_body: bytes, header: str) -> bool:
    try:
        parts = dict(kv.split("=", 1) for kv in (header or "").split(","))
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(SECRET.encode(), f"{t}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

app = Flask(__name__)

@app.post("/hooks/backstagepass")
def hook():
    raw = request.get_data()  # the bytes as sent
    if not verify(raw, request.headers.get("X-Backstagepass-Signature", "")):
        return "", 400
    delivery_id = request.headers.get("X-Backstagepass-Delivery")
    # if already_processed(delivery_id): return "", 200
    event = json.loads(raw)
    # enqueue(event, delivery_id)
    return "", 200

Delivery and retries

Success2xxAny 2xx status marks the delivery delivered. The response body is ignored.
Timeouts5s / 10s5 seconds to connect, 10 seconds to read the response. Answer first, then do the work.
Redirectsnot followedA 3xx counts as a failed attempt. Register the final URL.
Retries4 attemptsFirst try, then 30 seconds, 5 minutes and 30 minutes later. Then the delivery is marked failed.
Pause8 in a row8 deliveries in a row that fail all their attempts pause the endpoint. The card says so.
While pausedNew moments are not queued for it and are not replayed later. Send a ping still works. Turn back on resumes it.
OrderingnoneDeliveries are independent; a retry can arrive after a later event. Use occurred_at to order.
DuplicatespossibleDelivery is at least once. Deduplicate on X-Backstagepass-Delivery.

Some moments arrive twice by design, as two different events: going_live and stream.live at GO, encore_published and replay.ready when the replay is ready. Subscribe to one of each pair.

Legacy events

These five were the first webhooks. They are off the picker but still delivered to any endpoint that already holds them.

Wire nameLabelFires whenHow often
stream.liveWent liveAt GO, beside going_live.Once per live.
stream.cutWent backstageEvery social feed is cut (CUT THE SOCIALS).Each cut.
stream.restoredFeeds restoredThe social feeds come back after a cut.Each restore.
stream.endedShow endedThe live ends (END). Also fires for a live that was taken down.Once per live.
replay.readyReplay readyWith encore_published.Once per live.

Testing

  1. Get a public HTTPS URL: a request bin such as webhook.site, or a tunnel to your machine such as ngrok http 3000.
  2. Add it on the Webhooks card.
  3. Press Send a ping. A signed ping arrives. The row's delivered count moves when you answer 2xx.
  4. Real events fire the real thing: booking a live notifies your followers. A follow from a second account is a harmless welcome.

There is no sandbox account and no way to replay a past delivery.

Troubleshooting

SymptomCheck
Nothing arrivesIs the moment ticked on the row? Is the row on, not paused? Is the URL public HTTPS? Lives inside a live never send.
Signature never matchesYou are hashing parsed JSON. Hash the raw bytes, and use the secret from that endpoint's row.
Failed count risingYour server answered non-2xx, redirected, or took more than 10 seconds. Answer 200 first.
Paused after 8 straight failuresFix the receiver, press Send a ping to check, then Turn back on.
Duplicate eventsExpected at least once. Deduplicate on X-Backstagepass-Delivery.
No gone_quiet eventsIt only runs while the Became inactive automation is on for at least one rail.
API returns 401The key was replaced or disabled. Generate a new one.
API returns 429Over 120 requests a minute. Wait 60 seconds.

Embed on your website

On the same tab: one iframe that shows your room, your upcoming live or your latest encore, and stays current on its own. Copy it with Copy embed code.

<iframe src="https://backstagepass.com/embed/<handle>"
  width="560" height="315" style="border:0; border-radius:12px;" allow="autoplay"></iframe>
For AI agents

Integrate as an agent

Numbered steps. Each has the exact request, the expected answer, and what to do next.

The prompt at the top of this page points an agent here.

Steps

  1. Read the machine files

    Request

    GET https://backstagepass.com/developers/llms-full.txt

    Expect

    200, text/plain: this whole reference as markdown.

    Then

    AlwaysUse https://backstagepass.com/developers/openapi.json for request shapes and https://backstagepass.com/developers/webhooks.schema.json to validate webhook bodies.
  2. Get the API key from the human

    Say to the human

    Please open Studio > Automations > APIs & Webhooks, press Generate new key on API access, and paste the key here. Only the account owner can do this. If a key already exists, pressing it again replaces it.

    Expect

    40 letters and digits.

    Then

    Anything elseAsk again. Do not continue without a key.
  3. Prove the key

    Request

    GET https://backstagepass.com/api/v1/me
    Authorization: Bearer <key>

    Expect

    200
    {
      "handle": "maya",
      "display_name": "Maya Rivers",
      "plan": "free"
    }

    Then

    200Tell the human the handle. Continue.
    401 {"error":"unauthorized"}Stop. Ask the human for a new key (step 2). Do not retry with the same key.
    429 {"error":"rate limited"}Wait 60 seconds. Try once more.
    Anything elseStop. Report the status and body to the human.
  4. Read what you need

    Request

    GET https://backstagepass.com/api/v1/audience
    GET https://backstagepass.com/api/v1/streams
    GET https://backstagepass.com/api/v1/audience/export

    Expect

    200 JSON counts / 200 JSON {"streams": [...], "generated_at": ...} / 200 text/csv

    Then

    CSVStrip the leading byte order mark. It holds names, emails and phones: treat it as personal data.
    streamsAt most the 50 newest ended lives. Older lives cannot be read.
    401 / 429 / otherSame as step 3.
    PaceStay under 120 requests a minute.
  5. Ask the human to add your webhook

    Say to the human

    Please open Studio > Automations > APIs & Webhooks. On Webhooks, paste <your HTTPS URL>, leave only these moments ticked: <list>, press Add the webhook, and paste me the signing secret shown on that row.

    Expect

    A 40-character signing secret.

    Then

    Refused URLThe card says "That doesn't look like a URL we can POST to." Give a public https:// URL with no username or password.
    No secretWait. You cannot verify anything without it.
  6. Receive each POST

    Request

    POST <your URL>
    Content-Type: application/json
    X-Backstagepass-Event: <event>
    X-Backstagepass-Delivery: <id>
    X-Backstagepass-Signature: t=<unix>,v1=<hex>
    
    <raw JSON body>

    Expect

    hex(HMAC-SHA256(secret, "<t>.<raw body>")) equals v1.

    Then

    Signature mismatchAnswer 400. Do not act on the body. Stop and report to the human: the secret may be from another row.
    Signature matchesAnswer 200 within 10 seconds, then do the work.
    Delivery id seen beforeAnswer 200. Do nothing else.
    event is pingAnswer 200. Nothing to do.
    event you do not knowAnswer 200 and ignore it. Re-read the schema later.
  7. Prove it end to end

    Say to the human

    Please press Send a ping on that row.

    Expect

    One POST, event "ping", body {"event":"ping","creator":{"handle":...},"occurred_at":...}, valid signature.

    Then

    Nothing within a minuteAsk the human whether the row says paused, and whether the URL is public HTTPS.
    Row's failed count went upYour receiver answered non-2xx, redirected, or took over 10 seconds.
  8. Find a person

    Request

    GET https://backstagepass.com/api/v1/people?email=<email>
    Authorization: Bearer <key>

    Expect

    200 the person object

    Then

    "status": "invited"Added, not signed in yet. They hold nothing; queued_passes is what they get when they sign in. Nothing to block yet.
    "status": "signed_in"passes is what they hold now. blocked and revoked_at are as the Followers page shows them.
    404 {"error":"not found"}Not on the list. To add them, go to step 9.
    401 / 429 / otherSame as step 3.
  9. Add a person

    Say to the human

    Add <email> to <Pass name>, until <date or no end>? They stay Invited until they sign in, and get no email from you before that unless you want the welcome letter sent. Do you confirm they opted in to hear from you by email?

    Request

    POST https://backstagepass.com/api/v1/people
    Authorization: Bearer <key>
    Content-Type: application/json
    
    {"email": "<email>", "pass_id": "<pass_id>", "expires_at": "<ISO 8601, optional>", "welcome": <true|false>, "attested": true}

    Expect

    201 or 200, the person object

    Then

    No yes on opt-inStop. Send attested: true only when the human confirms it.
    No pass_idGET /api/v1/passes. Ask the human which Pass.
    201They are Invited. Tell the human the Pass is theirs when they sign in with that email.
    200 "status": "invited"Already Invited. New Passes joined the queue; a queued Pass's until is now expires_at.
    200 "status": "signed_in"Already a follower. The Pass is granted now.
    422Stop. Show the human the error sentence. Do not retry until it is fixed.
  10. Revoke or block

    Say to the human

    Take <Pass name, or everything> from <email>? A paid membership stops renewing and runs to the end of its paid term, unless you want it closed now. Nothing is refunded.

    Request

    POST https://backstagepass.com/api/v1/people/revoke
    Authorization: Bearer <key>
    Content-Type: application/json
    
    {"email": "<email>", "pass_id": "<pass_id, or leave out for everything>", "mode": "at_period_end"}

    Expect

    200 the person object, plus revoked and skipped

    Then

    revokedTell the human each line: what ended, and until when.
    skippedTell the human the reason. lifetime seat — nothing renews: ask whether to send mode: "immediate".
    "status": "withdrawn"The invite is gone. A GET for them is now 404.
    To stop them coming backPOST /api/v1/people/block with {"email": "<email>"}. Undo with POST /api/v1/people/unblock.
    422 "not signed in yet — withdraw the invite instead"Revoke without pass_id instead.
    422 "One bulk act at a time — the last one hasn't finished."Wait 60 seconds. Try once more.
    404 {"error":"not found"}Not on the list. Nothing to revoke.

What an agent cannot do today

An agent cannotToday
Add, list, ping or delete a webhookStudio only. A human does it on the Webhooks card.
Change a webhook's signing secretNot possible anywhere. Delete the row and add it again.
Create, replace or disable the API keyStudio only, account owner only.
Read a delivery log or resend a deliveryNot available. The row shows delivered and failed counts only.
Write anything but the People callsThe People calls are the only writes. Nothing can send, charge or change the account.
Refund a chargeStudio only: Transactions, one charge at a time.
Block someone who has not signed inNot possible. Withdraw the invite instead: revoke without pass_id.
Read lives older than the newest 50Not available. /api/v1/streams has no paging.

Machine files

FileWhat
https://backstagepass.com/developers/llms.txtIndex for language models (llmstxt.org).
https://backstagepass.com/developers/llms-full.txtThis whole reference as markdown.
https://backstagepass.com/developers/openapi.jsonOpenAPI 3.1 for every endpoint.
https://backstagepass.com/developers/webhooks.schema.jsonJSON Schema 2020-12 for every webhook body.
OpenAPI 3.1, inline
{
  "openapi": "3.1.0",
  "info": {
    "title": "BackstagePass API",
    "version": "1.0.0",
    "summary": "Access to one creator's own BackstagePass data, and the Followers tab's verbs for one person.",
    "description": "One key per creator account, scoped to that creator's own data. The People endpoints are the only writes: each does what Studio's Followers page does for one person, keyed by email. A Pass is named by its public id (pass_ plus 16 lowercase letters and digits); the integer id never appears. Responses are bare JSON objects (no envelope), except /api/v1/audience/export, which returns CSV. Generated from app/controllers/api/v1/*.rb."
  },
  "externalDocs": {
    "description": "Guides, webhook events and the agent steps",
    "url": "https://backstagepass.com/help/how/automations"
  },
  "servers": [
    {
      "url": "https://backstagepass.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "The creator the key belongs to."
    },
    {
      "name": "Audience",
      "description": "Your followers and imported contacts."
    },
    {
      "name": "Streams",
      "description": "Your ended lives and the notifications sent for them."
    },
    {
      "name": "Passes",
      "description": "Your Passes, by their public id."
    },
    {
      "name": "People",
      "description": "One person on your list, keyed by email: add, find, revoke, block and unblock."
    }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Your creator profile",
        "description": "The creator the key belongs to.",
        "responses": {
          "200": {
            "description": "The creator profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "examples": {
                  "default": {
                    "value": {
                      "handle": "maya",
                      "display_name": "Maya Rivers",
                      "plan": "free"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/audience": {
      "get": {
        "operationId": "getAudience",
        "tags": [
          "Audience"
        ],
        "summary": "Your audience list, as counts",
        "description": "Aggregate counts only, no people. The reachable counts come from the same eligibility query the Followers page, the composer preview and the receipt use.",
        "responses": {
          "200": {
            "description": "Audience counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Audience"
                },
                "examples": {
                  "default": {
                    "value": {
                      "fans": 1240,
                      "push_reachable": 610,
                      "email_reachable": 980,
                      "imported_awaiting_first_sign_in": 75,
                      "text_reachable": 0,
                      "imported": 300,
                      "imported_claimed": 120,
                      "generated_at": "2026-10-27T18:04:05Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/audience/export": {
      "get": {
        "operationId": "exportAudience",
        "tags": [
          "Audience"
        ],
        "summary": "The same CSV as Export on Followers",
        "description": "One row per person: every follower, then unclaimed imported contacts. Byte-identical to the Export button on the Followers page. The body starts with a UTF-8 byte order mark. Streamed: no Content-Length and no Content-Disposition header.",
        "responses": {
          "200": {
            "description": "CSV file. Header row = the columns listed in the schema description.",
            "content": {
              "text/csv; charset=utf-8": {
                "schema": {
                  "type": "string",
                  "description": "Columns, in order: name, email, phone, active_access, expired_access, active_names, expired_names, lifetime_spend, spend_passes, spend_tickets, spend_drops, eligible_lives, showed_up, showed_up_%, encores_started, encore_started_%, total_watch_time, live_minutes, encore_minutes, reachable_push, reachable_email, reachable_sms, sms_consent, email_sent, email_opened, email_open_rate, push_sent, push_delivered, push_open_rate, last_attended, lives_ago, last_sign_in, joined, source, blocked, tags, notes"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/streams": {
      "get": {
        "operationId": "listStreams",
        "tags": [
          "Streams"
        ],
        "summary": "Your ended lives and their receipts",
        "description": "Your 50 most recently ended lives, newest first. A live that was taken down is left out. No fan appears in this payload. No pagination and no filters.",
        "responses": {
          "200": {
            "description": "Ended lives.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamList"
                },
                "examples": {
                  "default": {
                    "value": {
                      "streams": [
                        {
                          "title": "Friday soundcheck",
                          "slug": "friday-soundcheck",
                          "started_at": "2026-10-24T01:00:00Z",
                          "ended_at": "2026-10-24T02:10:00Z",
                          "peak_in_room": 99,
                          "hearts": 412,
                          "cut_retention": {
                            "before": 90,
                            "after": 72,
                            "pct": 80
                          },
                          "broadcasts": [
                            {
                              "trigger": "go_live",
                              "sent_at": "2026-10-24T01:00:12Z",
                              "receipt": {
                                "push": {
                                  "sent": 1,
                                  "delivered": 0,
                                  "opened": 0,
                                  "no_transport": 0,
                                  "suppressed_quiet_hours": 0,
                                  "suppressed_never_activated": 0,
                                  "suppressed_no_consent": 0,
                                  "suppressed_muted": 0,
                                  "suppressed_signal_tier": 0,
                                  "suppressed_digest": 0,
                                  "suppressed_plan_cap": 0,
                                  "suppressed_announcement_type": 0,
                                  "suppressed_frequency_cap": 0,
                                  "suppressed_push_primary": 0,
                                  "failed": 0
                                }
                              },
                              "walked_in_10m": {
                                "push": 1
                              }
                            }
                          ]
                        }
                      ],
                      "generated_at": "2026-10-27T18:04:05Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/passes": {
      "get": {
        "operationId": "listPasses",
        "tags": [
          "Passes"
        ],
        "summary": "Every Pass you have, with its public id",
        "description": "Every Pass on your account, oldest first, with the shelf's state word and every price it is sold at.",
        "responses": {
          "200": {
            "description": "Your Passes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PassList"
                },
                "examples": {
                  "default": {
                    "value": {
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "status": "published",
                          "prices": [
                            {
                              "cadence": "monthly",
                              "price_cents": 900
                            },
                            {
                              "cadence": "yearly",
                              "price_cents": 9000
                            }
                          ]
                        }
                      ],
                      "generated_at": "2026-10-29T12:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/people": {
      "get": {
        "operationId": "getPerson",
        "tags": [
          "People"
        ],
        "summary": "One person on your list, by email",
        "description": "An Invited person or a signed-in one. A withdrawn invite is a 404.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "examples": {
                  "signed_in": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": true,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "1p",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "grant",
                          "until": "2026-12-31T23:59:00Z",
                          "active": true
                        }
                      ]
                    }
                  },
                  "invited": {
                    "value": {
                      "status": "invited",
                      "id": null,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": null,
                      "follower": false,
                      "invited": true,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "Invited",
                      "source": "API",
                      "passes": [],
                      "invited_at": "2026-10-29T12:00:00Z",
                      "welcome_sent": true,
                      "queued_passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "until": "2026-12-31T23:59:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "addPerson",
        "tags": [
          "People"
        ],
        "summary": "Add one person, as the Add people sheet on Studio's Followers page does",
        "description": "Adds them as Invited: no account and no follow are created, the Passes are queued on their invite, and the only email they can get before signing in is the welcome letter, when welcome is true (once ever per person). When they sign in they become a follower and every queued Pass is granted, carrying its until. Idempotent on email. Already a follower: the Pass is granted now, and a Pass they hold has its end set to expires_at. 422 when attested is not true, email is missing or unreadable, the phone cannot be read, the pass_id is unknown, archived or another creator's, expires_at has no Pass, is not ISO 8601 or is in the past, or the email is your own account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "attested"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "attested": {
                    "type": "boolean",
                    "const": true,
                    "description": "You confirm this person consented to be added to your audience and to get email from you. Anything but true adds nobody."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "If the person already has an account, their own name wins."
                  },
                  "phone": {
                    "type": "string",
                    "description": "Digits with country_code, or E.164 on its own. Stored on their contact row, never a sign-in."
                  },
                  "country_code": {
                    "type": "string",
                    "description": "US, GB or +44: reads phone, as the sheet's country picker does."
                  },
                  "pass_id": {
                    "type": "string",
                    "pattern": "^pass_[a-z0-9]{16}$",
                    "description": "Queues this Pass for them."
                  },
                  "pass_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "pattern": "^pass_[a-z0-9]{16}$",
                      "description": "The Pass's public id. Studio's Passes page prints it with a Copy button."
                    },
                    "description": "Several Passes at once. May be sent beside pass_id."
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "In the future. Requires a Pass. The end of every Pass in this request. Leave it out for a grant with no end."
                  },
                  "welcome": {
                    "type": "boolean",
                    "default": false,
                    "description": "true sends the welcome letter, once ever per person."
                  }
                }
              },
              "examples": {
                "pass_with_expiry_welcome_on": {
                  "value": {
                    "email": "ada@example.com",
                    "name": "Ada Lovelace",
                    "pass_id": "pass_3f9a0c1e7b2d4a68",
                    "expires_at": "2026-12-31T23:59:00Z",
                    "welcome": true,
                    "attested": true
                  }
                },
                "welcome_off": {
                  "value": {
                    "email": "ada@example.com",
                    "pass_id": "pass_3f9a0c1e7b2d4a68",
                    "attested": true,
                    "welcome": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "They were not on your list and are now Invited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "examples": {
                  "welcome_on": {
                    "value": {
                      "status": "invited",
                      "id": null,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": null,
                      "follower": false,
                      "invited": true,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "Invited",
                      "source": "API",
                      "passes": [],
                      "invited_at": "2026-10-29T12:00:00Z",
                      "welcome_sent": true,
                      "queued_passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "until": "2026-12-31T23:59:00Z"
                        }
                      ]
                    }
                  },
                  "welcome_off": {
                    "value": {
                      "status": "invited",
                      "id": null,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": null,
                      "follower": false,
                      "invited": true,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "Invited",
                      "source": "API",
                      "passes": [],
                      "invited_at": "2026-10-29T12:00:00Z",
                      "welcome_sent": false,
                      "queued_passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "until": "2026-12-31T23:59:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "They were already on your list (Invited, or a follower).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "examples": {
                  "already_a_follower": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": true,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "1p",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "grant",
                          "until": "2026-12-31T23:59:00Z",
                          "active": true
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The request cannot be done as sent. The error is one of these sentences.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "e1": {
                    "summary": "attested was not true.",
                    "value": {
                      "error": "Adding people needs your word first \u2014 confirm everyone here opted in to hear from you by email. Nobody was added."
                    }
                  },
                  "e2": {
                    "summary": "email is required",
                    "value": {
                      "error": "email is required"
                    }
                  },
                  "e3": {
                    "summary": "That email address cannot be read",
                    "value": {
                      "error": "That email address cannot be read"
                    }
                  },
                  "e4": {
                    "summary": "That phone number cannot be read \u2014 send it with its country code, like +14155550132",
                    "value": {
                      "error": "That phone number cannot be read \u2014 send it with its country code, like +14155550132"
                    }
                  },
                  "e5": {
                    "summary": "An unknown id, another creator's Pass, or an archived Pass.",
                    "value": {
                      "error": "No pass with that pass_id on your account"
                    }
                  },
                  "e6": {
                    "summary": "expires_at needs a pass_id",
                    "value": {
                      "error": "expires_at needs a pass_id"
                    }
                  },
                  "e7": {
                    "summary": "expires_at must be an ISO 8601 time, like 2026-12-31T23:59:00Z",
                    "value": {
                      "error": "expires_at must be an ISO 8601 time, like 2026-12-31T23:59:00Z"
                    }
                  },
                  "e8": {
                    "summary": "expires_at is in the past",
                    "value": {
                      "error": "expires_at is in the past"
                    }
                  },
                  "e9": {
                    "summary": "That is your own account",
                    "value": {
                      "error": "That is your own account"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/people/revoke": {
      "post": {
        "operationId": "revokePerson",
        "tags": [
          "People"
        ],
        "summary": "Revoke one Pass, or everything",
        "description": "Invited person: with pass_id, that Pass comes off their queue; without, the invite is withdrawn (status withdrawn, and a GET is then a 404). Signed-in person: with pass_id, a grant ends now and a membership is cancelled at period end (at_period_end) or revoked now (immediate); without pass_id, this is the Followers tab's Revoke for this one person, and the follow row stays stamped Revoked. Nothing is refunded. 422 for a bad mode, an unknown pass_id, or while a Studio bulk act is still running and this person holds a paid membership.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "pass_id": {
                    "type": "string",
                    "pattern": "^pass_[a-z0-9]{16}$",
                    "description": "Leave it out to revoke everything."
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "at_period_end",
                      "immediate"
                    ],
                    "default": "at_period_end",
                    "description": "Only applies to a paid membership on that Pass."
                  }
                }
              },
              "examples": {
                "one_pass_at_period_end": {
                  "value": {
                    "email": "ada@example.com",
                    "pass_id": "pass_3f9a0c1e7b2d4a68",
                    "mode": "at_period_end"
                  }
                },
                "one_pass_immediate": {
                  "value": {
                    "email": "ada@example.com",
                    "pass_id": "pass_3f9a0c1e7b2d4a68",
                    "mode": "immediate"
                  }
                },
                "everything": {
                  "value": {
                    "email": "ada@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person, plus what was revoked and what was skipped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokeResult"
                },
                "examples": {
                  "one_pass_at_period_end": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": true,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "1p",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "subscription",
                          "until": "2026-11-30T00:00:00Z",
                          "active": true
                        }
                      ],
                      "revoked": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "subscription",
                          "until": "2026-11-30T00:00:00Z"
                        }
                      ],
                      "skipped": []
                    }
                  },
                  "everything": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": false,
                      "invited": false,
                      "revoked_at": "2026-10-29T12:00:00Z",
                      "blocked": false,
                      "access": "Revoked",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "subscription",
                          "until": "2026-11-30T00:00:00Z",
                          "active": false
                        },
                        {
                          "pass_id": "pass_9b0e2f6c1a3d5e70",
                          "name": "Back Room",
                          "kind": "grant",
                          "until": "2026-10-29T12:00:00Z",
                          "active": false
                        }
                      ],
                      "revoked": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "subscription",
                          "until": "2026-11-30T00:00:00Z"
                        },
                        {
                          "pass_id": "pass_9b0e2f6c1a3d5e70",
                          "name": "Back Room",
                          "kind": "grant",
                          "until": "2026-10-29T12:00:00Z"
                        },
                        {
                          "name": "Tuesday coaching call",
                          "kind": "ticket"
                        },
                        {
                          "kind": "follow"
                        }
                      ],
                      "skipped": []
                    }
                  },
                  "invite_withdrawn": {
                    "value": {
                      "status": "withdrawn",
                      "id": null,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": null,
                      "follower": false,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": false,
                      "access": null,
                      "source": "API",
                      "passes": [],
                      "revoked": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "queued"
                        },
                        {
                          "kind": "invite"
                        }
                      ],
                      "skipped": []
                    }
                  },
                  "lifetime_seat_skipped": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": true,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "1p",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "subscription",
                          "until": null,
                          "active": true
                        }
                      ],
                      "revoked": [],
                      "skipped": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "subscription",
                          "reason": "lifetime seat \u2014 nothing renews"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The request cannot be done as sent. The error is one of these sentences.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "e1": {
                    "summary": "email is required",
                    "value": {
                      "error": "email is required"
                    }
                  },
                  "e2": {
                    "summary": "mode must be at_period_end or immediate",
                    "value": {
                      "error": "mode must be at_period_end or immediate"
                    }
                  },
                  "e3": {
                    "summary": "No pass with that pass_id on your account",
                    "value": {
                      "error": "No pass with that pass_id on your account"
                    }
                  },
                  "e4": {
                    "summary": "A Studio bulk act is still running and this person holds a paid membership.",
                    "value": {
                      "error": "One bulk act at a time \u2014 the last one hasn't finished."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/people/block": {
      "post": {
        "operationId": "blockPerson",
        "tags": [
          "People"
        ],
        "summary": "Block one person",
        "description": "The Followers tab's Block. They cannot enter a live (open ones included), watch an encore or buy. Nothing they paid for is cancelled or refunded. Idempotent. 422 for somebody you invited who has no account yet: withdraw the invite instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              },
              "examples": {
                "by_email": {
                  "value": {
                    "email": "ada@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person, blocked true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "examples": {
                  "default": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": true,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": true,
                      "access": "Blocked",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "grant",
                          "until": "2026-12-31T23:59:00Z",
                          "active": true
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Somebody you invited who has no account yet: withdraw the invite instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "e1": {
                    "summary": "Somebody you invited who has no account yet.",
                    "value": {
                      "error": "not signed in yet \u2014 withdraw the invite instead"
                    }
                  },
                  "e2": {
                    "summary": "email is required",
                    "value": {
                      "error": "email is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/people/unblock": {
      "post": {
        "operationId": "unblockPerson",
        "tags": [
          "People"
        ],
        "summary": "Unblock one person",
        "description": "Undoes a block. Every door comes back as it was. Idempotent. 422 for somebody you invited who has no account yet: withdraw the invite instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              },
              "examples": {
                "by_email": {
                  "value": {
                    "email": "ada@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person, blocked false.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "examples": {
                  "default": {
                    "value": {
                      "status": "signed_in",
                      "id": 4812,
                      "name": "Ada Lovelace",
                      "email": "ada@example.com",
                      "phone": "+14155550132",
                      "follower": true,
                      "invited": false,
                      "revoked_at": null,
                      "blocked": false,
                      "access": "1p",
                      "source": "API",
                      "passes": [
                        {
                          "pass_id": "pass_3f9a0c1e7b2d4a68",
                          "name": "Front Row",
                          "kind": "grant",
                          "until": "2026-12-31T23:59:00Z",
                          "active": true
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Somebody you invited who has no account yet: withdraw the invite instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "e1": {
                    "summary": "Somebody you invited who has no account yet.",
                    "value": {
                      "error": "not signed in yet \u2014 withdraw the invite instead"
                    }
                  },
                  "e2": {
                    "summary": "email is required",
                    "value": {
                      "error": "email is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer <api-key>. The key is generated by the account owner at Studio > Automations > APIs & Webhooks. 40 alphanumeric characters."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No key, an unknown key, or a disabled key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "unauthorized"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 120 requests in one minute with the same Authorization header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "rate limited"
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The email is not on your list (a withdrawn invite included). An account with nothing to do with you is a 404 too.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "not found"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Plain words. unauthorized (401), rate limited (429), not found (404), or the 422 sentence for the request."
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "handle",
          "display_name",
          "plan"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "description": "Your handle: backstagepass.com/<handle>."
          },
          "display_name": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "description": "Internal plan identifier."
          }
        }
      },
      "Audience": {
        "type": "object",
        "required": [
          "fans",
          "push_reachable",
          "email_reachable",
          "imported_awaiting_first_sign_in",
          "text_reachable",
          "imported",
          "imported_claimed",
          "generated_at"
        ],
        "properties": {
          "fans": {
            "type": "integer",
            "description": "People who follow you."
          },
          "push_reachable": {
            "type": "integer",
            "description": "Followers a push notification can reach now."
          },
          "email_reachable": {
            "type": "integer",
            "description": "Followers an email can reach now. Does not include imported contacts who have not signed in."
          },
          "imported_awaiting_first_sign_in": {
            "type": "integer",
            "description": "Imported contacts eligible for email who have not signed in yet."
          },
          "text_reachable": {
            "type": "integer",
            "description": "Followers a text message can reach."
          },
          "imported": {
            "type": "integer",
            "description": "Contacts you imported."
          },
          "imported_claimed": {
            "type": "integer",
            "description": "Imported contacts who have since signed in."
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "StreamList": {
        "type": "object",
        "required": [
          "streams",
          "generated_at"
        ],
        "properties": {
          "streams": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Stream"
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Stream": {
        "type": "object",
        "required": [
          "title",
          "slug",
          "started_at",
          "ended_at",
          "peak_in_room",
          "hearts",
          "cut_retention",
          "broadcasts"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "description": "The live's page: backstagepass.com/<handle>/s/<slug>."
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "peak_in_room": {
            "type": "integer",
            "description": "Most people in the room at once."
          },
          "hearts": {
            "type": "integer",
            "description": "Reactions sent between started_at and ended_at."
          },
          "cut_retention": {
            "description": "Who stayed after CUT THE SOCIALS: the room count at the cut and 10 to 14 minutes later. null when the live was never cut or the samples are missing.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "before",
                  "after",
                  "pct"
                ],
                "properties": {
                  "before": {
                    "type": "integer"
                  },
                  "after": {
                    "type": "integer"
                  },
                  "pct": {
                    "type": "integer",
                    "description": "after / before, as a rounded percentage."
                  }
                }
              }
            ]
          },
          "broadcasts": {
            "type": "array",
            "description": "Every notification blast sent for this live, oldest first.",
            "items": {
              "$ref": "#/components/schemas/Broadcast"
            }
          }
        }
      },
      "Broadcast": {
        "type": "object",
        "required": [
          "trigger",
          "sent_at",
          "receipt",
          "walked_in_10m"
        ],
        "properties": {
          "trigger": {
            "type": "string",
            "description": "What sent it.",
            "enum": [
              "go_live",
              "stream_reminder",
              "went_backstage",
              "clips_ready",
              "canceled",
              "welcome",
              "joined_backstage",
              "win_back",
              "waitlist_seat",
              "ticket_bought",
              "replay_watched",
              "push_granted",
              "gone_quiet",
              "stream_ended",
              "booking_reminder",
              "digest",
              "door_open",
              "show_booked",
              "new_pass",
              "rescheduled",
              "access_granted",
              "counted_in"
            ]
          },
          "sent_at": {
            "type": "string",
            "format": "date-time"
          },
          "receipt": {
            "type": "object",
            "description": "Delivery counts per channel. A channel appears only when at least one of its counts is above zero.",
            "propertyNames": {
              "enum": [
                "push",
                "email",
                "sms",
                "whatsapp"
              ]
            },
            "additionalProperties": {
              "$ref": "#/components/schemas/ChannelReceipt"
            }
          },
          "walked_in_10m": {
            "type": "object",
            "description": "Per channel: distinct people who entered the room within 10 minutes of this notification reaching them. A channel with nobody is left out.",
            "propertyNames": {
              "enum": [
                "push",
                "email",
                "sms",
                "whatsapp"
              ]
            },
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "ChannelReceipt": {
        "type": "object",
        "required": [
          "sent",
          "delivered",
          "opened",
          "no_transport",
          "suppressed_quiet_hours",
          "suppressed_never_activated",
          "suppressed_no_consent",
          "suppressed_muted",
          "suppressed_signal_tier",
          "suppressed_digest",
          "suppressed_plan_cap",
          "suppressed_announcement_type",
          "suppressed_frequency_cap",
          "suppressed_push_primary",
          "failed"
        ],
        "properties": {
          "sent": {
            "type": "integer",
            "description": "Rows marked sent or delivered."
          },
          "delivered": {
            "type": "integer"
          },
          "opened": {
            "type": "integer"
          },
          "no_transport": {
            "type": "integer"
          },
          "suppressed_quiet_hours": {
            "type": "integer"
          },
          "suppressed_never_activated": {
            "type": "integer"
          },
          "suppressed_no_consent": {
            "type": "integer"
          },
          "suppressed_muted": {
            "type": "integer"
          },
          "suppressed_signal_tier": {
            "type": "integer"
          },
          "suppressed_digest": {
            "type": "integer"
          },
          "suppressed_plan_cap": {
            "type": "integer"
          },
          "suppressed_announcement_type": {
            "type": "integer"
          },
          "suppressed_frequency_cap": {
            "type": "integer"
          },
          "suppressed_push_primary": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          }
        }
      },
      "Pass": {
        "type": "object",
        "required": [
          "pass_id",
          "name",
          "status",
          "prices"
        ],
        "properties": {
          "pass_id": {
            "type": "string",
            "pattern": "^pass_[a-z0-9]{16}$",
            "description": "The Pass's public id. Studio's Passes page prints it with a Copy button."
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "link_only",
              "archived",
              "draft"
            ],
            "description": "The word the Pass shelf uses."
          },
          "prices": {
            "type": "array",
            "description": "Every way the Pass is sold.",
            "items": {
              "type": "object",
              "required": [
                "cadence",
                "price_cents"
              ],
              "properties": {
                "cadence": {
                  "type": "string",
                  "enum": [
                    "monthly",
                    "quarterly",
                    "half_yearly",
                    "yearly",
                    "lifetime"
                  ]
                },
                "price_cents": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          }
        }
      },
      "PassList": {
        "type": "object",
        "required": [
          "passes",
          "generated_at"
        ],
        "properties": {
          "passes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pass"
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Person": {
        "type": "object",
        "required": [
          "status",
          "id",
          "name",
          "email",
          "phone",
          "follower",
          "invited",
          "revoked_at",
          "blocked",
          "access",
          "source",
          "passes"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "invited",
              "signed_in",
              "withdrawn"
            ],
            "description": "invited: added, and not signed in since. signed_in: has signed in and is on your list. withdrawn: only in the answer to the revoke that withdrew the invite."
          },
          "id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The person's account id. null for somebody who has never signed in."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "E.164."
          },
          "follower": {
            "type": "boolean",
            "description": "true when they follow you and the follow has not been revoked."
          },
          "invited": {
            "type": "boolean",
            "description": "true when status is invited."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When you revoked them (the follow row stays on your list)."
          },
          "blocked": {
            "type": "boolean",
            "description": "true when you have blocked them. A block closes every door."
          },
          "access": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Followers page's Access chip, word for word: Blocked, Revoked, a count such as 1p \u00b7 2t, Follower, or Invited. null for a withdrawn invite."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Followers page's Source column: API, Manual, Organic, or a tracking link's label."
          },
          "passes": {
            "type": "array",
            "description": "One entry per Pass of yours they hold or used to hold. Entries that have run out are included with active: false.",
            "items": {
              "type": "object",
              "required": [
                "pass_id",
                "name",
                "kind",
                "until",
                "active"
              ],
              "properties": {
                "pass_id": {
                  "type": "string",
                  "pattern": "^pass_[a-z0-9]{16}$",
                  "description": "The Pass's public id. Studio's Passes page prints it with a Copy button."
                },
                "name": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "grant",
                    "subscription"
                  ],
                  "description": "grant (you gave it) or subscription (they bought it)."
                },
                "until": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "When it ends: the grant's expiry, or the membership's cancel date. null means no end is scheduled."
                },
                "active": {
                  "type": "boolean"
                }
              }
            }
          },
          "invited_at": {
            "type": "string",
            "format": "date-time",
            "description": "Invited only. When you added them."
          },
          "welcome_sent": {
            "type": "boolean",
            "description": "Invited only. Whether the welcome letter has gone."
          },
          "queued_passes": {
            "type": "array",
            "description": "Invited only. The Passes waiting for them, each with the until its grant will carry when they sign in (null for no end).",
            "items": {
              "type": "object",
              "required": [
                "pass_id",
                "until"
              ],
              "properties": {
                "pass_id": {
                  "type": "string",
                  "pattern": "^pass_[a-z0-9]{16}$",
                  "description": "The Pass's public id. Studio's Passes page prints it with a Copy button."
                },
                "until": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "RevokeResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Person"
          },
          {
            "type": "object",
            "required": [
              "revoked",
              "skipped"
            ],
            "properties": {
              "revoked": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "kind"
                  ],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "invite",
                        "grant",
                        "subscription",
                        "ticket",
                        "follow"
                      ]
                    },
                    "pass_id": {
                      "type": "string",
                      "pattern": "^pass_[a-z0-9]{16}$",
                      "description": "The Pass's public id. Studio's Passes page prints it with a Copy button."
                    },
                    "name": {
                      "type": "string"
                    },
                    "until": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              },
              "skipped": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "reason"
                  ],
                  "properties": {
                    "pass_id": {
                      "type": "string",
                      "pattern": "^pass_[a-z0-9]{16}$",
                      "description": "The Pass's public id. Studio's Passes page prints it with a Copy button."
                    },
                    "name": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string",
                      "description": "The bulk tool's own words, such as already winding down, lifetime seat \u2014 nothing renews, not active, holds nothing on this pass."
                    }
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}
Webhook JSON Schema, inline
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://backstagepass.com/developers/webhooks.schema.json",
  "title": "BackstagePass webhook payload",
  "description": "The JSON body of every POST BackstagePass sends to a registered webhook URL. The event header X-Backstagepass-Event carries the same value as the body's event field. Draft for review (BSP-423); generated from app/services/webhooks/emit.rb.",
  "oneOf": [
    {
      "$ref": "#/$defs/event_ping"
    },
    {
      "$ref": "#/$defs/event_welcome"
    },
    {
      "$ref": "#/$defs/event_seat_reserved"
    },
    {
      "$ref": "#/$defs/event_going_live"
    },
    {
      "$ref": "#/$defs/event_reminder_15m"
    },
    {
      "$ref": "#/$defs/event_reminder_60m"
    },
    {
      "$ref": "#/$defs/event_reminder_1d"
    },
    {
      "$ref": "#/$defs/event_live_rescheduled"
    },
    {
      "$ref": "#/$defs/event_live_canceled"
    },
    {
      "$ref": "#/$defs/event_counted_in"
    },
    {
      "$ref": "#/$defs/event_encore_published"
    },
    {
      "$ref": "#/$defs/event_new_pass"
    },
    {
      "$ref": "#/$defs/event_win_back"
    },
    {
      "$ref": "#/$defs/event_joined_backstage"
    },
    {
      "$ref": "#/$defs/event_ticket_bought"
    },
    {
      "$ref": "#/$defs/event_gone_quiet"
    },
    {
      "$ref": "#/$defs/event_new_transaction"
    },
    {
      "$ref": "#/$defs/event_stream_live"
    },
    {
      "$ref": "#/$defs/event_stream_cut"
    },
    {
      "$ref": "#/$defs/event_stream_restored"
    },
    {
      "$ref": "#/$defs/event_stream_ended"
    },
    {
      "$ref": "#/$defs/event_replay_ready"
    }
  ],
  "$defs": {
    "creator": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "handle"
      ],
      "properties": {
        "handle": {
          "type": "string",
          "description": "The creator's handle: backstagepass.com/<handle>."
        }
      }
    },
    "fan": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "name",
        "email",
        "phone"
      ],
      "description": "The one person this moment is about. All four keys are always present; email and phone are null when the fan has none.",
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": "string"
        },
        "email": {
          "type": [
            "string",
            "null"
          ]
        },
        "phone": {
          "type": [
            "string",
            "null"
          ],
          "description": "E.164, e.g. +15551234567."
        }
      }
    },
    "stream": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "title",
        "slug",
        "status",
        "started_at",
        "ended_at",
        "room_url"
      ],
      "properties": {
        "title": {
          "type": "string"
        },
        "slug": {
          "type": "string"
        },
        "status": {
          "type": "string",
          "enum": [
            "scheduled",
            "live",
            "ended",
            "canceled"
          ]
        },
        "started_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "ended_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "room_url": {
          "type": "string",
          "format": "uri",
          "description": "The live's own page: https://backstagepass.com/<handle>/s/<slug>."
        }
      }
    },
    "stream_with_replay": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "title",
        "slug",
        "status",
        "started_at",
        "ended_at",
        "room_url",
        "replay_url"
      ],
      "properties": {
        "title": {
          "type": "string"
        },
        "slug": {
          "type": "string"
        },
        "status": {
          "type": "string",
          "enum": [
            "scheduled",
            "live",
            "ended",
            "canceled"
          ]
        },
        "started_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "ended_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "room_url": {
          "type": "string",
          "format": "uri",
          "description": "The live's own page: https://backstagepass.com/<handle>/s/<slug>."
        },
        "replay_url": {
          "type": "string",
          "format": "uri",
          "description": "Same address as room_url; the page plays the replay."
        }
      }
    },
    "pass": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "name"
      ],
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": "string"
        }
      }
    },
    "transaction": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "kind",
        "amount_cents",
        "amount_after_fees_cents",
        "occurred_at"
      ],
      "properties": {
        "id": {
          "type": "integer"
        },
        "kind": {
          "type": "string",
          "enum": [
            "subscription",
            "ticket",
            "tip",
            "drop"
          ]
        },
        "amount_cents": {
          "type": "integer"
        },
        "amount_after_fees_cents": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Net reported by the payment processor. null means no net was reported, never zero."
        },
        "occurred_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      }
    },
    "event_ping": {
      "title": "ping",
      "description": "Sent by Send a ping on the card. Not subscribable; every endpoint can receive it.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator"
      ],
      "properties": {
        "event": {
          "const": "ping"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        }
      }
    },
    "event_welcome": {
      "title": "welcome",
      "description": "Someone followed you. Card label: Follow confirmation.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan"
      ],
      "properties": {
        "event": {
          "const": "welcome"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        }
      }
    },
    "event_seat_reserved": {
      "title": "seat_reserved",
      "description": "You booked a live for a future time. Card label: Live scheduled.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "seat_reserved"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_going_live": {
      "title": "going_live",
      "description": "You went live (GO). Card label: Going live.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "going_live"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_reminder_15m": {
      "title": "reminder_15m",
      "description": "A scheduled live is 15 minutes out. Card label: 15 minutes before.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "reminder_15m"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_reminder_60m": {
      "title": "reminder_60m",
      "description": "A scheduled live is one hour out. Card label: 60 minutes before.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "reminder_60m"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_reminder_1d": {
      "title": "reminder_1d",
      "description": "A scheduled live is one day out. Card label: 1 day before.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "reminder_1d"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_live_rescheduled": {
      "title": "live_rescheduled",
      "description": "You moved a scheduled live to a new time. Card label: Live rescheduled.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "live_rescheduled"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_live_canceled": {
      "title": "live_canceled",
      "description": "You cancelled a scheduled live. Card label: Live cancelled.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "live_canceled"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_counted_in": {
      "title": "counted_in",
      "description": "One fan counted themselves in (RSVP) to a live. Card label: Seat reserved.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "counted_in"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_encore_published": {
      "title": "encore_published",
      "description": "A live's replay is ready to watch. Card label: Encore published.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "encore_published"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_new_pass": {
      "title": "new_pass",
      "description": "A Pass went on sale. Card label: New pass created.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "pass"
      ],
      "properties": {
        "event": {
          "const": "new_pass"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "pass": {
          "$ref": "#/$defs/pass"
        }
      }
    },
    "event_win_back": {
      "title": "win_back",
      "description": "A fan's Pass subscription was cancelled and has ended. Card label: Pass expired.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan"
      ],
      "properties": {
        "event": {
          "const": "win_back"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        }
      }
    },
    "event_joined_backstage": {
      "title": "joined_backstage",
      "description": "A fan bought a Pass. Card label: Joined backstage.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan"
      ],
      "properties": {
        "event": {
          "const": "joined_backstage"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        }
      }
    },
    "event_ticket_bought": {
      "title": "ticket_bought",
      "description": "A fan got a ticket to a live. Card label: Bought a ticket.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "ticket_bought"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_gone_quiet": {
      "title": "gone_quiet",
      "description": "A fan has not joined a room in 60 days. Only fires while the Became inactive automation is on. Card label: Became inactive.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan"
      ],
      "properties": {
        "event": {
          "const": "gone_quiet"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        }
      }
    },
    "event_new_transaction": {
      "title": "new_transaction",
      "description": "A fan paid you: a Pass charge, a ticket, a tip or a drop. Card label: New transaction.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "fan",
        "transaction"
      ],
      "properties": {
        "event": {
          "const": "new_transaction"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "fan": {
          "$ref": "#/$defs/fan"
        },
        "transaction": {
          "$ref": "#/$defs/transaction"
        }
      }
    },
    "event_stream_live": {
      "title": "stream.live",
      "description": "Legacy. Fires at GO, beside going_live. Card label: Went live (legacy).",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "stream.live"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_stream_cut": {
      "title": "stream.cut",
      "description": "Legacy. Every social feed was cut. Card label: Went backstage (legacy).",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "stream.cut"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_stream_restored": {
      "title": "stream.restored",
      "description": "Legacy. The social feeds came back after a cut. Card label: Feeds restored (legacy).",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "stream.restored"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_stream_ended": {
      "title": "stream.ended",
      "description": "Legacy. The live ended (END). Also fires for a live that was taken down. Card label: Show ended (legacy).",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "stream.ended"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream"
        }
      }
    },
    "event_replay_ready": {
      "title": "replay.ready",
      "description": "Legacy. Fires with encore_published. Card label: Replay ready (legacy).",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "event",
        "occurred_at",
        "creator",
        "stream"
      ],
      "properties": {
        "event": {
          "const": "replay.ready"
        },
        "occurred_at": {
          "type": "string",
          "format": "date-time"
        },
        "creator": {
          "$ref": "#/$defs/creator"
        },
        "stream": {
          "$ref": "#/$defs/stream_with_replay"
        }
      }
    }
  }
}

Need help

Email support@backstagepass.com with your handle and, for webhooks, the endpoint URL and the X-Backstagepass-Delivery id.

More in Your business

Contact us — support@backstagepass.com.