Notiboltdocs

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
The REST key is a server-side secret — never ship it in a mobile app. Mobile SDKs only use the public App ID.

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

FieldBehavior
segmentOmitted → all subscribed devices. All filters combine with AND. tagFilters ops: eq, ne, gt, lt, exists, not_exists, in.
scheduledAtFuture ISO date → queued until then. Cancel while queued with DELETE /apps/{appId}/notifications/{id}.
contentsPer-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.
contentAvailableSilent push — nothing rendered; only data is delivered (skips the in-app opt-out filter).
Idempotency-Key headerSame 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 decided

Journeys

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=14

Counters: 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 tracking

The 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:

EventFires when
notification.completedA send finishes (with final counters)
device.unsubscribedA 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

errorReasonMeaning
fcm_not_configured / apns_not_configuredCredentials missing for that platform
UNREGISTERED, Unregistered, BadDeviceTokenDead token — the device is auto-deactivated
TopicDisallowedBundle 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.