REST API reference

REST API for mobile apps and backends. Replace {appId} with your project's App ID.

Authentication

CredentialUsed inHow to send it
App ID Mobile app (public) Device API: in the URL /api/v1/apps/{appId}/…. Notification API: the app_id field (body or query).
API Key Backend only (secret) Header Authorization: Key psk_… (Bearer, Basic or X-Api-Key also accepted)

Endpoint

MethodPathAuthDescription
POST/api/v1/apps/{appId}/devicesApp IDRegister / update a device (upsert by token)
DELETE/api/v1/apps/{appId}/devices/{deviceId}App IDUnsubscribe
GET/api/v1/apps/{appId}/devicesAPI KeyList devices (?externalUserId=&platform=&active=&page=&pageSize=)
GET/api/v1/apps/{appId}/devices/{deviceId}API KeyGet a device
POST/notificationsAPI KeySend a notification, OneSignal format (app_id in the body). Delivered in the background.
GET/notifications?app_id=&limit=&offset=API KeySend history, newest first (limit max 50)
GET/notifications/{id}?app_id=API KeyDelivery results: successful / failed / errored / remaining

Register a device

POST /api/v1/apps/{appId}/devices
{
  "platform": "Ios",           // "Ios" | "Android"
  "token": "a1b2c3…",          // APNs hex token / FCM token
  "isSandbox": false,          // iOS debug build = true
  "externalUserId": "user-1",  // null on logout
  "appVersion": "1.2.0",
  "language": "vi",
  "tags": { "plan": "vip" },   // null = keep existing tags
  "voipToken": "9f2c…"         // iOS: PushKit token for calls; null = keep, "" = remove
}

Call it on every app launch, when the token changes, and on user login/logout. Keep the returned id if you need to unsubscribe.

Send a notification

Same structure as OneSignal's Create notification API. A backend on OneSignal only needs to switch the base URL https://api.onesignal.com to TingPush and change app_id and the key.

POST /notifications
Authorization: Key psk_…     (Basic / Bearer also accepted)
Content-Type: application/json
{
  "app_id": "<your project's App ID>",
  "target_channel": "push",
  "include_aliases": { "external_id": ["user-1"] },
  "headings": { "en": "Order delivered" },
  "contents": { "en": "Order #42 was delivered" },
  "data": { "screen": "order", "id": "42" },
  "big_picture": "https://…/img.jpg",
  "ios_badgeType": "SetTo", "ios_badgeCount": 1,
  "android_channel_id": "orders",
  "ttl": 86400, "collapse_id": "order-42"
}

200 → { "id": "…", "external_id": null }
200 → { "id": "", "errors": ["All included players are not subscribed"] }
400 → { "errors": ["…"] }      403 → wrong App ID / key

GET /notifications/{id}?app_id=…
→ { "id", "successful", "failed", "errored", "remaining",
    "queued_at", "completed_at", "headings", "contents", … }
OneSignal fieldSupport
headings, contents, subtitleUses en, otherwise the first language. Per-device language isn't supported yet.
include_aliases.external_id, include_external_user_idsSend by External ID
included_segmentsOnly All, Subscribed Users, Total Subscriptions (= everyone)
filtersOnly field: tag, relation: =, combined with AND
include_subscription_ids, include_player_idsThese are TingPush Device IDs, not OneSignal IDs
include_ios_tokens, include_android_reg_idsSend straight to tokens (iOS defaults to production)
isIos, isAndroidSet false to exclude a platform
data, url / app_urlSent in data; url becomes the url key
big_picture, ios_attachmentsImage attachment
ttl, collapse_id, priority, content_available, mutable_contentYes
ios_sound, thread_id, ios_category, ios_interruption_levelYes
ios_badgeTypeOnly SetTo; Increase is ignored
android_channel_id, small_icon, android_accent_colorYes
send_after, excluded_segments, other segments, non-tag filtersNot supported yet: returns 400

Calls (CallKit / Android)

The call extension field of POST /notifications (not in OneSignal). Recipients are chosen like a normal notification.

