Skip to main content
A subscriber is one of your users, addressed everywhere by the id you already use for them, so there is no BuzzKit id to store. A subscription is one way to reach that person on one channel: a push subscription is a device, an email subscription is an address. Nothing channel-specific lives on the subscriber itself, so one person can hold several subscriptions per channel, such as two phones or two addresses.

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 belongs to the subscriber: it is saved as the email attribute, and attributes.email means exactly the same thing, so pick whichever spelling suits you. When the tenant has an email provider connected, either one also registers an email subscription for that address. Without an email provider the address is kept as profile data and nothing fails, so you can set it from day one. Pass subscribe: { email: false } to keep an address as profile data only. 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.
An identical PUT writes nothing. A create records $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.
Registration is idempotent by tenant, channel and endpoint. Re-registering refreshes 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.

Anonymous to identified

Apps track people before they sign up. The iOS SDK mints an anonymous id at first launch and sends it as anonymousId on the first identify after the device was anonymous, and the API merges that subscriber into the identified one.
POST /v1/client/identify
Push subscriptions, topic preferences, attributes, live activities, widget registrations, delivery history and the event history all come across, so the person keeps their devices and their timeline. When the id you identify with is new the subscriber is simply renamed and keeps everything untouched, the same row under a new name, and anything already running for them keeps running. When the id already exists the anonymous subscriber is merged into it and retired. On a preference conflict the newer choice wins, and where both identities hold the same live activity the identified one is kept. A workflow run that was still in flight on a subscriber being retired is canceled with the reason subscriber_merged, because the row it was started for no longer exists. A run started before a rename is untouched. The anonymous id is kept as an alias, so it stays resolvable, and every merge writes a $subscriber.merged event carrying the id it merged from.
Only an anonymous, never-verified subscriber is merged automatically. Naming an identified subscriber is refused with merge_source_identified, and repeating a merge that already happened does nothing, so identifying on every launch is safe. To link two ids deliberately, use the alias endpoint below.
A merge moves the most recent 1,000 events of the retired subscriber. Anything older stays where it was written and is not lost, it simply does not appear on the merged timeline. A merge is also refused with merge_history_pending while the retired subscriber still has events waiting to reach the event store, so retry it and nothing is dropped.

Aliases

A subscriber can be known by more than one id. Every past id is kept as an alias, and any endpoint that takes an externalId accepts an alias, so an id never stops working once it has been used. That covers sends (to), including one scheduled before a merge, and the subscriber a source rule names when a provider still reports the old id.
POST /v1/subscribers/user_42/aliases
The call returns the subscriber’s full alias list and behaves in one of two ways:
  • The id is unused. It is recorded as an alias, and GET /v1/subscribers/onesignal:8f2c1a now answers with user_42.
  • The id already belongs to another subscriber. That subscriber is merged into this one: its subscriptions, preferences, attributes, live activities, widget registrations, deliveries and events move across, its own aliases follow, its in-flight runs are canceled, and it is retired. An id that is only an alias of another subscriber counts as belonging to it, so aliasing one merges that whole subscriber away. This is how you fold imported installs or a legacy id scheme into your real user ids.
GET /v1/subscribers/:externalId/aliases lists them, each with the source that created it: system for one BuzzKit recorded when a device identified, manual for one you added yourself.
Aliasing an id that belongs to another subscriber merges that subscriber away, and the merge is not reversible through the API. Aliasing a subscriber to its own primary id is refused with alias_is_primary.

Identity verification

A client key alone lets any caller claim any externalId. 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.
With verification off, an attacker can register their own device under someone else’s id. BuzzKit still refuses to move an endpoint that already belongs to another subscriber from an unverified call, but enable verification before you send anything sensitive.

Reading a subscriber

The retrieve embeds the subscriber’s subscriptions along with 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.