Amba

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

POST /v1/admin/projects/:projectId/push/send
 
{
  "app_user_ids": ["4f1c…", "9a02…"],
  "title": "ACE took your crown",
  "body": "Win it back on NEON STACKER",
  "data": { "route": "game/neon-stacker" },
  "idempotency_key": "crown-game_8812-4f1c…"
}
FieldTypeRequiredDescription
app_user_idsuuid[]yes1 to 500 app user ids. A repeated id is sent once.
titlestringyesNotification title.
bodystringyesNotification body.
dataobjectnoYour key-value payload, delivered as described in the payload contract.
idempotency_keystringnoUp to 255 characters. Makes a retry safe: see Idempotency.
rate_limit_per_user_per_hourintegernoPer-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:

export default async function onCrownLost(request: Request, env: Env) {
  const { game_id, winner_name, game_title, dethroned_user_id } = await request.json();
  const res = await fetch(
    `${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/push/send`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        app_user_ids: [dethroned_user_id],
        title: `${winner_name} took your crown`,
        body: `Win it back on ${game_title}`,
        data: { route: `game/${game_id}` },
        idempotency_key: `crown-${game_id}-${dethroned_user_id}`,
      }),
    },
  );
  const { data } = await res.json();
  return Response.json({ sent: data.summary.sent });
}

Response

The call answers 200 once every device has been tried, and reports each user:

{
  "data": {
    "idempotency_key": "crown-game_8812-4f1c…",
    "replayed": false,
    "rate_limit": { "max_per_user": 10, "window_seconds": 3600 },
    "summary": {
      "users": 2,
      "sent": 1,
      "no_active_tokens": 1,
      "rate_limited": 0,
      "failed": 0,
      "devices_sent": 2,
      "devices_failed": 0
    },
    "results": [
      { "app_user_id": "4f1c…", "status": "sent", "devices_sent": 2, "devices_failed": 0 },
      {
        "app_user_id": "9a02…",
        "status": "no_active_tokens",
        "devices_sent": 0,
        "devices_failed": 0
      }
    ]
  }
}
statusMeaning
sentAt least one of the user's devices accepted the push. devices_failed counts the user's other devices that refused it.
no_active_tokensThe user has no registered device (or the id matches no user). Nothing was sent and the user's rate limit is unchanged.
rate_limitedThe user already received the per-user maximum in the last hour. Nothing was sent.
failedEvery 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: true and 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

StatusCodeWhen
400MISSING_APP_USER_IDS, EMPTY_APP_USER_IDSapp_user_ids is missing or empty.
400TOO_MANY_APP_USER_IDSMore than 500 ids. Split the list across calls, or use a campaign.
400INVALID_APP_USER_IDSapp_user_ids is not an array, or an entry is not a UUID.
400MISSING_TITLE, MISSING_BODYtitle or body is missing or blank.
400INVALID_DATA, RESERVED_DATA_KEYdata is not an object, or uses a key the providers reserve: aps, from, message_type, or a key starting with google or gcm.
400PAYLOAD_TOO_LARGEtitle, body and data encode to more than 4096 bytes, the APNs and FCM limit.
400INVALID_IDEMPOTENCY_KEY, INVALID_RATE_LIMITOut-of-range values.
402FREE_TIER_LIMIT_REACHEDFree 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).
409IDEMPOTENCY_KEY_CONFLICT, SEND_IN_PROGRESSSee Idempotency.
503PUSH_SEND_UNAVAILABLEThe 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:

amba_push_send_to_users({
  project_id: "proj_xxx",
  app_user_ids: ["4f1c…"],
  title: "You received a gift",
  body: "Mia sent you 50 coins",
  data: { route: "inbox/gifts" },
  idempotency_key: "gift-g_311"
})

On this page