Quick links

Idempotency and retries

Pick one stable Idempotency-Key per event, retry only what is safe to retry, and honour Retry-After, so a flaky network never sends the same alert twice.

Networks drop responses. A request can time out on your side after Zona has already stored the alert. The Idempotency-Key header is what lets you retry that request without a second alert appearing on your phone.

Idempotency-Key is required on every request. It must be 8 to 128 characters and match:

^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$

A missing or malformed key returns 400 INVALID_IDEMPOTENCY_KEY.

A key names one logical event, not one HTTP request. Good keys come from the event itself:

Table, scrolls horizontally when narrow
Event Key
The 02:00 backup on 26 July 2026 backup-20260726-020000
A CI run and its attempt number ci-8841920731-1
A health check going down health-orders-api-down- followed by a UUID created when the state changed

Create the key once, when the event happens, and keep it until you have a definite answer. A key created fresh inside the retry loop defeats the purpose: each attempt looks like a new event.

What Zona does with a key

Link to this section

Keys are scoped to the source that sends them. The same key from two different sources is two independent events.

Table, scrolls horizontally when narrow
Request Result
First request with a key 202. A new inbox record is stored and delivery jobs are queued for eligible phones.
Same source, same key, same content 200. The original record comes back with idempotentReplay: true. Nothing new is queued.
Same source, same key, different content 409 IDEMPOTENCY_CONFLICT. Nothing new is stored.
Different source, same key A separate event, handled as a first request.

“Same content” means the same title, body, category, severity, data, todo and image set. Changing any of them, including only the severity or the order of the images, is a conflict.

A replay returns the original notificationId and acceptedAt. It omits pushQueued, reports pushAttempted: 0, does not retry a failed image upload, does not resend the push, and does not update the key’s last-used time.

Table, scrolls horizontally when narrow
Outcome Retry? How
Network error or timeout Yes Same key, same content. The alert may already be stored; the replay tells you.
429 RATE_LIMITED, 429 ACCOUNT_RATE_LIMITED Yes Wait at least the Retry-After seconds, then retry with the same key.
503 SERVICE_UNAVAILABLE Yes Wait at least the Retry-After seconds, then retry with the same key.
500 INTERNAL_ERROR and other 5xx Yes Exponential backoff with jitter, same key, same content.
409 IDEMPOTENCY_CONFLICT No Send the original content, or use a new key if this really is a new event.
Any other 4xx No Fix the request or the token first. Retrying unchanged gives the same answer.

Use a short client timeout, around 10 seconds for JSON and a little longer for images, so a stuck connection turns into a retry instead of a hung job.

A retry helper

Link to this section

Both helpers take the key from the caller, so every attempt reuses it, and both treat 200 and 202 as success.

import os
import random
import time

import requests

ENDPOINT = "https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify"


def send_alert(payload: dict, event_id: str, attempts: int = 5) -> dict:
    headers = {
        "Authorization": f"Bearer {os.environ['ZONA_SOURCE_TOKEN']}",
        "Idempotency-Key": event_id,
    }
    for attempt in range(attempts):
        wait = min(2 ** attempt, 30) + random.uniform(0, 1)
        try:
            response = requests.post(ENDPOINT, headers=headers, json=payload, timeout=10)
        except (requests.ConnectionError, requests.Timeout):
            response = None

        if response is not None:
            if response.status_code in (200, 202):
                return response.json()
            if response.status_code != 429 and response.status_code < 500:
                raise RuntimeError(f"Zona rejected the alert: {response.status_code} {response.text}")
            retry_after = response.headers.get("Retry-After", "")
            if retry_after.isdigit():
                wait = max(wait, int(retry_after))

        if attempt < attempts - 1:
            time.sleep(wait)
    raise RuntimeError("Zona did not confirm the alert; retry later with the same key")


send_alert(
    {"title": "Backup complete", "body": "The nightly backup finished successfully."},
    event_id="backup-20260726-020000",
)

With curl, --retry covers the same ground for a single command: it retries timeouts and HTTP 408, 429, 500, 502, 503 and 504, resends the same headers and body each time, and waits for Retry-After when the server sends it.

curl --retry 4 --max-time 10 --request POST \
  "https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify" \
  --header "Authorization: Bearer $ZONA_SOURCE_TOKEN" \
  --header "Idempotency-Key: backup-20260726-020000" \
  --header "Content-Type: application/json" \
  --data '{"title": "Backup complete", "body": "The nightly backup finished successfully."}'

If you are unsure whether an alert landed

Link to this section

Send it again with the same key and the same content. A 200 with idempotentReplay: true means it was already stored; a 202 means this attempt stored it. Either way there is exactly one alert.