Introduction
KD Push is a fully managed push service for platforms and their merchants. Merchants never touch OneSignal, Firebase or Apple certificates: they add their app in the portal, take an App Key and a Server Key, and their platform sends through this API.
All requests and responses are JSON over HTTPS, dates are ISO 8601 and public ids are ULIDs.
Quick start
- 1Create your app and take its keys
- 2Register the device from the appcURL
curl -X POST https://push.kd-app.com/api/v1/sdk/devices \ -H "X-KD-App-Key: kdp_app_…" \ -H "Content-Type: application/json" \ -d '{ "platform": "android", "token": "fcm-registration-token", "installation_id": "3f0c9a2e-6b1d-4c8e-9f7a-2d5e8b1c4a90", "android": { "fcm_project_id": "neon-push" }, "external_user_id": "42", "user_signature": "9f2c4e…", "locale": "ar", "app_version": "2.1.0" }' - 3Send from your backendcURL
curl -X POST https://push.kd-app.com/api/v1/messages \ -H "Authorization: Bearer kdp_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: app-created-1234" \ -d '{ "title": "تطبيق جديد", "body": "تمت إضافة واتساب الذهبي للمتجر", "url": "neonstore://app/1234", "image_url": "https://cdn.example.com/icons/1234.png", "data": { "app_id": "1234" }, "audience": { "type": "all" } }'
Authentication
Each app has two kinds of keys, each with exactly one place to live:
kdp_app_…X-KD-App-Key: kdp_app_…- Where:
- Inside the mobile app (public)
- Can:
- Register devices, read config, report opens
- Can never send
kdp_live_…Authorization: Bearer kdp_live_…- Where:
- Only on your platform backend (secret)
- Can:
- Send, track, cancel, stats
- Shown once — we store only a hash
404.Errors
Every error has the same shape and never contains internals:
{
"error": {
"code": "validation_failed",
"message": "The body field is required.",
"fields": {
"body": [
"The body field is required."
]
}
}
}invalid_app_keyinvalid_server_keysubscription_inactivequota_exceededinvalid_user_signatureweb_disabled · origin_not_allowedinvalid_subscription · provider_mismatchapp_disabled · account_bannednot_foundnot_cancelablevalidation_failedfields.invalid_tokenrate_limitedRetry-After.server_errorRate limits
Over the limit you get 429 rate_limited with a Retry-After header. Sending to many devices is one request — the engine fans it out internally.
Idempotency
Send an Idempotency-Key header with every send (e.g. your event id). Repeating the request with the same key within 24 h returns the same notification with 200 instead of creating a new one — retrying after a network failure is always safe.
Idempotency-Key: app-created-1234-v2.1.0Runtime config
/sdk/configApp Key — mobile appFetch it when the app starts. Android takes its Firebase options from here, so changing the Firebase project or provider never requires rebuilding the app.
Cache-Control: max-age=300.curl https://push.kd-app.com/api/v1/sdk/config \
-H "X-KD-App-Key: kdp_app_…"{
"app": {
"id": "01JB2Q6V8K4Y3N5M7R9T1W0XZC",
"name": "My Store"
},
"identity_verification": "required",
"android": {
"provider": "fcm",
"firebase": {
"project_id": "neon-push",
"app_id": "1:123456789:android:abcdef",
"api_key": "AIza…",
"sender_id": "123456789",
"storage_bucket": null
}
},
"ios": {
"provider": "apns"
},
"web": {
"provider": "webpush",
"vapid_public_key": "BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U",
"icon_url": null,
"prompt": {
"enabled": true,
"delay_seconds": 6,
"title": null,
"text": null
}
}
}Register a device
/sdk/devicesApp Key — mobile appCall it on every app start, token refresh, login and logout. 201 the first time, 200 afterwards.
platformios | android | webrequiredtokenstringrequiredinstallation_iduuidoptionalproviderapns | fcm | webpushoptionalios.bundle_idstringoptionalBundle.main.bundleIdentifier.ios.profile_app_idstringoptionalapplication-identifier from embedded.mobileprovision, without the team prefix.ios.team_idstring(10)optionalTeamIdentifier from the same file.ios.environmentproduction | sandboxoptionalaps-environment value.android.fcm_project_idstringoptionalweb.p256dh · web.authbase64urlconditionalexternal_user_idstring | nulloptionalnull on logout.user_signaturehexconditionalexternal_user_id while identity verification is on. How to compute ittagsobjectoptionallocale · timezone · app_version · os_version · device_model · sdk_versionstringoptionalcurl -X POST https://push.kd-app.com/api/v1/sdk/devices \
-H "X-KD-App-Key: kdp_app_…" \
-H "Content-Type: application/json" \
-d '{
"platform": "android",
"token": "fcm-registration-token",
"installation_id": "3f0c9a2e-6b1d-4c8e-9f7a-2d5e8b1c4a90",
"android": { "fcm_project_id": "neon-push" },
"external_user_id": "42",
"user_signature": "9f2c4e…",
"locale": "ar",
"app_version": "2.1.0"
}'{
"device": {
"installation_id": "3f0c9a2e-6b1d-4c8e-9f7a-2d5e8b1c4a90",
"platform": "android",
"status": "active",
"external_user_id": "42"
}
}{
"error": {
"code": "invalid_user_signature",
"message": "user_signature does not match external_user_id."
}
}Unsubscribe
/sdk/devices/unsubscribeApp Key — mobile appWhen the user turns notifications off inside the app. The device stops receiving until it registers again.
installation_iduuidrequiredcurl -X POST https://push.kd-app.com/api/v1/sdk/devices/unsubscribe \
-H "X-KD-App-Key: kdp_app_…" \
-H "Content-Type: application/json" \
-d '{ "installation_id": "3f0c9a2e-6b1d-4c8e-9f7a-2d5e8b1c4a90" }'{
"unsubscribed": true
}Report a notification open
/sdk/eventsApp Key — mobile appWhen the user taps a notification. Counted once per device and message, shown in the "opened" counter.
type"opened"requiredmessage_idULIDrequiredkd_message_id from the payload.installation_iduuidrequiredcurl -X POST https://push.kd-app.com/api/v1/sdk/events \
-H "X-KD-App-Key: kdp_app_…" \
-H "Content-Type: application/json" \
-d '{
"type": "opened",
"message_id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"installation_id": "3f0c9a2e-6b1d-4c8e-9f7a-2d5e8b1c4a90"
}'{
"accepted": true
}Website notifications (browsers)
The same notifications reach your site's visitors in their browser — no app, no Firebase, no certificates: every app has its own VAPID key, created by KD Push. Works in Chrome, Edge, Firefox, Samsung Internet and Safari on Mac, and on iPhone and iPad once the site is added to the Home Screen (iOS 16.4+).
- In the merchant portal: your app →
Web, switch website notifications on and add your domain (or from your backend with PUT /app/web). - Create
/kdpush-sw.jsat the root of your site with this one line:
importScripts('https://push.kd-app.com/sdk/web/v1/kdpush-sw.js');- Add the SDK script to every page, before
</head>:
<script src="https://push.kd-app.com/sdk/web/v1/kdpush.js"
data-app-key="kdp_app_…" async></script>That's it: a small card asks visitors to turn notifications on (its text and delay are set in the portal), the browser is registered automatically and checks in once a day. Clicking a notification opens its link and counts as opened.
Options and API
<!-- data-* options -->
data-app-key required
data-service-worker /kdpush-sw.js (default)
data-prompt "off" hides the built-in card
data-lang ar | en | es | fr | de (default: the page language)
data-user-id your user id + data-user-signature (identity verification)
data-tags JSON, e.g. '{"plan":"vip"}'
// JavaScript
KDPush.subscribe() // ask (from a click) and subscribe → "subscribed" | "denied" | "default" | "unsupported" | "disabled"
KDPush.unsubscribe()
KDPush.isSubscribed() // Promise<boolean>
KDPush.login('42', signature) // link the browser to your user
KDPush.logout()
KDPush.setTags({ plan: 'vip' })os tag by itself (ios · android · desktop), so you can, for example, announce an iPhone app only to visitors on iPhones: {"platforms":["web"],"audience":{"type":"tags","tags":{"os":"ios"}}}. If your site already has its own service worker, add the importScripts line to it and point data-service-worker at it.Web settings
/app/webServer Key — your backendFor a platform that links its merchants' sites: switch web on and set the domains when a store connects or changes domain. Switching on needs at least one domain and creates the app's VAPID key. Fields left out keep their value.
enabledbooleanrequiredoriginsstring[] ≤ 20conditionalicon_urlhttps URLoptionaldefault_urlURLoptionalprompt{enabled, delay_seconds, title, text}optionalcurl -X PUT https://push.kd-app.com/api/v1/app/web \
-H "Authorization: Bearer kdp_live_…" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"origins":["shop.example.com"],"icon_url":"https://cdn.example.com/logo.png"}'{
"web": {
"enabled": true,
"origins": [
"https://shop.example.com",
"https://www.shop.example.com"
],
"vapid_public_key": "BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U",
"subscribers": 0,
"icon_url": "https://cdn.example.com/logo.png",
"default_url": null,
"prompt": {
"enabled": true,
"delay_seconds": 6,
"title": null,
"text": null
}
}
}Send a notification
/messagesServer Key — your backendSends (or schedules) a notification to your app's devices only. The response is immediate with status queued; delivery runs in the background — follow status and counters with Get a notification.
bodystring ≤ 2000requiredtitlestring ≤ 255optionalurlstringoptionalhttps:// link or an in-app link such as neonstore://app/1234.image_urlhttps URLoptionaldataobject ≤ 30optionalkd_ are reserved.audienceobjectoptional{"type":"all"} (default) · users + external_user_ids · devices + installation_ids · tags + tags — up to 1000 ids.platforms["ios","android","web"]optionalscheduled_atISO 8601optionalttl_secondsintoptionalpriorityhigh | normaloptionalcollapse_idstring ≤ 64optionalHeaders
Idempotency-Keystring ≤ 128optional402 subscription_inactive. Without one nothing works: no runtime config (/sdk/config), no device registration and no app settings changes from your backend.curl -X POST https://push.kd-app.com/api/v1/messages \
-H "Authorization: Bearer kdp_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: app-created-1234" \
-d '{
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر",
"url": "neonstore://app/1234",
"image_url": "https://cdn.example.com/icons/1234.png",
"data": { "app_id": "1234" },
"audience": { "type": "all" }
}'{
"message": {
"id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"status": "queued",
"source": "api",
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر",
"image_url": "https://cdn.example.com/icons/1234.png",
"url": "neonstore://app/1234",
"data": {
"app_id": "1234"
},
"audience": {
"type": "all"
},
"platforms": null,
"scheduled_at": null,
"counts": {
"targeted": 0,
"sent": 0,
"failed": 0,
"invalid": 0,
"opened": 0
},
"error": null,
"failures": [],
"created_at": "2026-10-03T12:00:00+00:00",
"started_at": null,
"finished_at": null
}
}{
"error": {
"code": "subscription_inactive",
"message": "No active KD Push subscription for this account."
}
}Get a notification
/messages/{id}Server Key — your backendStatus and counters. See the message object for every field.
curl https://push.kd-app.com/api/v1/messages/01JB2QA4N7C2X9V5B8M3K6P1RD \
-H "Authorization: Bearer kdp_live_…"{
"message": {
"id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"status": "sent",
"source": "api",
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر",
"image_url": "https://cdn.example.com/icons/1234.png",
"url": "neonstore://app/1234",
"data": {
"app_id": "1234"
},
"audience": {
"type": "all"
},
"platforms": null,
"scheduled_at": null,
"counts": {
"targeted": 1240,
"sent": 1227,
"failed": 3,
"invalid": 10,
"opened": 214
},
"error": null,
"failures": [],
"created_at": "2026-10-03T12:00:00+00:00",
"started_at": "2026-10-03T12:00:01+00:00",
"finished_at": "2026-10-03T12:00:04+00:00"
}
}List notifications
/messagesServer Key — your backendYour app's history, newest first.
statusstringoptionalscheduled · queued · sending · sent · failed · canceledper_page1–100optionalcurl "https://push.kd-app.com/api/v1/messages?status=sent&per_page=20" \
-H "Authorization: Bearer kdp_live_…"{
"data": [
{
"id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"status": "sent",
"source": "api",
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر",
"image_url": "https://cdn.example.com/icons/1234.png",
"url": "neonstore://app/1234",
"data": {
"app_id": "1234"
},
"audience": {
"type": "all"
},
"platforms": null,
"scheduled_at": null,
"counts": {
"targeted": 0,
"sent": 0,
"failed": 0,
"invalid": 0,
"opened": 0
},
"error": null,
"failures": [],
"created_at": "2026-10-03T12:00:00+00:00",
"started_at": null,
"finished_at": null
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 1,
"last_page": 1
}
}Cancel a notification
/messages/{id}/cancelServer Key — your backendCancels a scheduled, queued or sending notification. Batches not yet sent stop immediately.
curl -X POST https://push.kd-app.com/api/v1/messages/01JB2QA4N7C2X9V5B8M3K6P1RD/cancel \
-H "Authorization: Bearer kdp_live_…"{
"message": {
"id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"status": "canceled",
"source": "api",
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر",
"image_url": "https://cdn.example.com/icons/1234.png",
"url": "neonstore://app/1234",
"data": {
"app_id": "1234"
},
"audience": {
"type": "all"
},
"platforms": null,
"scheduled_at": null,
"counts": {
"targeted": 0,
"sent": 0,
"failed": 0,
"invalid": 0,
"opened": 0
},
"error": null,
"failures": [],
"created_at": "2026-10-03T12:00:00+00:00",
"started_at": null,
"finished_at": null
}
}{
"error": {
"code": "not_cancelable",
"message": "Only scheduled, queued or sending messages can be canceled."
}
}App & subscription
/appServer Key — your backendVerify the key and whether sending is possible before turning on automatic notifications: sending.can_send weighs the subscription and this month's quota, and sending.reason gives the cause with the codes POST /messages uses (subscription_inactive · quota_exceeded). plan and usage show where the merchant stands; portal links to where they fix it.
curl https://push.kd-app.com/api/v1/app \
-H "Authorization: Bearer kdp_live_…"{
"app": {
"id": "01JB2Q6V8K4Y3N5M7R9T1W0XZC",
"name": "My Store",
"platform": "neon",
"status": "active",
"ios_bundle_id": "com.store.app",
"android_package": "com.store.app"
},
"subscription": {
"status": "active",
"expires_at": "2026-11-02T12:00:00+00:00",
"grace_until": null,
"days_left": 19,
"can_send": true
},
"plan": {
"name": "Pro",
"period": "monthly",
"monthly_messages": 100000,
"max_devices": null,
"max_apps": 3
},
"usage": {
"deliveries_this_month": 18240,
"remaining_this_month": 81760,
"active_devices": 1240
},
"sending": {
"can_send": true,
"reason": null
},
"portal": {
"plans": "https://push.kd-app.com/portal/plans",
"subscription": "https://push.kd-app.com/portal/subscription"
},
"devices": {
"active": 1240,
"ios": 800,
"android": 440,
"web": 0
},
"web": {
"enabled": false,
"origins": [],
"vapid_public_key": null,
"subscribers": 0,
"icon_url": null,
"default_url": null,
"prompt": {
"enabled": true,
"delay_seconds": 6,
"title": null,
"text": null
}
}
}Android package
/app/androidServer Key — your backendFor a platform that lets its merchant choose the Android package: send the package the store's app is built with whenever it changes, so the portal shows it and a Firebase file uploaded later is checked against it.
packagestringrequiredcom.store.app.409 package_mismatch and firebase_package names the registered one — so Android notifications never stop silently.curl -X PUT https://push.kd-app.com/api/v1/app/android \
-H "Authorization: Bearer kdp_live_…" \
-H "Content-Type: application/json" \
-d '{"package":"com.store.app"}'{
"app": {
"id": "01JB2Q6V8K4Y3N5M7R9T1W0XZC",
"android_package": "com.store.app"
}
}{
"error": {
"code": "package_mismatch",
"message": "The app's Firebase project is registered for com.store.app; upload one for com.other.app first.",
"firebase_package": "com.store.app"
}
}Device stats
/devices/statsServer Key — your backendActive devices per platform.
curl https://push.kd-app.com/api/v1/devices/stats \
-H "Authorization: Bearer kdp_live_…"{
"devices": {
"active": 1240,
"ios": 800,
"android": 440,
"web": 0
}
}Connect with KD Push
Link a merchant's store to a KD Push app in one click, without anyone copying a key: the merchant approves on a KD Push page and your platform receives the keys server to server (OAuth 2 authorization code with PKCE).
- We register your platform and give you a
client_idandclient_secret, with the exact redirect URIs allowed. - Send the merchant to
/portal/connect/authorizewith the parameters below. - They sign in or create an account (prefilled from what you sent), pick or create the app, and allow.
- We send them back to
redirect_uri?code=…&state=…— orerror=access_deniedif they decline. - Your backend trades the code for the keys with POST /connect/token.
Authorize parameters
response_typecoderequiredclient_idstringrequiredredirect_urihttps URLrequiredstatestring ≤ 255requiredcode_challengebase64urlrequiredBASE64URL(SHA256(code_verifier)); keep the code_verifier (43–128 chars) on your side.code_challenge_methodS256requiredapp_namestring ≤ 120optionalios_bundle_idstringoptionalandroid_packagestringoptionalexternal_refstring ≤ 191optionalexternal_labelstring ≤ 255optionalhint_name · hint_email · hint_company · hint_phonestringoptionalstate first. The code lives 10 minutes and works once; presenting it twice revokes what it produced. Neither the code_verifier nor the client_secret ever reaches the browser.// 1. Send the merchant to KD Push (keep $verifier and $state in their session)
$verifier = Str::random(64);
$state = Str::random(40);
session(['kdpush' => compact('verifier', 'state')]);
return redirect('https://push.kd-app.com/portal/connect/authorize?' . http_build_query([
'response_type' => 'code',
'client_id' => config('services.kdpush.connect_client_id'),
'redirect_uri' => 'https://your-platform.example/kdpush/callback',
'state' => $state,
'code_challenge' => rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='),
'code_challenge_method' => 'S256',
'app_name' => $store->name, // optional: prefills a new app
'ios_bundle_id' => $store->bundleId, // optional
'android_package' => $store->androidPackage, // optional
'external_ref' => $store->id, // optional: your id for the store
'hint_email' => $owner->email, // optional: prefills sign-up
], '', '&', PHP_QUERY_RFC3986));Exchange the code
/connect/tokenclient_id + client_secretgrant_typeauthorization_coderequiredclient_id · client_secretstringrequiredcodestringrequiredredirect_uristringrequiredcode_verifierstringrequiredYou get the app_key (for the app), a new server_key issued for this connection (shown once), the identity_secret, and the merchant's standing in account — the same shape as GET /app.
curl -X POST https://push.kd-app.com/api/v1/connect/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "kdp_cli_…",
"client_secret": "kdp_cs_…",
"code": "kdp_code_…",
"redirect_uri": "https://your-platform.example/kdpush/callback",
"code_verifier": "…"
}'{
"connection": {
"id": "01JB2QX5V8K4Y3N5M7R9T1W0XZ",
"connected_at": "2026-10-14T12:00:00+00:00"
},
"app": {
"id": "01JB2Q6V8K4Y3N5M7R9T1W0XZC",
"name": "My Store",
"platform": "neon",
"ios_bundle_id": "com.store.app",
"android_package": null
},
"merchant": {
"name": "Store owner",
"email": "[email protected]"
},
"app_key": "kdp_app_…",
"server_key": "kdp_live_…",
"identity_secret": "…",
"api_base": "https://push.kd-app.com/api/v1",
"account": {
"subscription": {
"status": "active",
"expires_at": "2026-11-02T12:00:00+00:00",
"grace_until": null,
"days_left": 19,
"can_send": true
},
"plan": {
"name": "Pro",
"period": "monthly",
"monthly_messages": 100000,
"max_devices": null,
"max_apps": 3
},
"usage": {
"deliveries_this_month": 18240,
"remaining_this_month": 81760,
"active_devices": 1240
},
"sending": {
"can_send": true,
"reason": null
},
"portal": {
"plans": "https://push.kd-app.com/portal/plans",
"subscription": "https://push.kd-app.com/portal/subscription"
}
}
}{
"error": {
"code": "invalid_grant",
"message": "Authorization code already used."
}
}Disconnect
/connect/revokeServer Key — your backendThe Server Key you send stops working at once; the app, its devices and its history stay with the merchant. The merchant can also disconnect from their panel — your key then answers 401 invalid_server_key; treat that as disconnected.
curl -X POST https://push.kd-app.com/api/v1/connect/revoke \
-H "Authorization: Bearer kdp_live_…"{
"revoked": true
}The message object
idULIDoptionalstatusstringoptionalsent once at least one device accepted it, failed when none did (reason in error).sourceapi | portal | adminoptionalcounts.targetedintoptionalcounts.sentintoptionalcounts.invalidintoptionalcounts.failedintoptionalcounts.openedintoptionalfailures[{provider, code, http_status, count}]optionalTopicDisallowed), the HTTP status and how many devices.created_at · started_at · finished_at · scheduled_atISO 8601optionalUser identity verification
To link a device to a user (for personal notifications such as "your subscription ends soon") the app needs a signature computed by your backend after login — otherwise anyone could register their phone as another user and receive their notifications.
user_signature = hex( HMAC-SHA256( external_user_id , identity_secret ) )// After the user logs in — on your backend, never in the app
$signature = hash_hmac('sha256', (string) $user->id, config('services.kdpush.identity_secret'));
return ['user_id' => $user->id, 'push_signature' => $signature];What the app receives
{
"aps": {
"alert": {
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر"
},
"sound": "default",
"mutable-content": 1
},
"kd_message_id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"kd_url": "neonstore://app/1234",
"kd_image": "https://cdn.example.com/icons/1234.png",
"app_id": "1234"
}{
"notification": {
"title": "تطبيق جديد",
"body": "تمت إضافة واتساب الذهبي للمتجر",
"image": "https://cdn.example.com/icons/1234.png"
},
"data": {
"kd_message_id": "01JB2QA4N7C2X9V5B8M3K6P1RD",
"kd_url": "neonstore://app/1234",
"app_id": "1234"
}
}Open kd_url on tap and send kd_message_id to /sdk/events. data values arrive as strings.
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final url = message.data['kd_url'];
final id = message.data['kd_message_id'];
if (url != null) openDeepLink(url); // e.g. neonstore://app/1234
if (id != null) reportOpened(id); // POST /sdk/events
});Platform integration checklist
- 1The merchant creates the app in the portal and pastes the App Key and Server Key into your platform panel.
- 2Your platform builds the app with the App Key inside — the app calls /sdk/config, then registers the device.
- 3After login your backend hands the app a user_signature for the user id.
- 4Your platform sends automatic notifications (new app, update) with one Idempotency-Key per event.
- 5When a catalog is connected for the first time, do not notify per app — send one digest.
- 6Check subscription.can_send via /app before turning automatic notifications on.
Postman
- Download the collection and import it in Postman (Import → choose the file).
- In the collection variables set
app_key,server_keyand a test device token indevice_token. - Run Register device, then Send a notification — the id is saved to
message_idfor the follow-up requests. - Optional: set
external_user_idandidentity_secretand the collection computesuser_signatureitself.