Amba

Push

Create and manage push campaigns — APNs + FCM delivery via workflows.

Push campaigns target either a segment or all users. Scheduled campaigns kick off a PUSH_CAMPAIGN workflow with startDelay so delivery fires at the intended time regardless of API uptime.

Notification data payload

Every send accepts an optional data object — arbitrary key/value pairs that ride alongside the visible title/body and are handed to your app when the notification is delivered or tapped. The full field-by-field on-device envelope (both platforms, annotated JSON) is documented in the push payload contract. How it reaches the device:

  • iOS (APNs): your data keys are placed at the top level of the APNs payload, as siblings of the reserved aps block (not nested inside it). The key aps is reserved by Apple; if you include it in data it is dropped and a warning is logged, so the notification envelope is never clobbered.
  • Android (FCM): your data keys are delivered in the FCM message's data field. FCM requires string values, so non-string values are coerced — numbers/booleans/objects are JSON-stringified. On Android, read every data value as a string and parse it yourself if it was an object.

Amba does not inject any fields into the device data payload — what you send is exactly what arrives (plus the coercion above). Campaign and delivery identifiers are recorded server-side on push_deliveries for analytics; they are not added to the pushed data.

There is no enforced schema for data, but the recommended convention for routing a notification tap is a single deep_link key holding an app route or universal link:

{
  "title": "Your chapter is ready",
  "body": "Tap to listen",
  "data": { "deep_link": "amba://chapter/42" }
}

Your client reads response.notification.data.deep_link in the tap handler and navigates. Pick one key and use it consistently across campaigns; mixing deep_link, route, and type across sends forces the client to handle all of them. See the SDK push guide for the client-side handler.

Endpoints

MethodPathDescription
POST/admin/projects/:projectId/push/campaignsCreate a campaign (draft or scheduled).
GET/admin/projects/:projectId/push/campaignsList campaigns.
GET/admin/projects/:projectId/push/campaigns/:campaignIdFetch a single campaign.
PATCH/admin/projects/:projectId/push/campaigns/:campaignIdUpdate a draft / scheduled campaign.
DELETE/admin/projects/:projectId/push/campaigns/:campaignIdCancel + hard-delete a draft / scheduled campaign.
POST/admin/projects/:projectId/push/campaigns/:campaignId/sendTrigger an immediate send.
POST/admin/projects/:projectId/push/campaigns/testSend a one-off test push (no campaign row).
POST/admin/projects/:projectId/users/:userId/push-scheduleConvenience wrapper: schedule a push for a single user at a future time.

POST /admin/projects/:projectId/push/campaigns

Create a push campaign. If scheduled_at is provided, a workflow is started with the delay and the row is stored as scheduled; otherwise the row is draft.

Request

FieldTypeRequiredDescription
namestringnoCampaign name (defaults to title).
titlestringyesNotification title.
bodystringyesNotification body.
dataobjectnoCustom key/value payload delivered to the device. See Notification data payload.
segment_iduuidnoTarget a specific segment; omit to target all users.
delivery_mode"absolute" | "local_time"noDefaults to absolute. See "Local-time mode" below.
scheduled_atISO-8601conditionalRequired UTC instant for absolute mode. Must be parseable and in the future.
local_dateYYYY-MM-DDconditionalRequired when delivery_mode='local_time'.
local_timeHH:MM[:SS]conditionalRequired when delivery_mode='local_time'.

Local-time mode

When delivery_mode='local_time', local_date + local_time are interpreted in each recipient's timezone (recipients without one default to UTC). Amba groups recipients by timezone and fires one delivery per bucket so 09:00 in New York is genuinely 09:00 in New York. scheduled_at MUST be omitted — sending both returns 400 CONFLICTING_SCHEDULE. Conversely, local_date / local_time may not appear when delivery_mode='absolute'.

Response 201

{
  "data": {
    "id": "…",
    "name": "…",
    "title": "…",
    "body": "…",
    "data": {},
    "segment_id": null,
    "status": "scheduled",
    "scheduled_at": "…",
    "sent_at": null,
    "created_at": "…"
  }
}

If the workflow start fails, the row is transitioned to failed and the response reflects that (the handler never returns a stale scheduled).

