Broadcasts

Send a broadcast

curl -X POST https://api.subscriby.net/v1/projects/$PROJECT_ID/broadcasts \
  -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "<b>New drop is live</b>\nDoors open at 19:00 UTC.",
    "audience": "customer"
  }'

202, not 200: the send is queued and runs in the background at roughly 28 messages a second to stay inside the platform's rate limit. The response describes what was addressed, not what was delivered.

Audience segments

SegmentWho it addresses
allEvery member reachable on the project's connector, whatever their status.
customerMembers holding a paid subscription right now.
trialingMembers currently inside a free trial.
leadMembers who have used the project's bot but never subscribed.
churnedMembers who were customers and are not any more.
expiring_soonMembers whose subscription genuinely lapses within expiring_within_days.
cancelled_still_activeMembers who cancelled but have not run out yet: still reachable, still winnable.
pausedMembers who paused their subscription rather than ending it.
trialing_cardlessMembers trialing without a card on file, who will not convert on their own.
all_pass_holdersEveryone holding a valid pass for any upcoming window.
all_pass_holders_not_in_queueEveryone who has not tapped their invite link, across every upcoming window.
pass_holdersEveryone holding a valid pass for one access window you choose (pass_window_id).
pass_holders_not_in_queueHolders of one window you choose who have not tapped their invite link yet.

A member is a recipient only if they have a linked chat on that project's connector. Members with no chat, or whose chat belongs to a different installation, are excluded silently, which is why recipient_estimate is frequently well below a project's total membership.

An auto-renewing subscription is never expiring_soon. A subscription's ends_at is rewritten to the new period end on every renewal, so a date inside the horizon describes an auto-renewing member's next invoice rather than an expiry. Counting it would place every monthly subscriber in this segment once a month. A member is counted only when their access genuinely lapses: they have cancelled, or the plan is not recurring.

Narrowing a segment to a plan

plan_id composes with audience rather than replacing it:

RequestWho it addresses
audience=customer + plan_id=<gold>People paying for Gold right now.
audience=churned + plan_id=<gold>People who held Gold and left.
audience=paused + plan_id=<gold>People whose paused plan is Gold.
audience=expiring_soon + plan_id=<gold>Gold members lapsing inside the horizon.

The plan and the state describe the same subscription, so a member paying for Silver who once trialled Gold is not matched by the first row. churned is the deliberate exception in the other direction: the subscription proving they held the plan is the one that ended, so it is not required to be live.

A plan_id sent with lead or a pass segment is refused with 422 and reason: plan_filter_unsupported, not ignored. Silently dropping it would return a recipient count for a different audience than the request described, and a caller has no way to notice.

Message formatting

The body is reduced to the HTML subset the delivering connector accepts before sending; for the messaging connector that is <b>, <strong>, <i>, <em>, <u>, <ins>, <s>, <strike>, <del>, <a>, <code>, <pre>, <blockquote> and <tg-spoiler>. Anything else is stripped, not escaped: the platform rejects an entire message containing an unknown tag, so removing it is what keeps the message deliverable.

The 4,096-character limit is the platform's own. A longer body is refused here rather than being truncated per recipient.

Refusals

Every 422 is refused before anything is queued. The underlying job re-checks the same conditions and aborts silently, which is correct for a background job and wrong for an API caller, so the endpoint rejects them up front rather than returning 202 for a send that could never leave. A segment that resolves to zero recipients is the send action's own refusal, shared with the dashboard: it is not queued.

post/v1/projects/{project}/broadcasts

Request

  • Base URL: https://api.subscriby.net
  • URL: https://api.subscriby.net/v1/projects/{project}/broadcasts
  • Auth: HTTP bearer

Path parameters

projectstring uuid required

The project, resolved by the route binder.

Headers

Idempotency-Keystring uuid required

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Request body

messagestring required

The message body, 1 to 4,096 characters, in the HTML subset the delivering connector accepts. Unknown tags are stripped rather than escaped, because the platform rejects a whole message that holds one.

audience'all' | 'customer' | 'trialing' | 'lead' | 'churned' | 'expiring_soon' | 'cancelled_still_active' | 'paused' | 'trialing_cardless' | 'all_pass_holders' | 'all_pass_holders_not_in_queue' | 'pass_holders' | 'pass_holders_not_in_queue'

Who a broadcast is addressed to. Previously a bare string that BroadcastMessageJob fed to ProjectUserStatus::tryFrom(), which quietly meant "anything unrecognised * broadcasts to everyone" — a typo in the audience widened the send rather than refusing it. The pass segments cannot be expressed as a status at all: they are a fact about a subscription bound to a window, not about the member. Declaration order is the order the picker offers them, so each pass segment's every-window form sits above the one that asks for a single date. The backing values of the status-derived cases match ProjectUserStatus so a broadcast queued before this enum existed still resolves.

pass_window_idstring uuid nullable

The access window for pass_holders and pass_holders_not_in_queue; required by those two, ignored by every other segment.

plan_idstring uuid nullable

Narrow the segment to members whose subscription is on this plan; it composes with audience rather than replacing it. Refused, not ignored, for lead and the pass segments.

expiring_within_daysinteger nullable

The horizon expiring_soon counts within, 1 to 90; defaults to 7. Read by no other segment.

Response

202 with the segment, the verified filters, the recipient count and the ETA.

Example response

{
  "data": {
    "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13",
    "audience": "customer",
    "recipient_estimate": 128,
    "estimated_seconds": 5,
    "status": "queued"
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.