POST /notifications
{
  "app_id": "<App ID>",
  "include_aliases": { "external_id": ["user-2"] },
  "call": {
    "action": "incoming",        // "incoming" | "ended"
    "id": "6f1c…-uuid",          // empty = auto-generated (incoming)
    "caller_name": "John Smith",
    "handle": "0901234567",
    "avatar": "https://…/a.jpg",
    "is_video": false,
    "timeout": 45                // ring seconds
  },
  "data": { "room": "abc" }      // extra data
}

When the call is answered / cancelled / times out: send again with "action": "ended" and the same id (optionally with headings/contents = "Missed call") so the other devices stop ringing.

iOSAndroid
incomingVoIP push (apns-push-type: voip, topic <bundle>.voip) to the VoIP token; APNs drops it after timeoutFCM data message, priority HIGH, TTL = timeout
endedRegular APNs to the regular token (with a title = notification, without = silent)FCM data message, priority HIGH

Payload (both platforms, all values are strings): type (incoming_call / call_ended), call_id, caller_name, caller_handle, caller_avatar, is_video ("true"/"false"), timeout, plus data. Android also gets title/body if sent.

iOS devices without a voipToken count as failed. A dead VoIP token is only removed; the device still gets normal notifications. The APNs .p8 key works for VoIP; no separate certificate is needed.

Mobile SDKs

Step-by-step guide with code samples: App integration guide →

PlatformFolderBuilt on
iOS (Swift)sdk/iosPushKit + CallKit, no Firebase (SPM / CocoaPods)
Android (Kotlin/Java)sdk/android/pushconsoleFirebase Messaging, ready-made full-screen call UI
Fluttersdk/flutter/push_consolefirebase_messaging + flutter_callkit_incoming
.NET MAUI (C#)sdk/maui/PushConsole.MauiXamarin.Firebase.Messaging (Android), PushKit + CallKit (iOS)
React Nativesdk/react-native/push-console@react-native-firebase/messaging + react-native-callkeep

Integrating without the SDK

iOS (Swift)

func application(_ app: UIApplication,
  didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    #if DEBUG
    let isSandbox = true
    #else
    let isSandbox = false
    #endif
    // POST /api/v1/apps/{appId}/devices
}

// Calls: PushKit + CallKit (enable Background Modes → Voice over IP)
let registry = PKPushRegistry(queue: .main)
registry.delegate = self
registry.desiredPushTypes = [.voIP]

func pushRegistry(_ r: PKPushRegistry,
  didUpdate creds: PKPushCredentials, for type: PKPushType) {
    let voipToken = creds.token.map { String(format: "%02x", $0) }.joined()
    // POST …/devices with "voipToken": voipToken
}

func pushRegistry(_ r: PKPushRegistry,
  didReceiveIncomingPushWith payload: PKPushPayload,
  for type: PKPushType, completion: @escaping () -> Void) {
    let p = payload.dictionaryPayload
    let update = CXCallUpdate()
    update.remoteHandle = CXHandle(type: .generic,
        value: p["caller_handle"] as? String ?? "")
    update.localizedCallerName = p["caller_name"] as? String
    update.hasVideo = (p["is_video"] as? String) == "true"
    // MUST report to CallKit immediately or iOS kills the app
    provider.reportNewIncomingCall(
        with: UUID(uuidString: p["call_id"] as! String)!,
        update: update) { _ in completion() }
}

Android (Kotlin)

FirebaseMessaging.getInstance().token
    .addOnSuccessListener { token ->
        // POST /api/v1/apps/{appId}/devices
    }

class PushService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        // register again with the new token
    }

    override fun onMessageReceived(msg: RemoteMessage) {
        val d = msg.data
        when (d["type"]) {
            "incoming_call" -> // show the call screen: notification
                // CATEGORY_CALL + full-screen intent (permission
                // USE_FULL_SCREEN_INTENT) or ConnectionService
                showIncomingCall(d["call_id"], d["caller_name"],
                    d["is_video"] == "true")
            "call_ended" -> dismissCall(d["call_id"])
        }
    }
}

Send your first notification in 15 minutes

Create a project, upload your APNs / FCM keys, install the SDK and send. No credit card required.