Notify docs
Concepts

How Notify thinks.

The nouns the API, the console and these docs share, and what happens between your send call and a user’s screen.


1

Recipients

A recipient is one of your users, identified by your own user id (external_id, up to 255 of A-Z a-z 0-9 . _ : @ | -). Its profile holds an email address, a phone number (kept for SMS later), a name, a locale, a time zone, quiet hours and your own properties. It can be linked to a ProductCraft Auth end user with auth_user_id, but it does not have to be.

Its devices are APNs tokens, FCM tokens and browser push subscriptions, each with the app it belongs to and when it was last seen. A recipient keeps up to 20. Dead ones are pruned automatically when Apple, Google or a push service says so.

Create recipients with identify (an upsert), inline in a send, in bulk (1,000 per call), or by minting a token for them.


2

Categories and notification types

A category (“Billing”, “Security”, “Product updates”) is what users set preferences on. It has a default per channel: on, off (only if the user opts in) or mandatory (users cannot turn that channel off; use it for the few things they must get). A transactional category sends email without a one-click unsubscribe header.

A notification type (invoice.paid) is one kind of message: its category, a template per channel and locale, its routing, a time to live (24 hours by default), an optional per-recipient throttle, and an optional critical flag. There is no workflow builder: the routing fields cover delays, unread checks and fallbacks.

Critical types (security notices) ignore opt-outs and quiet hours and use their own rate budget. They never bypass a hard bounce, a spam complaint, the Mail suppression list or an email pause: those protect your sender reputation.


3

Channels and routing

Channels are inbox, email, push (APNs and FCM together) and webpush. A type’s routing says which ones a message uses:

routing
routing: {
  always:   ["inbox"],                                  // every recipient
  fallback: [
    { channel: "push" },                                // tried first
    { channel: "email", after_unread_seconds: 900 },    // only if still unread after 15 min
  ],
}
  • always channels are planned for every recipient, normally the inbox.
  • fallback steps are tried in order. The next step runs when the current one had nothing to deliver (no device, no address, opted out, channel not set up) or failed.
  • A step with after_unread_seconds waits instead: it goes out only if the inbox item is still unread then, even if earlier steps succeeded. Reading the item first ends it as read_in_inbox.
  • A send can narrow the channels with channels, never add to them.

4

Notifications, deliveries and the Message log

A notification is one accepted send: one type, 1 to 1,000 recipients, your data. It fans out into deliveries, one per recipient, channel and device or address, each with a state, a reason, its attempts and (for push) the text that was rendered. The console’s Message log shows both; every recipient also has a timeline across notifications.

Delivery states
StateMeaning
pendingPlanned, waiting for a worker.
scheduledWaiting on purpose: send_at, quiet hours, the email budget or an “unread after” timer. The reason says which.
sendingHanded to the provider, answer not back yet.
retryThe provider failed temporarily; it is tried again with backoff until the type’s TTL.
sentAccepted by Apple, Google, the browser’s push service or Mail. Final for push.
deliveredInbox: written. Email: the receiving server accepted it (Mail reports it).
bouncedEmail bounced or was marked as spam.
failedWill not be delivered (credential rejected, payload too large, retries ran out, expired).
skippedNot attempted, with a reason: opted out, no device, cancelled…
prunedThe device token was dead; the device was removed.

Push “sent” is not “seen”. Apple and Google only confirm that they accepted a push. Whether the phone was on, the app still installed or the user looked is not reported to anyone. Every reason a delivery can carry, with its fix, is on Why didn’t it arrive?


5

Preferences

Each recipient has cells of scope × channel: per category, per type, or for everything, per channel or * for all channels, plus “unsubscribe from everything” (never blocks critical types). The most specific cell wins: type × channel, then type × all, then category × channel, category × all, all × channel, and finally the category’s default.

Users change them through your UI with a recipient token (preferences screen); your support team can change them from your backend. Every change is recorded with its source. The one-click unsubscribe link in an email turns that category’s email off.


6

Quiet hours, scheduling and cancellation

  • Quiet hours are a window in the recipient’s time zone (22:00–08:00), or the workspace default. Push and web push wait until the window ends (then up to 10 minutes of spread); email waits only if the type says so; the inbox never waits. No time zone means no quiet hours. A held delivery that passes the type’s TTL expires, so a 1-hour TTL means “don’t send at night”.
  • Scheduling: send_at or delay_seconds, at most the type’s TTL ahead.
  • Cancellation keys: give a send a cancellation_key (invite:inv_77) and cancel it later by that key, without storing our id. Retract removes an item already in the inbox.

7

Templates and languages

Templates use a Mustache subset: {{var}}, {{#section}} and {{^inverted}}, no partials. A missing variable renders empty and is recorded as a warning on the delivery. Links must be https:. Email templates live in ProductCraft Mail; the email channel names one.

Language fallback: the recipient’s exact locale (pt-PT), then its language (pt), then the type’s default_locale.


8

Tenants, actors and tags

  • tenant (your organisation or team id) on a send lets the inbox show one team’s items. Tenant objects with their own default preferences are planned.
  • actor is who caused the notification: their name and avatar appear in the inbox, and they are left out of the recipients unless you say otherwise.
  • tags (up to 10 pairs) make a notification searchable in the log.

9

Recipient tokens

Browsers and apps call /v1/client/… with a short-lived token your backend mints for one recipient: 1 hour by default, at most 24 hours, with scopes (inbox read and write, devices, preferences, the real-time stream). They are kept in memory by our browser packages, never in storage or URLs. Revoke one user’s tokens, or every token in the workspace, at any time. There is no public key that can act as any user and no shared signing secret in your backend.

The client API only accepts browser calls from the origins you list in Notify settings (while the list is empty, only http://localhost, for development). Native apps send no Origin and are not origin-checked.