Quick links

API reference

Send an alert to your phone with one authenticated HTTPS request toPOST /functions/v1/notify. It is the only endpoint a sender calls; sources and keys are managed in the Zona app. All traffic uses TLS.

Contract
Zona notification API 1.1.0
Format
OpenAPI 3.1.0
Endpoint
https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify
Auth
Bearer source token
curl · the whole integration
curl --request POST \
  'https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify' \
  --header "Authorization: Bearer $ZONA_SOURCE_TOKEN" \
  --header 'Idempotency-Key: build-2026-07-20-14' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Build complete",
    "body": "The release build finished successfully.",
    "category": "build",
    "severity": "high",
    "data": {
      "eventId": "build-2026-07-20-14"
    }
  }'

Before you integrate

A 202 response from notify means the durable, retention-bounded inbox record exists. pushQueued counts durable delivery jobs and can be zero when account or source quiet hours are active. A background worker sends queued jobs with bounded retries and checks Expo receipts; neither a queue entry nor a provider receipt proves that the phone displayed an alert.

Every JSON request has a 16 KiB transport limit. notify.data has an additional 4 KiB UTF-8 serialized limit. These byte limits are normative even where JSON Schema cannot express serialized byte size.

Send a notification

One endpoint, authenticated by a source token you create in the app. The server derives the source and the owning account from the token, so a request cannot choose or override either.

Field limits

titlerequired
1–120 characters
bodyrequired
1–2000 characters
category
1–80 characters
severity
low · medium · high · critical
todo
Up to 20 items, 200 characters each
attachment
Up to 5 images, multipart only
Idempotency-Keyrequired
8–128 characters

Accept a notification from the authenticated source

POST/functions/v1/notify

Endpoint URL
https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify

The source and owner are derived from the Bearer credential. Callers cannot provide or override source/owner identity. The inbox record and eligible delivery jobs are durable before the response is returned. Quiet hours suppress only those jobs; the inbox record is still accepted and returned normally.

Idempotency-Key is required. Re-sending the same key with an identical payload returns the stored notification with idempotentReplay: true and does not create a duplicate or retry push. Reusing the key with a different payload is rejected with 409 IDEMPOTENCY_CONFLICT. Optional severity is part of that payload, so changing only severity is also a conflict.

Up to five evidence images (PNG/JPEG/WebP) may be attached by sending multipart/form-data with the attachment part repeated once per image. The whole set shares the account's server-resolved byte budget, and the images participate in idempotency: a single file keeps its own SHA-256, and reusing the key with a different set of images (added, removed, or reordered) is a conflict.

Authentication

  • Authorization: Bearer zona_live_SOURCE_TOKENIndependent one-time-issued source credential.

Headers

HeaderValueDescription
AuthorizationrequiredBearer zona_live_SOURCE_TOKENIndependent one-time-issued source credential.
Idempotency-Keyrequiredstringlength 8–128pattern ^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$example build-2026-07-20-14

Sender-chosen unique event ID. Replays with an identical payload return the original notification; reuse with a different payload is rejected with 409 IDEMPOTENCY_CONFLICT.

Content-Typerequiredapplication/jsonmultipart/form-dataMedia type of the request body. For multipart requests, let the HTTP library set the boundary.

Request body required

Schema NotifyRequest

titlestringrequired

Whitespace-trimmed title.

length 1–120

bodystringrequired

Whitespace-trimmed message.

length 1–2000

categorystring or nulloptional

length 1–80

severitystring or nulloptional

Optional visual urgency. Null or omitted uses the active theme's neutral inbox style.

one of lowmediumhighcriticalnull

dataobjectoptional

JSON object no more than 4096 UTF-8 bytes when serialized. Reserved routing fields in the produced push are controlled by the server.

Free-form object: any keys are accepted.

todoarray of one of or nulloptional

Optional checklist delivered with the alert. Each entry is either a plain string or an object with text and an optional id. Ids must match ^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$ and be unique; omitted ids are assigned positionally as i1, i2, and so on. Completion is recipient state, so an entry carrying done_at is rejected with INVALID_PAYLOAD. The recipient may tick items off or remove them in the app; the sender cannot. Replaying the same idempotency key never rewrites an existing list, so completed items survive a retry. Rejected with 403 TODO_DISABLED when the operator switch is off.

max items 20

Each item

  1. Option 1: string

    stringlength 1–200

  2. Option 2: object

    idstringoptional

    length 1–64

    textstringrequired

    length 1–200

No other fields are accepted.

Example · buildComplete
{
  "title": "Build complete",
  "body": "The release build finished successfully.",
  "category": "build",
  "severity": "high",
  "data": {
    "eventId": "build-2026-07-20-14"
  }
}

Request samples

curl · reads ZONA_SOURCE_TOKEN from the environment
curl --request POST \
  'https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify' \
  --header "Authorization: Bearer $ZONA_SOURCE_TOKEN" \
  --header 'Idempotency-Key: build-2026-07-20-14' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Build complete",
    "body": "The release build finished successfully.",
    "category": "build",
    "severity": "high",
    "data": {
      "eventId": "build-2026-07-20-14"
    }
  }'

