Anatomy of an alert
The model in one sentence: a pusher asks; the phone decides. Priority is a ceiling, and the tone, the vibration and the spoken text are requests — each is ANDed on the handset with what that subscriber granted this channel and with their own global switches, so the loudest thing an unpermitted channel can achieve is an ordinary notification. Nothing in this API can promise that an alert will ring, take over a screen, or speak aloud.
Ask, grant, deliver
A channel declares what it asks subscribers for — sound,
vibrate, tts, full-screen, location, quiet-hours override —
and the subscribe screen lists every one of them as a switch before the button. What was never asked for
can never be granted, and what was never granted never happens. An alert that reaches for a capability its
channel did not ask for is not an error: it is simply quiet, and the create response's
notes field says so in a sentence, because silent success that isn't is a support
ticket. The fix is almost always to tick one more box on the channel.
Priorities are ceilings
| Priority | The most it may do |
|---|---|
normal |
An ordinary notification, whatever else the alert asks for. The default — deliberately the quiet one, so a caller that never mentions loudness cannot accidentally be loud. |
important |
May sound and buzz insistently, where the subscriber granted this channel sound and vibration. Still a notification. |
urgent |
The only ceiling that may take over a locked screen (where full was granted), be read aloud (where tts was granted), and break through Do Not Disturb (where quiet was granted). |
A ceiling, not a command: the phone may always present less than the priority allows — the subscriber's grants and global switches decide — and never more. An explicitly invalid value is a 422; there is no guessing.
Layouts
| Layout | What the phone renders |
|---|---|
text |
Title plus up to 2,000 characters of body. The default, and right for almost everything. |
image |
A picture card. image_url is required and https-only — it is opened from a notification a reader trusted. |
html |
A rich fragment, sanitised on write (up to 60,000 characters). What the sanitiser removes comes back in the response's blocked array — the alert is accepted, but markup that silently loses half its content is a support ticket, so the removals are named. |
document |
A whole page, kept intact up to four times the html cap and rendered inside the phone's sandboxed iframe, where scripts are inert. Nothing is removed; script_count tells you how many scripts the page carries — counted so you know they will never run. |
POST /v1/drop/preview round-trips your markup through the literal sanitiser the write
path calls — preview with it rather than trusting your own renderer, because your browser would happily run what
the phone will neuter.
Sounds
A pusher picks a tone id, not a file — the phone synthesises the tone, so it costs no asset, no decode
latency, and no network at the one moment the phone may have none. An id an older app does not recognise falls
back to the priority's default tone; a retired tone loses the tone, never the alert. Heard only where the channel
asked for sound and the subscriber granted it — and note the honest boundary: the tone
plays when the app presents the alert; the notification-shade sound belongs to the reader's own settings.
| id | Label | How it reads |
|---|---|---|
default |
Match the priority | Whatever this priority already sounds like. The safe choice. |
chime |
Chime | One soft note. For something worth knowing that is not worth interrupting. |
ping |
Ping | A single bright tick. Short enough to be missed in a pocket. |
bell |
Bell | Two rising notes. The everyday look-at-me. |
knock |
Knock | Two low taps. Quiet, but recognisably deliberate. |
alarm |
Alarm | A repeating pair, like a clock going off. Hard to sleep through. |
siren |
Siren | A rising and falling sweep. Reserve it for the alerts that mean leave now. |
silent |
Silent | No tone at all, even where sound was granted. For a channel that should arrive without a noise. |
Vibration
Patterns are milliseconds on, off, on… — the shape navigator.vibrate takes. On native
builds the pattern collapses to the nearest of the three system haptic weights; an approximate buzz is worth more
than none. default defers to the priority; none is a choice,
not an absence — it stores, and the phone will not buzz even where vibration was granted.
| id | Label | Pattern (ms) | How it reads |
|---|---|---|---|
default |
Match the priority | — |
What this priority already does. |
none |
No vibration | — |
Never buzz, even where vibration was granted. |
short |
Short | 35 |
One brief tap. |
double |
Double | 40 · 90 · 40 |
Two taps. Tells itself apart from an ordinary message without being urgent. |
long |
Long | 400 |
One sustained buzz. |
sos |
SOS | 90 · 70 · 90 · 70 · 90 · 200 · 260 · 70 · 260 · 70 · 260 · 200 · 90 · 70 · 90 · 70 · 90 |
Three short, three long, three short. Unmistakable, and unmistakably an emergency. |
heartbeat |
Heartbeat | 70 · 90 · 70 · 400 · 70 · 90 · 70 |
Two beats, repeated. Insistent without being alarming. |
Spoken text
tts_text is what a phone reads aloud — where the channel asked for
tts, the subscriber granted it, and the priority is high enough to warrant a voice in
somebody's kitchen. At most 400 characters, because that is what the phone's speech call clamps to: storing more
would store a sentence cut off at the moment it is spoken. tts_lang is a BCP-47 tag
("nl-BE"); null falls back to the phone's own language. Write the spoken text
separately from the body — what reads well silently rarely speaks well.
Location: narrow, never widen
A channel may declare a circle — lat, lon,
radius_km, all three or none — and an alert may carry its own. The alert's circle may
narrow the channel's area and never widen it: the subscribe screen already told every subscriber where
this channel operates, and an alert reaching past that would make the one disclosure they read a false one. A
channel with no circle may target anywhere.
- Evaluated on the device. The circle travels to the phone and the phone checks it against its own position. A subscriber's location never leaves their handset — not to the channel, and not to us.
- Smaller than a kilometre is fine.
radius_kmis a decimal:0.05is a 50-metre circle around one platform, one gate, one entrance. Containment against the channel's area is checked with a tolerance of 2% of the channel radius (at least 10 m), so a circle that pokes a few metres over a hand-drawn boundary is accepted and one that doubles the area is not. - The handset's accuracy is the real floor. A phone knows where it is to within about 5–20 m outdoors under open sky, and tens of metres indoors, under a roof or beside a train. A circle much smaller than that will alert some people just outside it and miss some just inside it — which is a property of satellites, not of this API.
- Fail open. A phone that cannot know where it is — location permission refused, no fix — shows the alert anyway. For the channels this feature exists for, a missed evacuation notice is the worse failure, and the failure mode is not symmetrical.
geo_rule(and the channel'sloc_rule) is a human sentence about where the alert applies — shown to readers, because a circle on a map explains less than “south bank of the river” does.
Collapse keys
collapse_key is an author label — storm-14,
gate-change — that makes a newer alert replace an older one in the notification
shade instead of piling up. Ten updates to one storm stay one entry. It is a slug on purpose
([a-zA-Z0-9._:-]): the value is hashed into a notification id on two sides of a
language boundary, and restricting the alphabet is what stops a key that round-trips differently producing two
entries where the author expected one. The full history stays in the app and in your
alert list — collapsing is presentation, not deletion.
Acknowledgements and expiry
requires_ack: true puts an acknowledgement button on the card; devices that press it
report back, and the alert's acks counter is the sum of those self-reports — a report,
not a receipt. expires_at means “stop treating this as current”, not “pretend it never
happened”: phones move the alert out of the spotlight and stop ringing, but a reader can still scroll back to
last night's storm warning. Combined, one rule is enforced: the acknowledgement window must stay open at least
five minutes. The reader may be asleep, and a window shorter than the time it takes to pick up a phone
produces a dashboard claiming nobody responded to an evacuation instruction.
Quota: reach at send time
There is no per-device delivery row in this product, so “one alert to one device” cannot be metered — and is not.
What is metered is the honest substitute: when an alert becomes SENT or SCHEDULED, the channel's subscriber count
at that moment (its reach) is added to that channel's monthly meter, and
cancelling a scheduled alert before it goes out subtracts exactly what it added. Sending costs the same effort at
four subscribers and four million — the meter exists for the included volumes that arrive with plans and billing,
and until those land nothing clamps. Every surface that mentions quota states this same rule, because a quota
metered on a number nobody can verify would be the delivery fiction this product refuses everywhere else.