Push Scheduling
How Amba runs scheduled pushes — delays, retries, idempotency, and delivery windows.
When you schedule a push, Amba takes care of waiting until the right moment, retrying transient failures, and recording per-token delivery results. You don't manage timers or job queues — schedule the campaign and Amba handles delivery.
What Amba does for a scheduled campaign
When you POST a campaign with scheduled_at, or call /send, the API:
- Atomically marks the campaign row (
scheduledorsending). - Schedules delivery on the campaign's
scheduled_at. - Returns to the caller. The rest happens in the background.
When the scheduled time arrives:
- The target segment is resolved.
- Active push tokens for matching users are fetched.
- Users are sent in batches.
- A per-token delivery record is written with status, provider message id, and error detail on failure.
- The campaign is marked
sent.
Trying to schedule the same campaign twice raises 409 ALREADY_SCHEDULED — delivery is idempotent and double-dispatch is impossible.
Scheduling with scheduled_at
scheduled_at is a UTC ISO-8601 timestamp. The API rejects past and unparseable values:
The campaign row is inserted with status = 'scheduled'. Amba fires delivery at the caller's intended time without drift from API-side latency.
Cancelling a scheduled push
There is no DELETE /admin/push/:id today. To kill a scheduled campaign before it fires, contact support. The row stays at scheduled; you can flip it manually if needed.
Trying to /send a scheduled campaign returns 409 ALREADY_SCHEDULED — delivery is already on the way.
Retries and idempotency
Delivery is retried automatically:
- Transient APNs / FCM 5xx → retried with backoff, bounded by max-attempts.
BadDeviceToken/Unregistered→ the token is marked inactive, no retry (permanent).- Workflow-level failures roll the campaign status to
failedso operators see it.
Replaying /send on a failed campaign re-attempts delivery cleanly — the campaign id makes sends idempotent.
Per-project fan-out
Every campaign runs against exactly one project's data. There is no cross-project query. Hot projects are kept in memory so repeated campaigns don't pay a cold-start cost.
Per-user local time (delivery_mode: "local_time")
Push at "09:00" in each user's timezone, not 09:00 UTC. The classic morning-reminder use case for utility apps with a global audience.
What happens:
- The campaign is created with
delivery_mode='local_time',local_date,local_time. No absolutescheduled_atis set. - Delivery starts immediately. Amba groups recipients by their
timezone(recipients without one default toUTC). - For each timezone bucket, the UTC instant that corresponds to
local_date + local_timein that zone is computed (DST-correct). - Delivery waits until each bucket's UTC fire-time, then sends in batches of 500. Buckets whose fire-time is already past send immediately — so a "today, 09:00 local" campaign still reaches UTC-12 users whose 09:00 has passed.
- Stats accumulate across buckets and the campaign is marked
sentonce every bucket has finished.
Mixing scheduled_at with delivery_mode='local_time' is rejected with 400 CONFLICTING_SCHEDULE — pick one or the other.
Setting the user's timezone
Clients populate the value three ways:
- Explicit
PATCH /client/users/me { "timezone": "America/New_York" }. The API validates the IANA name and rejects bogus zones with400 INVALID_TIMEZONE. - Auto-detection: any
GET /client/users/merequest that includesTime-Zone(RFC 7231) orX-Amba-Timezoneheaders backfills the value when the user hasn't set one. Existing values are never overwritten by header detection. - Server-side updates via the admin API or your own backend.
Users without a timezone are bucketed as UTC so they still receive the campaign — just not in their local time.
Delivery windows
There is no "quiet hours" feature at the protocol layer. For per-user local delivery, use delivery_mode: "local_time" (above). For a hard daily window across the whole campaign, schedule per-timezone via repeated delivery_mode='absolute' campaigns.
Observability
Per-token delivery records carry status (sent / failed), provider_message_id, and error_message on failure. Query the per-campaign delivery list when you need a user-level audit trail.