Runs onWrit CloudDesktopSelf-hosted
On this page
Nine channels, and one grid.
Two different systems share the word “notification”, and confusing them costs an afternoon. Channels are what a monitor or an automation fires. Preferences are what the platform tells you. This page keeps them apart.
split ▸ two systems
Which one do you mean?
Both live under Settings, both say “notifications”, and they do not share a channel list. Start here:
| Monitor and automation channels | Nine providers, configured once for the organization. A monitor detects a change, or an automation reaches a notify step, and the message goes out. Recipients are addressed as "channel:id". |
| Platform notification preferences | Six channels across seven categories, per user. Security, billing, team, runs, agents, marketplace and support events — the things Writ tells you about your own account. |
channels ▸ nine
Set up a channel.
Configure each one once for the organization, then add recipients to it. Every channel has a test send.
| Channel | API key | What you provide |
|---|---|---|
| Pushover | pushover | Application token and user key. Optionally a title, a message, a priority from −2 to 2, a sound, a link title, and HTML on or off. |
| Email (SMTP) | email | Host, port, username, password, from address, from name, and TLS on or off. |
| Email (Google Workspace) | email | Connect with OAuth instead of SMTP. The config reports which provider is active and which account is connected, and you can disconnect. |
| SMS | twilio | Account SID, auth token, and a sending phone number in E.164 form. The auth token is never returned once saved. |
whatsapp | A sending number in whatsapp:+1234567890 form. It rides on the same SMS provider credentials, so it only becomes usable once both are configured. | |
| Signal | signal | The URL of a signal-cli REST server you run yourself, plus the sender number in E.164 form. |
| Slack | slack | A channel webhook URL per recipient. The API only ever shows the host back, never the full URL. |
| Discord | discord | A channel webhook URL per recipient. The API only ever shows the host back, never the full URL. |
| Telegram | telegram | The token BotFather gives you, plus a chat id per recipient. The token is masked in responses. |
| Webhook | webhook | Your own URL, with optional custom headers and HMAC signature verification. Deliveries retry with exponential backoff. |
Credentials go in and never come back out: the SMS auth token is not returned once saved, Slack and Discord URLs are shown as a bare host, and the Telegram token is masked. If you need to change one, replace it — you cannot read it back to check it.
The webhook channel signs every delivery. The exact signature contract, header names and a verification snippet live on Webhooks.
Webhooks →recipients ▸ channel:id
Naming who to reach.
Once a channel has recipients, an automation refers to them as "channel:id" strings — for example ["pushover:1","email:3"]. One call lists every recipient across every channel, each with its identifier masked, which is what a picker renders from.
recipients
{
"channels": ["pushover", "email"],
"recipients": ["pushover:1", "email:3"]
} list-recipients.sh
curl https://api.usewrit.app/api/notifications/recipients/all \
-H "Authorization: Bearer $WRIT_API_KEY" Note the key for SMS is twilio, not sms — that is the value the API expects in a channel list. In the platform preferences grid further down, the same idea is spelled sms. They are different systems.
controls ▸ tenant-wide
Delivery controls.
These apply across the organization, not per channel — so a noisy night does not become a hundred messages:
| Control | What it does |
|---|---|
| Quiet hours | A start and an end time during which nothing is delivered. |
| Rate limiting | A maximum number of messages per period. |
| Batching | Collect what happens inside a window, in minutes, and send it as one message. |
| Agent-error alerts | Tell me when an agent errors, with a threshold and a delay in minutes. |
| Target-health alerts | Tell me when a target keeps failing, with a threshold and a check interval. |
| System-health alerts | Tell me when an agent disconnects, after a delay. |
placeholders ▸ change alerts
What you can put in the message.
Change notifications render these placeholders into your message template. Group them by what they answer:
| What was watched | {url} {target_id} {target_name} {selector} {selector_name} {agent_id} {agent_platform} {change_count} {check_count} |
| What changed | {diff} {detected_change} {previous_content} {new_content} {content_hash} {previous_hash} |
| When | {timestamp} {date} {time} |
An unknown placeholder is left as-is rather than failing, so a typo costs you a literal token in the message, not a lost alert. Previews are truncated: {diff} and {detected_change} at 500 characters, {previous_content} and {new_content} at 200.
relay ▸ from a device
A linked device handing off to the cloud.
A device runs automations locally, but some channels need the cloud to deliver. Only email, pushover, SMS, WhatsApp and Signal relay; webhook, Slack, Discord and Telegram are delivered by the device itself, because it can reach them directly.
relay.sh
curl -X POST https://api.usewrit.app/api/notifications/relay \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channels": ["email", "pushover"],
"recipients": ["email:3", "pushover:1"],
"title": "Price dropped",
"message": "Now $41.00, was $49.00.",
"url": "https://example.com/product/42"
}' | Field | Rule |
|---|---|
channels | 1 to 8, and a subset of the relayable set. Anything else is refused with 400. |
recipients | Recipient references, the same "channel:id" strings. |
title | Up to 200 characters. |
message | 1 to 4000 characters. |
priority | Optional. |
url | Optional link, up to 2000 characters. |
email_subject | Optional, up to 300 characters. |
Delivery goes to your own configured recipients — never an arbitrary address supplied in the call. Relayed notifications are rate-limited to 120 per hour per organization, so a flapping local automation cannot drain an SMS budget in minutes.
preferences ▸ the other grid
Platform notification preferences.
This is the second system: what Writ tells you, per user, at GET and PUT /api/notifications/preferences. Different channel set, different purpose.
Six channels
Not the same nine as above:
in_app | The bell and the inbox inside Writ. |
email | Your account email. |
sms | A personal contact point on your preference row. |
whatsapp | The same personal contact point. |
signal | The same personal contact point. |
pushover | Your own Pushover user key. |
Seven categories
Events are grouped so you can reason about them without reading every row:
| Security | Account security activity. |
| Billing & payments | Balance, receipts, payment problems. |
| Team | Invites and membership. |
| Automations & runs | What your workflows did. |
| Agents & devices | What your machines did. |
| Marketplace & creator | Listings, installs, earnings. |
| Support | Ticket activity. |
Four rules that surprise people
- The grid is sparse. Only the channels an event actually offers are shown — a blank cell is not a switch you are missing.
- Some cells are locked and always on: security alerts, payment problems and team invites. The API will not store an override for them.
- Some events are email-only, by design.
- Per-monitor change alerts deliberately live outside this grid, with their own per-target settings.
The self-hosted build ships a trimmed catalog, without the billing and marketplace events — there is no billing or marketplace in that build to notify you about.
reference ▸ next
Keep going
What detects the change that fires the alert.
→ WebhooksThe signature contract, verified.
→ Your own machinesThe devices that relay through the cloud.
→ Users and teamsWho gets which platform events.
→ Writ DesktopAutomations that run and alert locally.
→ Self-hostingThe trimmed catalog in the open-core build.
→faq