Any HTTP request rings an alarm on every device paired to your account, at once.
This is the whole of the public interface: two ways to ring, and one management API.
Printed from this deployment ·
Hosts
Two domains, and the split is deliberate.
| Host | Serves |
alarmsender.com |
Everything: the console, the management API, the MCP endpoint, and triggers. |
alrm.sh |
Triggers only. Every other path answers 404 — not 401,
not 403, absent. Trigger URLs live in other people's cameras and
controllers, so that domain admits as little as possible.
|
Two ways to ring
A trigger URL carries its own secret in the path and needs no other
credential. Give one to a camera, a cron line or a webhook you do not control. Each
is revocable on its own, can be restricted to a set of source addresses, and has its
own rate limit.
An API key is a bearer credential for code you wrote. One key can
ring any chime on the account and, with the manage scope, can also
administer it. Prefer a trigger URL when you are handing the secret to something else.
Authentication
Authorization: Bearer ak_live_...
Keys are shown once, at creation, and only a hash is stored. A lost key is replaced, not recovered.
| Scope | Grants |
trigger |
POST /v1/trigger and nothing else. Every management route answers
403. Available on every account.
|
manage |
The management API below: chimes, triggers, devices, keys, agents, activity.
Granted per account by an administrator. Where it is not granted,
the console does not offer it and the server refuses both minting one and using one.
|
Ringing is open to everyone because a key that can only ring is the same risk as an
agent over MCP, which any account may connect. Managing is a different thing, so it is
a decision somebody takes.
A key inherits the account it belongs to. There is no expiry — a key
works until it is revoked — and a manage key can mint further keys. Give
a script the trigger scope unless it genuinely has to manage something.
POST /v1/trigger
Rings a chime. Requires the trigger scope.
curl -X POST https://alarmsender.com/v1/trigger \
-H "Authorization: Bearer $ALARMSENDER_KEY" \
-H "content-type: application/json" \
-d '{
"chime": "backup-failed",
"title": "Nightly backup failed",
"body": "rsync exited 23 on fileserver",
"dedupe_id": "backup-nightly"
}'
| Field | Meaning |
chime | Slug of the chime to ring. Required — there is no account-wide default on this route. |
title | First line on the device. |
body | Second line. |
dedupe_id | Your own label for "this is the same alert as before". Drives the cooldown; see below. |
vars | Object of extra values passed through to the device. A query parameter of the same name wins. |
ttl | Seconds the alarm stays valid for a device that is offline right now. Clamped to 5–86400; defaults to the chime's own. |
Bodies are read up to 256 KB. A ?ttl= query parameter overrides the body's.
Responses
202 {"ok":true,"event_id":"evt_M3JMTBCH...","chime":"backup-failed","delivered":2}
delivered counts devices the alarm was written to, not people who saw it.
Zero means nothing was connected; the alarm is replayed when a device reconnects, so
it is not lost.
202 {"ok":true,"suppressed":"cooldown","suppressed_by":"evt_...","chime":"backup-failed"}
Accepted and deliberately not rung, because an identical alert rang inside the chime's cooldown window.
GET or POST /t/<token>
No credential beyond the token in the path. Both methods work, so anything that can
fetch a URL can ring an alarm:
curl "https://alrm.sh/t/t_7J9KCZ...?title=Gate+opened&body=North+drive"
What a trigger reads out of the request is set by its field mapping in the
console — query parameters by default, or a path into a fixed JSON body for gear whose
payload you cannot change. A trigger has its own default chime, so the caller need not
name one. /v1/t/<token> is the same route under its older spelling
and still works, so a URL already pasted into a device never has to be changed.
Cooldown, TTL and rate limits
Cooldown
Set per chime. Within the window, a repeat of the same alert is accepted and not rung.
"The same alert" means the same dedupe_id if you sent one — and if you did
not, the same chime, title and body. That fallback exists because the senders that
repeat themselves most are the ones least likely to label anything.
Time to live
A device that is offline when an alarm fires receives it on reconnect, provided the TTL
has not run out. Set it to the period over which the alarm still means something.
Rate limits
| Path | Bucket | Limit |
/t/<token> | per trigger | that trigger's own setting, 60/min burst 5 by default |
/v1/trigger | per API key | 60/min, burst 5 |
| MCP | per account | 60/min, burst 5 |
Over the limit is 429 with a retry-after header in seconds.
Back off; do not retry immediately. The limit paces a flood rather than ending
one — at 60/min a stuck sender rings a phone once a second indefinitely, so the chime
cooldown is what actually stops that.
Refusals on a trigger URL are recorded and appear in the console's activity log.
Refusals on /v1/trigger are not: that log hangs off a trigger, and an
API key is not one. A throttled script sees 429s and the console sees
nothing.
Errors
{"error":"rate_limited","message":"rate limit of 60/min exceeded; retry in 1s"}
| Status | code | Usually means |
| 400 | bad_request | No chime named, or a field is malformed. |
| 401 | unauthorized | No credential, or one that is not valid. |
| 403 | forbidden | Valid credential, wrong scope — or a source address the trigger does not accept. |
| 404 | not_found | No such chime or trigger. On the trigger host, also everything that is not a trigger. |
| 409 | conflict | Slug already taken, or a chime still in use by a trigger. |
| 413 | payload_too_large | Body over 256 KB. |
| 429 | rate_limited | Over the limit. Honour retry-after. |
| 403 |
account_suspended |
The account is frozen: nothing rings and nothing can be changed until an
administrator restores it. Nothing has been deleted — every chime, trigger and
URL is still there and still works the moment it is lifted. Stop retrying; this
one does not clear on its own.
|
| 403 |
account_lapsed |
The account's plan has ended and its grace period is over, so nothing rings.
The account can still be signed into and changed, and nothing has been
deleted: everything works again the moment it is renewed. Stop retrying;
this one does not clear on its own.
|
Management API
Everything the console does, it does through these. All require a key with the
manage scope, on the console host only.
| Route | Does |
GET /v1/chimes | List chimes with their specs, versions and timestamps. |
POST /v1/chimes
PUT /v1/chimes/:slug | Create or replace a chime. Saving bumps its version. |
DELETE /v1/chimes/:slug | Delete one. Refused while a trigger still points at it. |
POST /v1/chimes/:slug/test | Ring it on every connected device, bypassing the cooldown. |
GET /v1/endpoints | List triggers. |
POST /v1/endpoints | Create one. The URL or address is returned once and never again. |
PATCH /v1/endpoints/:id | Rename, re-point, enable or disable, change guards. |
POST /v1/endpoints/:id/regenerate | Issue a new token. The old URL stops working at once. |
POST /v1/endpoints/:id/duplicate | Copy a trigger's settings under a new secret. |
GET /v1/endpoints/:id/captures | Payloads captured for building a field mapping. |
DELETE /v1/endpoints/:id | Delete a trigger. |
GET /v1/events | Activity: every alarm, with what was delivered and what was answered. |
GET /v1/rejections | Refusals: who was turned away, why, and how often. |
GET /v1/devices
DELETE /v1/devices/:id | Paired devices, and un-pairing one. |
POST /v1/pair-code | A short-lived code for pairing a new device. |
GET /v1/keys
POST /v1/keys
DELETE /v1/keys/:id | API keys. |
GET /v1/agents | Connected agents, what each may create, and what it has made. |
PATCH /v1/agents/:id/permissions | Let one agent create chimes or triggers, or stop it. |
POST /v1/agents/purge | Remove an agent and, optionally, everything it made. |
DELETE /v1/agents/:id | Disconnect an agent and keep its work. |
Account ceilings: 50 chimes and 100 triggers. Generous rather than tight — a great deal
of work fits in three chimes and two triggers, because a trigger is reusable and its
field mapping does the varying. They exist so a loop cannot quietly fill the account up.
Composing a chime
A chime's spec.audio.source is one of builtin:<name>,
tones, or silent. tones is a sound you write
yourself as [hz, ms] pairs, synthesised on the device — nothing is
uploaded and nothing is fetched at ring time.
curl -X PUT https://alarmsender.com/v1/chimes/desk \
-H "Authorization: Bearer $ALARMSENDER_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Desk",
"spec": {
"priority": "normal",
"audio": {
"source": "tones",
"tones": [[587,90],[0,50],[880,160]],
"volume": 0.4, "loop": 1, "gapMs": 0, "stream": "notification"
},
"visual": { "mode": "pulse", "colors": ["#e02424","#0d1117"], "hz": 1.2, "durationMs": 4000 },
"notify": { "title": "{{title}}", "body": "{{body}}", "persistent": true },
"cooldownSec": 30, "ttlSec": 300
}
}'
0 hz is a rest. Sine tones only, so a chime gets its character from
interval, rhythm and repetition rather than from timbre. The limits:
| Limit | Why |
| 32 pairs | Repeats are written out, not looped — see below. |
| 6000 ms in total | The length of one utterance. loop and gapMs repeat the whole sequence on top of it. |
| 180–8000 hz | A phone speaker cannot usefully reproduce much below 180 hz. Under it the chime rings and is inaudible, which is the one failure worth refusing outright. |
loop repeats the whole sequence, not the end of it. A motif
that should sound three times has to be written out three times, and that is what the
pair limit is mostly spent on. A sequence over 6000 ms is trimmed and the response says
so in warnings; a note below 180 hz is kept but warned about, because a
person editing in the console can hear their own chime and may have speakers that
justify it. The same sequence sent by an agent over MCP is refused
instead — an agent cannot hear what it wrote.
Agents and MCP
An AI assistant does not use an API key. It connects to
https://alarmsender.com/mcp over MCP, is approved on a consent
page you see, and holds a grant you can revoke with one click. Its tools cover listing
chimes, ringing one, and asking whether a person actually answered.
Creating and editing are off until you switch them on for that agent specifically, on
the Agents tab. An agent can never mint a chime that overrides Do Not Disturb, and may
only change or delete what it created itself. Everything it makes is labelled with its
name, and can be removed along with the agent in one action.
Alarm Sender · this document describes the deployment it was served from.