REST API
Base URL: https://api.notibolt.com/api/v1. Authenticate server-to-server calls with your app's REST API key:
Authorization: Key nb_your_rest_key # also accepted: "Bearer nb_…", "Basic nb_…" or the X-Api-Key header
Using Node.js? The official server SDK wraps this API with typed methods:
npm install notibolt
import Notibolt from 'notibolt';
const notibolt = new Notibolt({ appId: '…', apiKey: 'nb_…' });
await notibolt.notifications.create({ title: 'Hello', body: '👋' });Send a notification
POST /apps/{appId}/notifications{
"title": "Order shipped 📦",
"body": "Arrives Thursday",
"url": "myapp://orders/1234",
"imageUrl": "https://example.com/banner.png",
"data": { "orderId": "1234" },
"segment": {
"platforms": ["IOS", "ANDROID"],
"tags": { "plan": "pro" },
"tagFilters": [{ "key": "score", "op": "gt", "value": "100" }],
"languages": ["tr", "en"],
"countries": ["TR", "DE"],
"lastActiveWithinDays": 30,
"externalUserIds": ["user-42"],
"deviceIds": ["<uuid>"]
},
"scheduledAt": "2026-09-01T09:00:00Z",
"contents": {
"tr": { "title": "Kargon yola çıktı 📦", "body": "Perşembe kapında" },
"en": { "title": "Order shipped 📦", "body": "Arrives Thursday" }
},
"defaultLanguage": "en",
"sound": "default",
"badge": 3,
"androidChannel": "orders",
"interruptionLevel": "time-sensitive",
"collapseId": "order-1234",
"ttlSeconds": 86400,
"contentAvailable": false
}Only title and body are required. Returns the notification with status: "QUEUED" immediately — delivery happens asynchronously.
Field notes
| Field | Behavior |
|---|---|
segment | Omitted → all subscribed devices. All filters combine with AND. tagFilters ops: eq, ne, gt, lt, exists, not_exists, in. |
scheduledAt | Future ISO date → queued until then. Cancel while queued with DELETE /apps/{appId}/notifications/{id}. |
contents | Per-language title/body; each device gets its own language (BCP-47 prefix match), falling back to defaultLanguage, then top-level title/body. |
| Templates | {{externalUserId}}, {{language}} and {{tags.name}} in title/body are filled per-device. |
contentAvailable | Silent push — nothing rendered; only data is delivered (skips the in-app opt-out filter). |
Idempotency-Key header | Same key + same app → returns the original notification instead of sending twice. |
Smart delivery
Set deliveryMode: "timezone" with sendAtLocalTime: "09:00" to deliver when each device's own clock reaches that time (within 24 h, using the timezone the SDK reports). App-level policies configured in the panel also apply at dispatch time: quiet hours (deliveries landing inside the window are held until it ends, in each device's local time) and a daily frequency cap (devices over the limit are skipped). Silent pushes are exempt from both.
{
"title": "Good morning",
"body": "Your daily digest is ready",
"deliveryMode": "timezone",
"sendAtLocalTime": "09:00"
}A/B tests
A slice of the audience (deterministic device-hash buckets) splits across 2–4 variants; after winnerDelayMinutes the variant with the best open rate is sent to the remaining audience automatically.
POST /apps/{appId}/ab-tests
{ "name": "Subject line test", "testPercent": 20,
"winnerDelayMinutes": 60,
"variants": [ { "title": "…", "body": "…" },
{ "title": "…", "body": "…" } ] }
GET /apps/{appId}/ab-tests
GET /apps/{appId}/ab-tests/{id}
DELETE /apps/{appId}/ab-tests/{id} # cancel before the winner is decidedJourneys
Trigger-based series: device_registered runs when a new device registers; inactive_days enrolls devices that have been inactive for triggerConfig.days (scanned daily). A device enters each journey at most once.
POST /apps/{appId}/journeys
{ "name": "Welcome series", "trigger": "device_registered",
"steps": [
{ "delayMinutes": 0, "payload": { "title": "Welcome!", "body": "…" } },
{ "delayMinutes": 1440, "payload": { "title": "Day 2", "body": "…" } } ] }
GET /apps/{appId}/journeys
PATCH /apps/{appId}/journeys/{id} # { "active": false } pauses
DELETE /apps/{appId}/journeys/{id}Segments
Named, reusable audiences (panel: Segments). A segment stores a filter; devices match it live at send time. The estimate endpoint returns the current audience size without saving — exact: false means the count was sampled.
GET /apps/{appId}/segments
POST /apps/{appId}/segments # { "name": "...", "filter": { ...segment } }
PATCH /apps/{appId}/segments/{id} # rename and/or replace filter
DELETE /apps/{appId}/segments/{id}
POST /apps/{appId}/segments/estimate # { "filter": { ...segment } } → { count, exact }In-app messages
Banners, modals, full-screen takeovers and carousels shown inside the app — no push permission needed. They are not pushed: the SDK pulls active messages when the app comes to the foreground and presents one when its trigger fires. Impressions and clicks are tracked.
layout is one of top, center, bottom, fullscreen, carousel. Content is either a block document (editor: "block") or your own markup (editor: "html"); the server renders both to the HTML the device displays. Block types: image, heading, text, button, spacer, divider.
POST /apps/{appId}/in-app-messages
{ "name": "Spring sale", "layout": "center", "editor": "block",
"content": {
"pages": [ { "blocks": [
{ "type": "heading", "text": "Sale!" },
{ "type": "text", "text": "20% off this week" },
{ "type": "button", "label": "See offer",
"action": "open", "url": "myapp://sale" } ] } ],
"style": { "background": "#ffffff", "cornerRadius": 16 }
},
"triggers": [ { "type": "session_time", "seconds": 5 } ],
"displayLimit": 1, "displayDelayMinutes": 0,
"segment": { "platforms": ["IOS"] } }
# triggers: app_open | session_time (seconds) | custom (key)
# custom is fired from the app: Notibolt.addTrigger("cart_abandoned")
# displayLimit: 0 = unlimited
POST /apps/{appId}/in-app-messages/preview # renders without saving
GET /apps/{appId}/in-app-messages
GET /apps/{appId}/in-app-messages/{id}
PATCH /apps/{appId}/in-app-messages/{id}
DELETE /apps/{appId}/in-app-messages/{id}Recurring campaigns
Cron-scheduled sends ("every Monday at 10:00"). The payload is a full notification body as above.
POST /apps/{appId}/recurring-campaigns
{ "name": "Weekly digest", "cronPattern": "0 10 * * 1",
"timezone": "Europe/Istanbul",
"payload": { "title": "…", "body": "…" } }
GET /apps/{appId}/recurring-campaigns
PATCH /apps/{appId}/recurring-campaigns/{id} # { "active": false } pauses
DELETE /apps/{appId}/recurring-campaigns/{id}Read results
GET /apps/{appId}/notifications # latest 50
GET /apps/{appId}/notifications/{id} # live counters
GET /apps/{appId}/notifications/{id}/deliveries?take=100&status=FAILED
GET /apps/{appId}/notifications/{id}/deliveries.csv # CSV export
GET /apps/{appId}/notifications/stats/daily?days=14Counters: totalTargeted, totalSent, totalDelivered (device-confirmed), totalFailed, totalOpened. The deliveries endpoint includes a per-error breakdown.
Device endpoints (public, used by SDKs)
Authenticated by the public App ID; they never return push tokens or other devices' data. You normally don't call these yourself — the SDKs do.
POST /sdk/devices # register (upsert by push token)
PUT /sdk/devices/{deviceId} # update tags / user / prefs
# tag fields: "tags" replaces the whole set (legacy);
# "mergeTags" patches only the given keys, "removeTagKeys" deletes keys,
# "clearTags": true wipes them. Values: string or string[]
GET /sdk/devices/{deviceId}/inbox?appId=… # notification history (max 50)
POST /sdk/notifications/{id}/received # delivery confirmation
POST /sdk/notifications/{id}/opened # open trackingThe inbox endpoint powers an in-app "notification history" screen: it returns the device's visible notifications (silent pushes excluded), localized to the device's language, newest first.
Webhooks
Create webhooks in the panel (App → Settings) or via POST /apps/{appId}/webhooks. Events:
| Event | Fires when |
|---|---|
notification.completed | A send finishes (with final counters) |
device.unsubscribed | A device's token goes dead and it is deactivated |
Payloads are signed: verify the X-Notibolt-Signature: sha256=… header with your webhook secret (HMAC-SHA256 of the raw body).
Delivery error reasons
errorReason | Meaning |
|---|---|
fcm_not_configured / apns_not_configured | Credentials missing for that platform |
UNREGISTERED, Unregistered, BadDeviceToken | Dead token — the device is auto-deactivated |
TopicDisallowed | Bundle ID not registered as an App ID in your Apple developer account |
timeout, connection_error: … | Transient — retried automatically with backoff |
Rate limits
Public SDK endpoints are limited to 120 requests/minute per IP. Sending endpoints are limited per plan — contact us if you need more.