Sending push notifications
push is the interruptive delivery layer. It is separate from durable
content, so you can update the in-app UI without buzzing anyone, and send a
follow-up push only when attention is needed.
One thing to swap
The API lives at https://api.roundtrip.sh. Replace ak_xxx with a
workspace-scoped API key. Every channel you target must already exist in the
workspace.
The minimum
A channel and a push.title are enough:
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "deploys",
"push": { "title": "Build #1283 passed" }
}'Channels decide who is notified
You publish to channels, not devices. The HTTP body accepts either a single
channel or a channels array:
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channels": ["deploys", "alerts"],
"push": { "title": "Nightly job finished" }
}'The 200 body has one notifications entry per channel that resolved, and
unknown channels land in errors without failing the rest:
{
"ok": true,
"notifications": [
{ "channel": "deploys", "id": "ntf_1", "deviceCount": 3 },
{ "channel": "alerts", "id": "ntf_2", "deviceCount": 5 }
],
"errors": []
}Push fields
titlestringrequiredThe lock-screen title. The only required push field.
priority"min" | "low" | "normal" | "high" | "critical"optionalHow hard the push tries to break through. Named or numeric 1-5. Defaults
to normal.
bodystringoptionalThe lock-screen body. Set this independently from content.description when
the push should be shorter than the card it opens.
soundbooleanoptionalPlay a sound. Defaults to true; set false for a silent delivery.
badgebooleanoptionalUpdate the app icon badge. Defaults to true.
enabledbooleanoptionalWhen false, no push is sent. The notification still lands in the in-app
feed.
Priority
Priority maps to ntfy-style levels 1-5. Pass the name or the number.
| Name | Number | Behavior |
|---|---|---|
min | 1 | Quietest. No sound, easy to miss by design. |
low | 2 | Low-key. |
normal | 3 | The default if you omit priority. |
high | 4 | Prominent; surfaces above routine notifications. |
critical | 5 | Bypasses quiet hours / Do Not Disturb. |
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "alerts",
"push": { "title": "Disk at 96% on db-primary", "priority": "critical" }
}'Custom push copy
Use push for short lock-screen copy and content for the durable card:
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "billing",
"push": {
"title": "New invoice to review",
"body": "Acme Corp - $48,200"
},
"content": {
"title": "Invoice #2042 - Acme Corp",
"description": "Net-30, due 2026-07-20. Tap to review line items."
}
}'Quiet and silent deliveries
Two quiet modes:
- Silent push: set
push.soundtofalse. - No push at all: set
push.enabledtofalse; the notification still appears in the feed.
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "audit-log",
"push": { "title": "Config synced (no changes)", "enabled": false }
}'Avoid duplicates
Put an idempotencyKey on every HTTP write. For initial sends, derive it from
your source event id:
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "deploys",
"push": { "title": "Build #1283 passed" },
"idempotencyKey": "build-1283-passed"
}'For SDK workflows, use the shorter idempotency option on notification
handles. The SDK adds the notification id, operation, and slot:
await notification.content.replace("decision", decisionUpdate, {
idempotency: incident.decisionVersion,
});See Idempotency for the full retry model.
Follow-up pushes
replace and append update durable UI. They do not send push unless you call
the push endpoint explicitly:
curl -X POST https://api.roundtrip.sh/api/v1/notifications/ntf_1/push \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"push": {
"title": "Rollback decision needed",
"body": "A rollback is ready for approval.",
"priority": "high"
},
"idempotencyKey": "incident-42:rollback-push"
}'The mobile push payload carries notificationId and, for update-specific pushes,
an updateId. Tapping opens the existing notification instead of creating a new
feed item.