Amba

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:

  1. Atomically marks the campaign row (scheduled or sending).
  2. Schedules delivery on the campaign's scheduled_at.
  3. Returns to the caller. The rest happens in the background.

When the scheduled time arrives:

  1. The target segment is resolved.
  2. Active push tokens for matching users are fetched.
  3. Users are sent in batches.
  4. A per-token delivery record is written with status, provider message id, and error detail on failure.
  5. 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:

POST /admin/push
 
{
  "title": "Morning reminder",
  "body": "Time to check your goals.",
  "scheduled_at": "2026-05-01T13:00:00Z"
}

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 failed so 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.

POST /admin/projects/:projectId/push/campaigns
 
{
  "title": "Morning reminder",
  "body": "Time to check your goals.",
  "delivery_mode": "local_time",
  "local_date": "2026-05-04",
  "local_time": "09:00"
}

What happens:

  1. The campaign is created with delivery_mode='local_time', local_date, local_time. No absolute scheduled_at is set.
  2. Delivery starts immediately. Amba groups recipients by their timezone (recipients without one default to UTC).
  3. For each timezone bucket, the UTC instant that corresponds to local_date + local_time in that zone is computed (DST-correct).
  4. 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.
  5. Stats accumulate across buckets and the campaign is marked sent once 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 with 400 INVALID_TIMEZONE.
  • Auto-detection: any GET /client/users/me request that includes Time-Zone (RFC 7231) or X-Amba-Timezone headers 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.

Next

On this page