Responses

  • 200Idempotent replay: this source already sent the same Idempotency-Key with an identical payload.

    The stored notification is returned; no duplicate is created and push is not re-attempted.

    Headers

    Cache-Control string
    always no-store

    Body application/json · NotifyAccepted

    notificationIdstring (uuid)required
    sourceIdstring (uuid)required
    sourceNamestringrequired

    length 1–80

    acceptedAtstring (date-time)required
    idempotentReplaybooleanrequired

    True when an existing record was replayed instead of newly accepted.

    attachmentAcceptedbooleanrequired

    True when every sent evidence image is stored for this notification.

    attachmentErrorstring or nullrequired

    Sanitized failure code (UPLOAD_FAILED) when a sent image set could not be stored.

    pushAttemptedintegerrequired

    Compatibility alias. For a newly accepted notification this equals pushQueued; for an idempotent replay it is zero. It does not mean that an Expo request has already run.

    minimum 0

    pushAcceptedintegerrequired

    Compatibility field retained for older clients. notify returns zero because Expo ticket and receipt processing is asynchronous.

    minimum 0

    pushQueuedintegeroptional

    Durable delivery jobs created for eligible phones. This is zero while account or source quiet hours are active. Present on a newly accepted notification and omitted on an idempotent replay.

    minimum 0

    No other fields are accepted.

  • 202Inbox record accepted.

    Eligible push delivery jobs are durably queued, or intentionally omitted while quiet hours are active.

    Headers

    Cache-Control string
    always no-store

    Body application/json · NotifyAccepted

    notificationIdstring (uuid)required
    sourceIdstring (uuid)required
    sourceNamestringrequired

    length 1–80

    acceptedAtstring (date-time)required
    idempotentReplaybooleanrequired

    True when an existing record was replayed instead of newly accepted.

    attachmentAcceptedbooleanrequired

    True when every sent evidence image is stored for this notification.

    attachmentErrorstring or nullrequired

    Sanitized failure code (UPLOAD_FAILED) when a sent image set could not be stored.

    pushAttemptedintegerrequired

    Compatibility alias. For a newly accepted notification this equals pushQueued; for an idempotent replay it is zero. It does not mean that an Expo request has already run.

    minimum 0

    pushAcceptedintegerrequired

    Compatibility field retained for older clients. notify returns zero because Expo ticket and receipt processing is asynchronous.

    minimum 0

    pushQueuedintegeroptional

    Durable delivery jobs created for eligible phones. This is zero while account or source quiet hours are active. Present on a newly accepted notification and omitted on an idempotent replay.

    minimum 0

    No other fields are accepted.

  • 400Invalid JSON, content type, field, or size-constrained value.INVALID_PAYLOADINVALID_IDEMPOTENCY_KEY

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example · invalidPayload
    {
      "error": "INVALID_PAYLOAD"
    }
    Example · invalidIdempotencyKey
    {
      "error": "INVALID_IDEMPOTENCY_KEY"
    }
  • 401Source token is absent, malformed, unknown, or revoked.INVALID_TOKEN

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "INVALID_TOKEN"
    }
  • 403A server control currently rejects this optional capability.ATTACHMENTS_DISABLEDCRITICAL_SEVERITY_DISABLEDTODO_DISABLED

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example · attachmentsDisabled
    {
      "error": "ATTACHMENTS_DISABLED"
    }
    Example · criticalSeverityDisabled
    {
      "error": "CRITICAL_SEVERITY_DISABLED"
    }
    Example · todoDisabled
    {
      "error": "TODO_DISABLED"
    }
  • 405Endpoint accepts POST only, except for CORS preflight.METHOD_NOT_ALLOWED

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "METHOD_NOT_ALLOWED"
    }
  • 409The Idempotency-Key was already used by this source with a different payload.IDEMPOTENCY_CONFLICT

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "IDEMPOTENCY_CONFLICT"
    }
  • 413Request body exceeds 16 KiB (JSON) or the plan-resolved attachment budget plus 64 KiB (multipart).PAYLOAD_TOO_LARGE

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "PAYLOAD_TOO_LARGE"
    }
  • 423The owning account is deleted, deleting, suspended, or otherwise inactive.ACCOUNT_INACTIVE

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "ACCOUNT_INACTIVE"
    }
  • 429Rate limit exceeded; currently 60 accepted requests per source or 20 per standard account in the rolling minute.RATE_LIMITEDACCOUNT_RATE_LIMITED

    Operator-configured plan limits may be lower or higher where documented.

    Headers

    Retry-After integer
    Minimum retry delay in seconds.minimum 1example 60

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example · sourceLimited
    {
      "error": "RATE_LIMITED"
    }
    Example · accountLimited
    {
      "error": "ACCOUNT_RATE_LIMITED"
    }
  • 500The request was not confirmed as accepted.INTERNAL_ERROR

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "INTERNAL_ERROR"
    }
  • 503A fail-closed operator switch temporarily paused this operation.SERVICE_UNAVAILABLE

    Headers

    Retry-After integer
    Minimum retry delay in seconds.minimum 1example 60

    Body application/json · Error

    errorstringrequired

    No other fields are accepted.

    Example
    {
      "error": "SERVICE_UNAVAILABLE"
    }

The contract file

This page is generated from openapi.yaml 1.1.0 at build time. If this reference, the contract and the deployed behaviour disagree, that is a defect: tell us at terabasedevteam@gmail.com.