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 tin | Dùng ở đâu | Cá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ẫn | Xác thực | Mô tả |
|---|---|---|---|
| POST | /api/v1/apps/{appId}/devices | App ID | Đăng ký / cập nhật thiết bị (upsert theo token) |
| DELETE | /api/v1/apps/{appId}/devices/{deviceId} | App ID | Huỷ nhận thông báo |
| GET | /api/v1/apps/{appId}/devices | API Key | Danh sách thiết bị (?externalUserId=&platform=&active=&page=&pageSize=) |
| GET | /api/v1/apps/{appId}/devices/{deviceId} | API Key | Chi tiết thiết bị |
| POST | /notifications | API Key | Gử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 Key | Lịch sử gửi, mới nhất trước (limit tối đa 50) |
| GET | /notifications/{id}?app_id= | API Key | Kế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 OneSignal | Hỗ trợ |
|---|---|
headings, contents, subtitle | Lấ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_ids | Gửi theo External ID |
included_segments | Chỉ All, Subscribed Users, Total Subscriptions (= tất cả) |
filters | Chỉ field: tag, relation: =, nối bằng AND |
include_subscription_ids, include_player_ids | Là Device ID của TingPush, không phải ID trên OneSignal |
include_ios_tokens, include_android_reg_ids | Gửi thẳng tới token (iOS mặc định production) |
isIos, isAndroid | Đặt false để loại nền tảng |
data, url / app_url | Gử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_content | Có |
ios_sound, thread_id, ios_category, ios_interruption_level | Có |
ios_badgeType | Chỉ SetTo; Increase bị bỏ qua |
android_channel_id, small_icon, android_accent_color | Có |
send_after, excluded_segments, segment khác, filter khác tag | Chư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.
| iOS | Android | |
|---|---|---|
| incoming | VoIP 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 |
| ended | APNs 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ảng | Thư mục | Dựa trên |
|---|---|---|
| iOS (Swift) | sdk/ios | PushKit + CallKit, không cần Firebase (SPM / CocoaPods) |
| Android (Kotlin/Java) | sdk/android/pushconsole | Firebase Messaging, màn hình cuộc gọi toàn màn hình dựng sẵn |
| 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 |
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"])
}
}
}