Identify a subscriber
PUT /v1/subscribers/:externalId is an idempotent upsert on your own id. It answers 201 the first time and 200 on every call after, so it is safe on every login.
email is sugar that upserts an email subscription. timezone takes an IANA name and sets $timezone from your backend, which is what scheduling in each subscriber’s local time reads. Anything else is a 400 invalid_timezone.
externalId must be URL-encoded in the path. Emails, slashes and spaces all work as ids.$subscriber.created on the event stream and a change records $subscriber.updated, both carrying the attributes snapshot.
Attributes
attributes is a free-form JSON object. Segments filter on it, workflows branch on it, and your own reads see it on every subscriber. Two rules apply:
- The server-side PUT replaces attributes wholesale when the field is present. The client API merges instead, so the app can add keys without wiping what your backend set.
- The serialized object is capped at 64KB. Past that the call is a 400
attributes_too_large.
System attributes
Keys starting with$ belong to BuzzKit and are refused in a PUT body with a 400 system_attribute. They survive a wholesale replace and ride along in attributes on every read.
They refresh on every client identify and every client subscription registration, newest wins.
$timezone is the one you can also set from your backend, through the timezone field above, for subscribers whose devices never call the client API.
Subscriptions
A subscription is registered by channel shape, and it creates the subscriber implicitly if the id is new.lastSeenAt, reactivates an endpoint that was marked invalid, and moves the endpoint if the externalId changed, which is what happens when a device changes hands. You get 201 on create and 200 on a refresh. Every write that is more than a lastSeenAt refresh records $subscription.registered on the stream, and a move also records $subscription.removed for the previous owner. The channel must have a live credential on the tenant, otherwise the call is a 400 channel_not_connected.
Push subscriptions also carry environment, production by default and sandbox for debug builds. It selects which APNs credential slot is used at delivery time.
In practice the iOS SDK makes this call for you on every launch. Register from your backend when you hold the token yourself.
Muting one device versus removing it
PATCH /v1/subscriptions/:id with { "enabled": false } mutes a single subscription. The work iPhone goes quiet while every other device of the same person keeps receiving. A refresh never resets enabled, so a muted subscription stays muted when the app re-registers.
DELETE /v1/subscriptions/:id soft-deletes it instead. The endpoint can register again fresh, and a fresh registration is not muted.
A send goes out only through subscriptions that are enabled, active, and opted in to the message’s topic and channel.
status is active or invalid. Delivery feedback from the provider, an APNs 410 or an FCM UNREGISTERED, flips a push subscription to invalid on its own, so dead tokens need no pruning of yours.Identity verification
A client key alone lets any caller claim anyexternalId. To prove the claim, your backend computes identityHash = HMAC-SHA256(externalId, identitySecret) as hex and hands it to the app at login. The secret comes from GET /v1/tenants/:slug/identity-secret, which is session-only. Keep it server-side and never ship it in the app binary. POST /v1/tenants/:slug/identity-secret/rotate invalidates every outstanding hash.
A valid hash on any client call stamps the subscriber verified with an identityVerifiedAt, both visible on every subscriber read, so anonymous and verified users coexist and you can tell them apart. An invalid hash is a 401 whether or not enforcement is on. Turn enforcement on per tenant and every client call must then carry a valid hash.
Reading a subscriber
verified and identityVerifiedAt. GET /v1/subscribers/:externalId/subscriptions returns the same list on its own.
The timeline is this person’s slice of the event stream, newest first, keyset-paginated and filterable by name, source and provider. It holds every event you tracked plus the lifecycle BuzzKit writes for them: $subscriber.created, updated and deleted, $subscription.registered, muted, unmuted, removed and invalidated, $preferences.updated and $identify. Every $subscription.* event names the subscription it is about, with externalId, channel, platform and endpoint, so a timeline row can say which device changed.
The deliveries list is every delivery addressed to this subscriber, newest first with a total, each carrying a summary of its message: id, channel, topic, title, body and createdAt. It needs the messages:read scope. See delivery for the statuses.
Listing and searching
search matches external ids starting with the text, or a name attribute containing it. Each item carries lastSeenAt, the newest across the subscriber’s live subscriptions and null when there are none, channels such as ["push", "email"], and platforms such as ["ios", "android"].
DELETE /v1/subscribers/:externalId soft-deletes the subscriber and all their subscriptions.
Next
Segments
Target subscribers by attributes, events and activity instead of by id.
Topics and preferences
Let each subscriber choose which notifications reach them, per channel.