Skip to main content

Sending

There are two ways to get a push out. They share content, delivery options, the worker, and the report; they differ in who they address and when.

TransactionalCampaign
EndpointPOST /v1/notificationsWizard, or POST /v1/campaigns then POST /v1/campaigns/:id/send
Addressed byspecific users / devices, a segment, or everyonea segment or everyone
Timingimmediatelynow, at a time, at the user's local time, or recurring
Status202 with a campaign id, transactional: truedraft → scheduled → sending → sent
A/Bnoyes (not for silent sends)
Typical useorder shipped, new message, password resetpromotions, announcements, re-engagement

Both write a campaign row: the tracking id in every push embeds a campaignId, so a transactional send needs somewhere for its delivered/opened counters to land. It shows up in the dashboard list and has a full report.

Transactional — POST /v1/notifications

curl -X POST https://push.example.com/v1/notifications \
-H "Authorization: Bearer sk_live_…" \
-H "content-type: application/json" \
-H "Idempotency-Key: order-48213-shipped" \
-d '{
"target": { "externalIds": ["user_123"] },
"content": {
"en": { "title": "Your order is on its way", "body": "Order #48213 ships today.", "url": "acme://orders/48213" },
"tr": { "title": "Siparişin yola çıktı", "body": "#48213 bugün kargoda.", "url": "acme://orders/48213" },
"_default": "en"
},
"data": { "screen": "order", "id": "48213" },
"collapseId": "order-48213",
"ttl": 86400,
"bypass": { "quietHours": true }
}'

202 { "id": "…", "status": "sending" }. The API validates, writes the campaign, enqueues a fan-out job, and returns — delivery happens in the worker.

Targets — exactly one of

SelectorReachesLimit
externalIds: ["user_123", …]every opted-in device of those users1 000 ids
userIds: ["<ObjectId>", …]same, by our ids1 000
subscriptionIds: ["<ObjectId>", …]those exact devices1 000
segment: { … }devices matching the segment DSL
segmentId: "<ObjectId>"a saved segment; its definition is copied at send time
all: trueevery opted-in device

Unknown external ids are silently skipped (there is nothing to send to). The response is always 202 — inspect /v1/campaigns/:id/stats for what actually happened.

Delivery fields (both kinds)

FieldDefaultMeaning
contentone of content / templateId / silentPer-locale copy. See Content.
templateIdTake copy and delivery defaults from a template. Fields given in the request override the template's.
silentfalseBackground push: nothing is shown, the app is woken with data. No content needed. See Silent pushes.
dataFlat string → string map delivered with the push.
ttl259200 (3 days)Seconds the push service keeps trying while the device is offline. Max 4 weeks.
collapseIdNewer push with the same id replaces an undelivered older one.
priority"high"high → APNs priority 10, FCM high. normal → 5 / normal. Delivery urgency only; see presentation.interruptionLevel for how loudly iOS shows it.
presentationsound, badge, threadId, icon, interruptionLevel, relevanceScore, channelId. See Content.
bypass{ frequencyCap?: true, quietHours?: true } — skip a delivery rule.

Precedence when a template is used: request field → template field → default.

Campaigns — API

# 1. create (draft)
curl -X POST https://push.example.com/v1/campaigns \
-H "Authorization: Bearer sk_live_…" -H "content-type: application/json" \
-d '{
"name": "Weekend flash sale",
"content": { "en": { "title": "Weekend flash sale", "body": "30% off until Sunday." }, "_default": "en" },
"segment": { "and": [
{ "field": "tags.marketingOptIn", "op": "eq", "value": true },
{ "field": "lastActiveAt", "op": "gt", "value": "-30d" }
]},
"schedule": { "type": "user_timezone", "localTime": "19:30" }
}'
# 201 { "id": "66f1…", "status": "draft" }

# 2. send (or schedule)
curl -X POST https://push.example.com/v1/campaigns/66f1…/send \
-H "Authorization: Bearer sk_live_…"
# 202 { "id": "66f1…", "status": "scheduled" }

# 3. cancel while still scheduled
curl -X POST https://push.example.com/v1/campaigns/66f1…/cancel -H "Authorization: Bearer sk_live_…"

# 4. stats
curl https://push.example.com/v1/campaigns/66f1…/stats -H "Authorization: Bearer sk_live_…"

schedule is { "type": "now" } (default), { "type": "at", "at": "2026-10-01T09:00:00Z" }, { "type": "user_timezone", "localTime": "HH:MM" } or { "type": "recurring", … }. send on a now campaign queues immediately; at and user_timezone become scheduled; recurring becomes active and spawns a run per tick. See Scheduling.

A campaign without a segment reaches everyone. segmentId targets a saved segment (not together with segment); templateId starts from a template.

Campaigns — dashboard

The four-step wizard is documented in Dashboard › Campaigns. It produces exactly the same document the API does; the Generated DSL panel in step 1 shows the segment JSON you would put in a POST /v1/campaigns body.

What happens after 202

fan-out job ──▶ count audience ──▶ cursor over opted-in, non-invalidated subscriptions
──▶ for each device: pick locale, pick A/B variant, compute notBefore (user tz)
──▶ one job per device on push-ios / push-android / push-web
worker ──▶ delivery rules (cap, quiet hours) ──▶ personalise ──▶ transport
──▶ event (sent | failed | skipped) + campaign counters + webhook

stats.queued is set when fan-out completes; the other counters move as pushes go through. campaign.completed fires at the end of fan-out — every push is queued, not necessarily sent yet.

Rate limits

/v1/notifications is limited per API key (SEND_RATE_LIMIT, 100/min by default). One call to a 1 000-user externalIds target counts once. For high-volume transactional traffic batch ids per call, or raise the limit.