Transactional Push
Send one push to specific users from your server code, with a per-user result, a per-user rate limit, and retry-safe idempotency keys.
A transactional push goes to the particular users an event is about: "ACE took your crown on NEON STACKER", "you received a gift", "challenge results are in". Your server code (an Amba function, or any backend holding a server key) names the users, and Amba delivers to every active device each of them has registered, in the same request.
Use a campaign to reach a segment or your whole audience, and a test send to check your APNs and FCM setup.
Send
| Field | Type | Required | Description |
|---|---|---|---|
app_user_ids | uuid[] | yes | 1 to 500 app user ids. A repeated id is sent once. |
title | string | yes | Notification title. |
body | string | yes | Notification body. |
data | object | no | Your key-value payload, delivered as described in the payload contract. |
idempotency_key | string | no | Up to 255 characters. Makes a retry safe: see Idempotency. |
rate_limit_per_user_per_hour | integer | no | Per-user cap for this call, 1 to 100. Defaults to 10. See Rate limit. |
From an Amba function, call it with the project-scoped token every function receives:
Response
The call answers 200 once every device has been tried, and reports each user:
status | Meaning |
|---|---|
sent | At least one of the user's devices accepted the push. devices_failed counts the user's other devices that refused it. |
no_active_tokens | The user has no registered device (or the id matches no user). Nothing was sent and the user's rate limit is unchanged. |
rate_limited | The user already received the per-user maximum in the last hour. Nothing was sent. |
failed | Every device refused the push. error holds the first provider error, such as Unregistered or PROVIDER_NOT_CONFIGURED. |
A device the provider reports as unregistered is marked inactive, so later sends skip it. Every attempt is recorded in the project's push delivery log, and each device fires an engagement.push.delivered or engagement.push.failed webhook event with campaign_id: null.
Per-user rate limit
Each user receives at most 10 transactional pushes per rolling hour. Set rate_limit_per_user_per_hour (1 to 100) on a call to use a different cap for that call: a user is admitted when they received fewer than that many transactional pushes in the last hour. Every call that tries to deliver to a user counts toward that user's hour, whether or not the devices accept it. Calls racing for a user's last slot are serialized, so a user is never admitted past the cap. Campaigns and test sends do not count toward this limit.
Idempotency
Pass an idempotency_key built from the event that triggered the push, such as crown-<gameId>-<userId> or gift-<giftId>. Then a retried function call never notifies anyone twice:
- The same key with the same request (same users in any order, title, body, data and rate limit) returns the first call's response with
replayed: trueand sends nothing. - The same key with a different request answers
409 IDEMPOTENCY_KEY_CONFLICT. - A retry that arrives while the first call is still sending answers
409 SEND_IN_PROGRESS. Retry a few seconds later to read the result.
A key is remembered for 24 hours. Without a key, every call sends.
A call that stops partway through sending (the request was cut off server-side) leaves its key answering SEND_IN_PROGRESS until the key expires, because some of its pushes may already have been delivered. Amba never resends under that key; send again with a new key if the users still need the notification.
Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_APP_USER_IDS, EMPTY_APP_USER_IDS | app_user_ids is missing or empty. |
| 400 | TOO_MANY_APP_USER_IDS | More than 500 ids. Split the list across calls, or use a campaign. |
| 400 | INVALID_APP_USER_IDS | app_user_ids is not an array, or an entry is not a UUID. |
| 400 | MISSING_TITLE, MISSING_BODY | title or body is missing or blank. |
| 400 | INVALID_DATA, RESERVED_DATA_KEY | data is not an object, or uses a key the providers reserve: aps, from, message_type, or a key starting with google or gcm. |
| 400 | PAYLOAD_TOO_LARGE | title, body and data encode to more than 4096 bytes, the APNs and FCM limit. |
| 400 | INVALID_IDEMPOTENCY_KEY, INVALID_RATE_LIMIT | Out-of-range values. |
| 402 | FREE_TIER_LIMIT_REACHED | Free projects: the daily direct-push budget can't cover this call (each user it would send to counts as one; a refused call spends nothing). |
| 409 | IDEMPOTENCY_KEY_CONFLICT, SEND_IN_PROGRESS | See Idempotency. |
| 503 | PUSH_SEND_UNAVAILABLE | The project's database is being upgraded for this feature. Retry after the Retry-After delay. |
From an agent
The MCP tool amba_push_send_to_users takes the same fields: