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. One row per token is written to push_deliveries 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 inserted with delivery_mode='local_time', local_date, local_time. scheduled_at stays NULL.
  2. A PUSH_CAMPAIGN workflow starts immediately. It groups recipients by app_users.timezone (NULL → UTC).
  3. For each timezone bucket, it computes the UTC instant that corresponds to local_date + local_time in that zone (DST-correct via Intl.DateTimeFormat).
  4. The workflow sleeps 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 app_users.timezone

Clients populate the column three ways:

  • Explicit PATCH /client/users/me { "timezone": "America/New_York" }. The API validates the IANA name (Intl.DateTimeFormat probe) 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 column when it's still NULL. Existing values are never overwritten by header detection.
  • Direct DB UPDATE via the admin API or your own backend.

Users with timezone IS NULL 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 or wait for first-class delivery-windows on the roadmap.

Observability

Per-token delivery rows land in push_deliveries with status (sent / failed), provider_message_id, and error_message on failure. Query that table directly when you need a user-level audit trail.

Next

On this page