REST API reference
REST API for mobile apps and backends. Replace {appId} with your project's App ID.
Authentication
| Credential | Used in | How 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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/apps/{appId}/devices | App ID | Register / update a device (upsert by token) |
| DELETE | /api/v1/apps/{appId}/devices/{deviceId} | App ID | Unsubscribe |
| GET | /api/v1/apps/{appId}/devices | API Key | List devices (?externalUserId=&platform=&active=&page=&pageSize=) |
| GET | /api/v1/apps/{appId}/devices/{deviceId} | API Key | Get a device |
| POST | /notifications | API Key | Send a notification, OneSignal format (app_id in the body). Delivered in the background. |
| GET | /notifications?app_id=&limit=&offset= | API Key | Send history, newest first (limit max 50) |
| GET | /notifications/{id}?app_id= | API Key | Delivery 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 field | Support |
|---|---|
headings, contents, subtitle | Uses en, otherwise the first language. Per-device language isn't supported yet. |
include_aliases.external_id, include_external_user_ids | Send by External ID |
included_segments | Only All, Subscribed Users, Total Subscriptions (= everyone) |
filters | Only field: tag, relation: =, combined with AND |
include_subscription_ids, include_player_ids | These are TingPush Device IDs, not OneSignal IDs |
include_ios_tokens, include_android_reg_ids | Send straight to tokens (iOS defaults to production) |
isIos, isAndroid | Set false to exclude a platform |
data, url / app_url | Sent in data; url becomes the url key |
big_picture, ios_attachments | Image attachment |
ttl, collapse_id, priority, content_available, mutable_content | Yes |
ios_sound, thread_id, ios_category, ios_interruption_level | Yes |
ios_badgeType | Only SetTo; Increase is ignored |
android_channel_id, small_icon, android_accent_color | Yes |
send_after, excluded_segments, other segments, non-tag filters | Not 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.
| iOS | Android | |
|---|---|---|
| incoming | VoIP push (apns-push-type: voip, topic <bundle>.voip) to the VoIP token; APNs drops it after timeout | FCM data message, priority HIGH, TTL = timeout |
| ended | Regular 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 →
| Platform | Folder | Built on |
|---|---|---|
| iOS (Swift) | sdk/ios | PushKit + CallKit, no Firebase (SPM / CocoaPods) |
| Android (Kotlin/Java) | sdk/android/pushconsole | Firebase Messaging, ready-made full-screen call UI |
| Flutter | sdk/flutter/push_console | firebase_messaging + flutter_callkit_incoming |
| .NET MAUI (C#) | sdk/maui/PushConsole.Maui | Xamarin.Firebase.Messaging (Android), PushKit + CallKit (iOS) |
| React Native | sdk/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"])
}
}
}