Tài liệu REST API

REST API cho app mobile và backend. Thay {appId} bằng App ID của dự án.

Xác thực

Thông tinDùng ở đâuCách gửi
App ID App mobile (công khai) API thiết bị: nằm trong URL /api/v1/apps/{appId}/…. API thông báo: trường app_id (body hoặc query).
API Key Chỉ ở backend (bí mật) Header Authorization: Key psk_… (nhận cả Bearer, Basic, hoặc X-Api-Key)

Endpoint

MethodĐường dẫnXác thựcMô tả
POST/api/v1/apps/{appId}/devicesApp IDĐăng ký / cập nhật thiết bị (upsert theo token)
DELETE/api/v1/apps/{appId}/devices/{deviceId}App IDHuỷ nhận thông báo
GET/api/v1/apps/{appId}/devicesAPI KeyDanh sách thiết bị (?externalUserId=&platform=&active=&page=&pageSize=)
GET/api/v1/apps/{appId}/devices/{deviceId}API KeyChi tiết thiết bị
POST/notificationsAPI KeyGửi thông báo, cấu trúc như OneSignal (app_id trong body). Gửi ở nền.
GET/notifications?app_id=&limit=&offset=API KeyLịch sử gửi, mới nhất trước (limit tối đa 50)
GET/notifications/{id}?app_id=API KeyKết quả gửi: successful / failed / errored / remaining

Đăng ký thiết bị

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 khi đăng xuất
  "appVersion": "1.2.0",
  "language": "vi",
  "tags": { "plan": "vip" },   // null = giữ tag cũ
  "voipToken": "9f2c…"         // iOS: token PushKit để nhận cuộc gọi; null = giữ, "" = xoá
}

Gọi mỗi lần mở app, khi token đổi, khi user đăng nhập/đăng xuất. Lưu lại id trả về nếu cần huỷ đăng ký.

Gửi thông báo

Cùng cấu trúc với API Create notification của OneSignal. Backend đang dùng OneSignal chỉ cần đổi base URL https://api.onesignal.com sang địa chỉ TingPush, đổi app_id và key.

POST /notifications
Authorization: Key psk_…     (nhận cả Basic / Bearer)
Content-Type: application/json
{
  "app_id": "<App ID của dự án>",
  "target_channel": "push",
  "include_aliases": { "external_id": ["user-1"] },
  "headings": { "en": "Đơn hàng đã giao" },
  "contents": { "en": "Đơn #42 đã giao thành công" },
  "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 → sai App ID / key

GET /notifications/{id}?app_id=…
→ { "id", "successful", "failed", "errored", "remaining",
    "queued_at", "completed_at", "headings", "contents", … }
Trường OneSignalHỗ trợ
headings, contents, subtitleLấy bản en; không có thì lấy ngôn ngữ đầu tiên. Chưa gửi theo ngôn ngữ từng máy.
include_aliases.external_id, include_external_user_idsGửi theo External ID
included_segmentsChỉ All, Subscribed Users, Total Subscriptions (= tất cả)
filtersChỉ field: tag, relation: =, nối bằng AND
include_subscription_ids, include_player_idsLà Device ID của TingPush, không phải ID trên OneSignal
include_ios_tokens, include_android_reg_idsGửi thẳng tới token (iOS mặc định production)
isIos, isAndroidĐặt false để loại nền tảng
data, url / app_urlGửi kèm trong data; url thành key url
big_picture, ios_attachmentsẢnh đính kèm
ttl, collapse_id, priority, content_available, mutable_contentCó
ios_sound, thread_id, ios_category, ios_interruption_levelCó
ios_badgeTypeChỉ SetTo; Increase bị bỏ qua
android_channel_id, small_icon, android_accent_colorCó
send_after, excluded_segments, segment khác, filter khác tagChưa hỗ trợ: trả lỗi 400

Cuộc gọi (CallKit / Android)

Trường mở rộng call của POST /notifications (OneSignal không có). Người nhận chọn như thông báo thường.

POST /notifications
{
  "app_id": "<App ID>",
  "include_aliases": { "external_id": ["user-2"] },
  "call": {
    "action": "incoming",        // "incoming" | "ended"
    "id": "6f1c…-uuid",          // bỏ trống = tự sinh (incoming)
    "caller_name": "Nguyễn Văn A",
    "handle": "0901234567",
    "avatar": "https://…/a.jpg",
    "is_video": false,
    "timeout": 45                // giây đổ chuông
  },
  "data": { "room": "abc" }      // gửi kèm
}

Khi người nhận bắt máy / người gọi huỷ / hết giờ: gửi lại với "action": "ended" cùng id (có thể kèm headings/contents = "Cuộc gọi nhỡ") để các máy khác tắt chuông.

iOSAndroid
incomingVoIP push (apns-push-type: voip, topic <bundle>.voip) tới VoIP token; hết timeout thì APNs huỷFCM data message, priority HIGH, TTL = timeout
endedAPNs thường tới token thường (có tiêu đề = thông báo, không có = silent)FCM data message, priority HIGH

Payload (cả 2 nền tảng, mọi giá trị là chuỗi): type (incoming_call / call_ended), call_id, caller_name, caller_handle, caller_avatar, is_video ("true"/"false"), timeout, cộng với data. Android có thêm title/body nếu gửi kèm.

Thiết bị iOS chưa đăng ký voipToken sẽ bị tính lỗi. VoIP token chết chỉ bị xoá, thiết bị vẫn nhận thông báo thường. Key .p8 của APNs dùng được cho VoIP, không cần certificate riêng.

SDK cho app mobile

Hướng dẫn từng bước có code mẫu: Hướng dẫn tích hợp app →

Nền tảngThư mụcDựa trên
iOS (Swift)sdk/iosPushKit + CallKit, không cần Firebase (SPM / CocoaPods)
Android (Kotlin/Java)sdk/android/pushconsoleFirebase Messaging, màn hình cuộc gọi toàn màn hình dựng sẵn
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

Tích hợp app (không dùng 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
}

// Cuộc gọi: PushKit + CallKit (bật 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 với "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"
    // BẮT BUỘC báo CallKit ngay, nếu không iOS sẽ tắt 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) {
        // đăng ký lại với token mới
    }

    override fun onMessageReceived(msg: RemoteMessage) {
        val d = msg.data
        when (d["type"]) {
            "incoming_call" -> // hiện màn hình cuộc gọi: notification
                // CATEGORY_CALL + full-screen intent (quyền
                // USE_FULL_SCREEN_INTENT) hoặc ConnectionService
                showIncomingCall(d["call_id"], d["caller_name"],
                    d["is_video"] == "true")
            "call_ended" -> dismissCall(d["call_id"])
        }
    }
}

Gửi thông báo đầu tiên trong 15 phút

Tạo dự án, tải lên khoá APNs / FCM, cài SDK và gửi. Không cần thẻ thanh toán.