Quick links

Sending alerts

Every header, every body field and its exact rules, how severity and category behave, and what each field in the response means.

An alert is one POST to /notify with a JSON body. This page is the full contract for that request. To attach images, see Images; for checklists, see Checklists.

POST https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify
Table, scrolls horizontally when narrow
Header Required Example Purpose
Authorization Yes Bearer zona_live_... Authenticates one source. Use the Bearer scheme exactly.
Idempotency-Key Yes backup-20260726-020000 Names one logical event so retries cannot create duplicates. See Idempotency and retries.
Content-Type Yes application/json Use multipart/form-data only when sending images, and let your HTTP library write the boundary.

Header names are case-insensitive. Only POST is accepted; any other method returns 405 METHOD_NOT_ALLOWED.

{
  "title": "Build complete",
  "body": "The release build finished successfully.",
  "category": "build",
  "severity": "high",
  "todo": ["Tag the release", "Post the notes"],
  "data": {
    "buildId": "2026.07.26.14",
    "branch": "main",
    "durationSeconds": 482
  }
}
Table, scrolls horizontally when narrow
Field Type Required Rules
title string Yes 1 to 120 characters after surrounding whitespace is trimmed.
body string Yes 1 to 2,000 characters after surrounding whitespace is trimmed.
category string or null No 1 to 80 characters when present. Omitted, null or "" means no category.
severity string or null No low, medium, high or critical, case-insensitive. Omitted, null or "" means no severity ring.
todo array or null No Up to 20 checklist entries. Rules in Checklists.
data object No Defaults to {}. At most 4,096 UTF-8 bytes once serialized. Arrays and plain values are rejected.

The whole JSON body is limited to 16 KiB; a larger body returns 413 PAYLOAD_TOO_LARGE. A field that breaks a rule returns 400 INVALID_PAYLOAD.

Zona also caps the push it generates at a conservative 3,800 UTF-8 bytes. Text that uses several bytes per character, such as Chinese or emoji, can therefore return 400 INVALID_PAYLOAD before it reaches the character limits above. If that happens, shorten the title and body.

Severity is optional presentation. It changes how the alert looks, not how it is delivered: it does not affect delivery priority, sound, rate limits or which source the alert belongs to.

Table, scrolls horizontally when narrow
Value In Zona Notification accent
omitted, null or "" No ring Zona green
low Green ring around the source’s avatar Green
medium Yellow ring Yellow
high Orange ring Orange
critical Red ring Red

The ring shows on the inbox row, on the alert’s details, which also name the level, and on the banner Zona shows while it is open. It is thicker for more urgent levels, so the level reads without relying on colour. While the alert is unread the ring pulses, faster for more urgent levels; once you have read it, or with Reduce Motion on, the ring stays still. Its colours are the system green, yellow, orange and red for the active theme’s light or dark appearance.

On iPhone, iOS controls the icon in a system notification, so a lock-screen or system banner does not show the severity colour. It appears once you are in Zona.

The operator can temporarily switch off critical alerts. While that switch is off, a critical request returns 403 CRITICAL_SEVERITY_DISABLED. Only send a lower severity instead if it still describes the event honestly.

Severity is part of the payload for idempotency: resending a key with only the severity changed returns 409 IDEMPOTENCY_CONFLICT.

category is a free-form label such as backup, build or deploy. It groups and describes alerts; it does not choose a sound. Each source’s sound is set in the app.

The data object

Link to this section

data carries context for your own tooling, such as a build ID, a branch or a job name. Zona stores it with the inbox item. It does not change routing, source identity, sound, severity or push behaviour.

  • Use namespaced, non-sensitive keys.
  • Keep large logs and files elsewhere, and link to them.
  • Never put tokens, passwords or secrets in data. Like the title and body, it can reach the phone.
curl --request POST \
  "https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify" \
  --header "Authorization: Bearer $ZONA_SOURCE_TOKEN" \
  --header "Idempotency-Key: build-20260726-14" \
  --header "Content-Type: application/json" \
  --data '{
    "title": "Build complete",
    "body": "The release build finished successfully.",
    "category": "build",
    "severity": "high",
    "data": {"buildId": "2026.07.26.14", "branch": "main"}
  }'

These examples mint a fresh key each run, which is fine for a one-off send. A job that retries should create its key once per event and reuse it; see Idempotency and retries.

A newly accepted alert returns 202. An identical replay of an earlier request returns 200 with the original record.

Table, scrolls horizontally when narrow
Field Meaning
notificationId UUID of the inbox record. Log it if you want to correlate later.
sourceId The source’s permanent ID, derived from the token.
sourceName The source name stored with this alert.
acceptedAt When the inbox record was created, in UTC. A replay returns the original time.
idempotentReplay true when the response returns an existing record instead of a new one.
attachmentAccepted true when every image sent is stored. false when no image was sent.
attachmentError UPLOAD_FAILED when images could not be stored after the alert was accepted; otherwise null.
pushQueued Delivery jobs created for eligible phones. Present on a new alert, omitted on a replay.
pushAttempted Kept for older clients. Equals pushQueued on a new alert and 0 on a replay. It does not mean a push has been sent yet.
pushAccepted Kept for older clients. Always 0 from /notify, because push results arrive later.

Errors use a small envelope, {"error": "INVALID_TOKEN"}. Every code is listed in Errors and limits.

Settings a sender cannot change

Link to this section

These live in the app and are deliberately out of reach of a request:

Table, scrolls horizontally when narrow
Setting Effect
Source name Shown in the push and saved with each alert as it was at the time.
Source sound Picks the notification sound for that source.
Push notifications When off, alerts are still stored; only the phone push is skipped.
Notification sound A global switch; when off, every source is silent.
Message previews When off, the push and Zona’s in-app banner show generic text, and the full alert stays in the inbox.
In-app banners While Zona is open, shows a new alert as a banner at the top of the screen. On by default and saved on each phone.