Errors

  • 400 INVALID_DELIVERY_MODEdelivery_mode is not absolute or local_time.
  • 400 INVALID_SCHEDULED_ATscheduled_at is not a parseable ISO-8601 string.
  • 400 SCHEDULED_AT_IN_PASTscheduled_at is already past.
  • 400 INVALID_LOCAL_DATElocal_date is not a YYYY-MM-DD string (local_time mode).
  • 400 INVALID_LOCAL_TIMElocal_time is not an HH:MM[:SS] string (local_time mode).
  • 400 CONFLICTING_SCHEDULE — mixing scheduled_at with localtime mode, or local* fields with absolute mode.
  • 500 CREATE_FAILED.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/admin/projects/{projectId}/push/campaigns' \
  -H 'Authorization: Bearer ${DEV_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'

GET /admin/projects/:projectId/push/campaigns

List all campaigns for the project, newest first.

Response 200

{
  "data": [
    {
      "id": "…",
      "name": "…",
      "title": "…",
      "body": "…",
      "status": "sent",
      "scheduled_at": "…",
      "sent_at": "…",
      "stats": { "sent": 1200, "delivered": 1180, "opened": 340, "failed": 20 },
      "created_at": "…"
    }
  ]
}

Errors

  • 500 LIST_FAILED.

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET '${BASE_URL}/admin/projects/{projectId}/push/campaigns' \
  -H 'Authorization: Bearer ${DEV_TOKEN}'

GET /admin/projects/:projectId/push/campaigns/:campaignId

Fetch a single campaign.

Response 200

Same shape as an entry in the list response.

Errors

  • 404 NOT_FOUND.
  • 500 FETCH_FAILED.

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET '${BASE_URL}/admin/projects/{projectId}/push/campaigns/{campaignId}' \
  -H 'Authorization: Bearer ${DEV_TOKEN}'

POST /admin/projects/:projectId/push/campaigns/:campaignId/send

Atomically transition status to sending and kick off the PUSH_CAMPAIGN workflow. Refuses to send a campaign that is already sending, sent, or scheduled (a scheduled campaign's delayed workflow is already queued).

Response 200

{ "data": { "status": "sending", "campaign_id": "…" } }

Errors

  • 404 NOT_FOUND — campaign does not exist.
  • 409 ALREADY_SCHEDULED — campaign is already scheduled; wait for it to fire or terminate the push-campaign-<campaignId> workflow in the the Amba support channel.
  • 409 ALREADY_SENT — campaign is already sending or sent.
  • 500 SEND_FAILED — workflow start failed; campaign rolled back to failed.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D/send
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D/send'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/admin/projects/{projectId}/push/campaigns/{campaignId}/send' \
  -H 'Authorization: Bearer ${DEV_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'

POST /admin/projects/:projectId/push/campaigns/test

Send a one-off test push without creating a campaign. Exists so developers can verify APNs / FCM credentials.

Request

FieldTypeRequiredDescription
titlestringyesTest title.
bodystringyesTest body.
app_user_iduuidconditionalResolve to the user's most recent active push token. Required unless device_token+provider are supplied.
device_tokenstringconditionalExplicit APNs/FCM token. If supplied, provider must also be set.
provider"apns" | "fcm"conditionalRequired when device_token is set.
dataobjectnoCustom data payload.

Response 200

{
  "data": {
    "sent": true,
    "delivery_id": "…",
    "provider": "apns",
    "provider_message_id": "…"
  }
}

Errors

  • 400 INVALID_BODY — not valid JSON.
  • 400 MISSING_TITLE / MISSING_BODY — required fields missing.
  • 400 MISSING_TARGET — neither app_user_id nor device_token.
  • 400 MISSING_PROVIDERdevice_token set but provider missing.
  • 404 NO_ACTIVE_TOKENapp_user_id has no active push token.
  • 400 NO_TOKEN — could not resolve a token.
  • 502 SEND_FAILED / PUSH_FAILED — the push provider rejected the send. error.details includes delivery_id, provider, provider_message_id, invalid_token.
  • 500 PUSH_TEST_FAILED.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/test
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/test'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/admin/projects/{projectId}/push/campaigns/test' \
  -H 'Authorization: Bearer ${DEV_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'

PATCH /admin/projects/:projectId/push/campaigns/:campaignId

Update a campaign before send. Only draft and scheduled campaigns are mutable — anything past that (sending, sent, cancelled, failed) returns 409 ALREADY_SENT. If scheduled_at changes on a scheduled campaign, the existing workflow is terminated and re-started with the new startDelay so the campaign fires at the new time. Setting scheduled_at on a draft promotes it to scheduled.

At least one editable field must be supplied; otherwise the handler returns 400 EMPTY_UPDATE.

Request

FieldTypeDescription
titlestringNew notification title.
bodystringNew notification body.
dataobjectNew custom data payload.
segment_iduuid | nullNew target segment (or null to broadcast to all users).
scheduled_atISO-8601New send time. Must be in the future.

Response 200

{
  "data": {
    "id": "…",
    "name": "…",
    "title": "…",
    "body": "…",
    "data": {},
    "segment_id": null,
    "status": "scheduled",
    "scheduled_at": "…",
    "updated_at": "…"
  }
}

Errors

  • 400 INVALID_BODY — body is not JSON.
  • 400 INVALID_DATAdata is not an object.
  • 400 INVALID_SEGMENT_IDsegment_id is not a string or null.
  • 400 INVALID_SCHEDULED_ATscheduled_at is not a parseable ISO-8601 string.
  • 400 SCHEDULED_AT_IN_PASTscheduled_at is already past.
  • 400 EMPTY_UPDATE — no editable fields supplied.
  • 400 INVALID_CAMPAIGN_IDcampaignId is not a UUID.
  • 404 NOT_FOUND — campaign does not exist.
  • 409 ALREADY_SENT — campaign is past draft/scheduled (status is included in the message).
  • 502 RESCHEDULE_FAILED — the row updated but the workflow could not be restarted; the campaign is rolled back to failed.
  • 500 UPDATE_FAILED.

Try it:

PATCH/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D
developer auth
curl -X PATCH 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X PATCH '${BASE_URL}/admin/projects/{projectId}/push/campaigns/{campaignId}' \
  -H 'Authorization: Bearer ${DEV_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'

POST /admin/projects/:projectId/users/:userId/push-schedule

Convenience wrapper for the most common scheduling case: "send one push to one user at one specific time." Amba creates an ephemeral system segment containing only userId and schedules a campaign targeted at that segment for delivery at fire_at, regardless of API uptime in the interim.

The created segment is named __user_<userId>_<campaignId> and flagged as system-internal so segment-list UIs can identify and skip it. Two scheduled pushes for the same user create two distinct segments + campaigns — there is no implicit dedupe.

When to use this vs. a regular campaign: if you need timezone-aware delivery (e.g. "fire at 9am in each user's local time"), use a regular POST /push/campaigns call with delivery_mode='local_time' instead. This endpoint is for absolute-time scheduling only.

On failure: if scheduling fails (or any step after the inserts errors), the ephemeral segment, its membership, and the campaign are deleted as compensation so no orphan __user_* segments accumulate.

Request

FieldTypeRequiredDescription
fire_atISO-8601yesFuture delivery time. Must be parseable and in the future.
titlestringyesNotification title.
bodystringyesNotification body.
dataobjectnoCustom key/value payload delivered to the device. See Notification data payload.

Response 201

{
  "data": {
    "campaign_id": "…",
    "segment_id": "…",
    "fire_at": "2026-05-12T13:00:00-05:00"
  }
}

Errors

  • 400 INVALID_BODY — body is not JSON.
  • 400 INVALID_FIRE_ATfire_at is missing or not a parseable ISO-8601 string.
  • 400 FIRE_AT_IN_PASTfire_at is already past.
  • 400 MISSING_TITLE / MISSING_BODY — required fields missing.
  • 400 INVALID_DATAdata is not an object.
  • 400 INVALID_USER_IDuserId is not a UUID.
  • 404 USER_NOT_FOUNDuserId does not exist in this project's your database.
  • 500 SCHEDULE_FAILED — segment + campaign rolled back after a workflow-start (or other downstream) failure.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/users/%7B%7BuserId%7D%7D/push-schedule
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/users/%7B%7BuserId%7D%7D/push-schedule'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/admin/projects/{projectId}/users/{userId}/push-schedule' \
  -H 'Authorization: Bearer ${DEV_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{
    "fire_at": "2026-05-12T13:00:00-05:00",
    "title": "Reminder",
    "body": "Time to log today!",
    "data": { "deep_link": "amba://streak" }
  }'

DELETE /admin/projects/:projectId/push/campaigns/:campaignId

Cancel and hard-delete a campaign. Same ALREADY_SENT guard as PATCH — only draft and scheduled campaigns can be deleted. Any pending workflow is terminated as part of the delete; if the worker is unreachable the row is still removed (the orphaned workflow is logged as a warning).

Response 204

No body.

Errors

  • 400 INVALID_CAMPAIGN_IDcampaignId is not a UUID.
  • 404 NOT_FOUND — campaign does not exist.
  • 409 ALREADY_SENT — campaign is past draft/scheduled.
  • 500 DELETE_FAILED.

Try it:

DELETE/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/push/campaigns/%7B%7BcampaignId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE '${BASE_URL}/admin/projects/{projectId}/push/campaigns/{campaignId}' \
  -H 'Authorization: Bearer ${DEV_TOKEN}'