Accept a notification from the authenticated source
POST/functions/v1/notify
https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notifyThe 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
| Header | Value | Description |
|---|---|---|
Authorizationrequired | Bearer zona_live_SOURCE_TOKEN | Independent one-time-issued source credential. |
Idempotency-Keyrequired | stringlength 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 |
Content-Typerequired | application/jsonmultipart/form-data | Media type of the request body. For multipart requests, let the HTTP library set the boundary. |
Request body
Schema NotifyRequest
titlestringrequiredWhitespace-trimmed title.
length
1–120bodystringrequiredWhitespace-trimmed message.
length
1–2000categorystring or nulloptionallength
1–80severitystring or nulloptionalOptional visual urgency. Null or omitted uses the active theme's neutral inbox style.
one of
lowmediumhighcriticalnulldataobjectoptionalJSON 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 nulloptionalOptional checklist delivered with the alert. Each entry is either a plain string or an object with
textand an optionalid. Ids must match^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$and be unique; omitted ids are assigned positionally asi1,i2, and so on. Completion is recipient state, so an entry carryingdone_atis rejected withINVALID_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 with403 TODO_DISABLEDwhen the operator switch is off.max items
20Each item
Option 1: stringstringlength
1–200Option 2: objectidstringoptionallength
1–64textstringrequiredlength
1–200
No other fields are accepted.
{
"title": "Build complete",
"body": "The release build finished successfully.",
"category": "build",
"severity": "high",
"data": {
"eventId": "build-2026-07-20-14"
}
}Schema NotifyMultipartRequest
titlestringrequiredWhitespace-trimmed title.
length
1–120bodystringrequiredWhitespace-trimmed message.
length
1–2000categorystringoptionallength
1–80severitystringoptionalone of
lowmediumhighcriticaldatastringoptionalJSON-encoded object, at most 4096 UTF-8 bytes when serialized.
todostringoptionalJSON-encoded array of checklist entries, at most 20, matching the JSON body's
todofield.attachmentarray of string (binary)optionalOptional evidence images, one
attachmentpart per file, up to five. Only magic bytes are trusted: PNG, JPEG, and WebP are accepted; anything else (including SVG and renamed executables) is rejected withINVALID_PAYLOAD. The set shares the server-resolved byte budget (5 MiB for the standard plan by default): the sum of the images must fit the plan limit, and the whole multipart request is limited to that budget plus 64 KiB of form overhead.max items
5
- Part
data application/json- Part
attachment image/pngimage/jpegimage/webp
curl --request POST \
'https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify' \
--header "Authorization: Bearer $ZONA_SOURCE_TOKEN" \
--header 'Idempotency-Key: build-2026-07-20-14' \
--form 'title=Build complete' \
--form 'body=The release build finished successfully.' \
--form 'category=build' \
--form 'severity=high' \
--form 'data={"eventId":"build-2026-07-20-14"}' \
--form 'attachment=@first.png' \
--form 'attachment=@second.png'Request samples
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"
}
}'const response = await fetch('https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZONA_SOURCE_TOKEN}`,
'Idempotency-Key': 'build-2026-07-20-14',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Build complete',
body: 'The release build finished successfully.',
category: 'build',
severity: 'high',
data: {
eventId: 'build-2026-07-20-14',
},
}),
});
const result = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${result.error}`);
console.log(result);import os
import requests
response = requests.post(
"https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify",
headers={
"Authorization": f"Bearer {os.environ['ZONA_SOURCE_TOKEN']}",
"Idempotency-Key": "build-2026-07-20-14",
},
json={
"title": "Build complete",
"body": "The release build finished successfully.",
"category": "build",
"severity": "high",
"data": {
"eventId": "build-2026-07-20-14",
},
},
timeout=10,
)
response.raise_for_status()
print(response.json())$headers = @{
Authorization = "Bearer $env:ZONA_SOURCE_TOKEN"
'Idempotency-Key' = 'build-2026-07-20-14'
}
$body = @'
{
"title": "Build complete",
"body": "The release build finished successfully.",
"category": "build",
"severity": "high",
"data": {
"eventId": "build-2026-07-20-14"
}
}
'@
Invoke-RestMethod `
-Method Post `
-Uri 'https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify' `
-Headers $headers `
-ContentType 'application/json; charset=utf-8' `
-Body $body `
-TimeoutSec 10Responses
200Idempotent replay: this source already sent the same
Idempotency-Keywith an identical payload.The stored notification is returned; no duplicate is created and push is not re-attempted.
Headers
Cache-Controlstring- always
no-store
Body
application/json·NotifyAcceptednotificationIdstring (uuid)requiredsourceIdstring (uuid)requiredsourceNamestringrequiredlength
1–80acceptedAtstring (date-time)requiredidempotentReplaybooleanrequiredTrue when an existing record was replayed instead of newly accepted.
attachmentAcceptedbooleanrequiredTrue when every sent evidence image is stored for this notification.
attachmentErrorstring or nullrequiredSanitized failure code (UPLOAD_FAILED) when a sent image set could not be stored.
pushAttemptedintegerrequiredCompatibility 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
0pushAcceptedintegerrequiredCompatibility field retained for older clients.
notifyreturns zero because Expo ticket and receipt processing is asynchronous.minimum
0pushQueuedintegeroptionalDurable 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-Controlstring- always
no-store
Body
application/json·NotifyAcceptednotificationIdstring (uuid)requiredsourceIdstring (uuid)requiredsourceNamestringrequiredlength
1–80acceptedAtstring (date-time)requiredidempotentReplaybooleanrequiredTrue when an existing record was replayed instead of newly accepted.
attachmentAcceptedbooleanrequiredTrue when every sent evidence image is stored for this notification.
attachmentErrorstring or nullrequiredSanitized failure code (UPLOAD_FAILED) when a sent image set could not be stored.
pushAttemptedintegerrequiredCompatibility 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
0pushAcceptedintegerrequiredCompatibility field retained for older clients.
notifyreturns zero because Expo ticket and receipt processing is asynchronous.minimum
0pushQueuedintegeroptionalDurable 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_KEYBody
application/json·Errorerrorstringrequired
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_TOKENBody
application/json·Errorerrorstringrequired
No other fields are accepted.
Example { "error": "INVALID_TOKEN" }403A server control currently rejects this optional capability.
ATTACHMENTS_DISABLEDCRITICAL_SEVERITY_DISABLEDTODO_DISABLEDBody
application/json·Errorerrorstringrequired
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_ALLOWEDBody
application/json·Errorerrorstringrequired
No other fields are accepted.
Example { "error": "METHOD_NOT_ALLOWED" }409The
Idempotency-Keywas already used by this source with a different payload.IDEMPOTENCY_CONFLICTBody
application/json·Errorerrorstringrequired
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_LARGEBody
application/json·Errorerrorstringrequired
No other fields are accepted.
Example { "error": "PAYLOAD_TOO_LARGE" }423The owning account is deleted, deleting, suspended, or otherwise inactive.
ACCOUNT_INACTIVEBody
application/json·Errorerrorstringrequired
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_LIMITEDOperator-configured plan limits may be lower or higher where documented.
Headers
Retry-Afterinteger- Minimum retry delay in seconds.minimum
1example60
Body
application/json·Errorerrorstringrequired
No other fields are accepted.
Example · sourceLimited { "error": "RATE_LIMITED" }Example · accountLimited { "error": "ACCOUNT_RATE_LIMITED" }500The request was not confirmed as accepted.
INTERNAL_ERRORBody
application/json·Errorerrorstringrequired
No other fields are accepted.
Example { "error": "INTERNAL_ERROR" }503A fail-closed operator switch temporarily paused this operation.
SERVICE_UNAVAILABLEHeaders
Retry-Afterinteger- Minimum retry delay in seconds.minimum
1example60
Body
application/json·Errorerrorstringrequired
No other fields are accepted.
Example { "error": "SERVICE_UNAVAILABLE" }