Introduction

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.

Your platform
→
KD Push API
→
Queue & engine
→
APNs / FCM
→
Customers' devices

All requests and responses are JSON over HTTPS, dates are ISO 8601 and public ids are ULIDs.

Quick start

Quick start

  1. 1
    Create your app and take its keys
    In the merchant portal: New app → copy the App Key and the Server Key (shown once).
  2. 2
    Register the device from the app
    cURL
    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"
      }'
  3. 3
    Send from your backend
    cURL
    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

Authentication

Each app has two kinds of keys, each with exactly one place to live:

App Key
kdp_app_…
X-KD-App-Key: kdp_app_…
Where:
Inside the mobile app (public)
Can:
Register devices, read config, report opens
Can never send
Server Key
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
The key decides the app. No endpoint accepts an app or merchant id in the body, so one app's key can never reach another app's devices or messages — it simply gets 404.
Errors

Errors

Every error has the same shape and never contains internals:

422 Unprocessable Entity
{
  "error": {
    "code": "validation_failed",
    "message": "The body field is required.",
    "fields": {
      "body": [
        "The body field is required."
      ]
    }
  }
}
401invalid_app_key
Unknown App Key or disabled app.
401invalid_server_key
Unknown or revoked Server Key.
402subscription_inactive
The merchant has no active subscription.
402quota_exceeded
The plan's monthly notification quota is used up.
403invalid_user_signature
The signature does not match external_user_id.
403web_disabled · origin_not_allowed
Web is off for the app, or the page is not on one of its sites.
422invalid_subscription · provider_mismatch
Unusable browser subscription keys, or a platform/provider mismatch.
403app_disabled · account_banned
App disabled or account banned.
404not_found
Not found — or it belongs to another app.
409not_cancelable
The notification is already final.
422validation_failed
Invalid input; details in fields.
422invalid_token
Malformed device token.
429rate_limited
Too many requests; wait per Retry-After.
500server_error
Internal error — retry later.
Rate limits

Rate limits

60
req / min
App endpoints, per IP
20,000
req / min
App endpoints, per app
600
req / min
Server endpoints, per Server Key

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

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.

Header
Idempotency-Key: app-created-1234-v2.1.0
Mobile app

Runtime config

GET/sdk/configApp Key — mobile app

Fetch it when the app starts. Android takes its Firebase options from here, so changing the Firebase project or provider never requires rebuilding the app.

Safe to cache — the response carries Cache-Control: max-age=300.
Request
curl https://push.kd-app.com/api/v1/sdk/config \
  -H "X-KD-App-Key: kdp_app_…"
200 OK
{
  "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
    }
  }
}
Mobile app

Register a device

POST/sdk/devicesApp Key — mobile app

Call it on every app start, token refresh, login and logout. 201 the first time, 200 afterwards.

FieldDescription
platformios | android | webrequired
Device platform.
tokenstringrequired
APNs token (hex) or FCM registration token; for a browser, the subscription endpoint.
installation_iduuidoptional
A stable id you generate once and keep in the app per App Key. Links token refreshes to the same device.
providerapns | fcm | webpushoptional
Inferred from the platform when omitted.
ios.bundle_idstringoptional
The value of Bundle.main.bundleIdentifier.
ios.profile_app_idstringoptional
application-identifier from embedded.mobileprovision, without the team prefix.
ios.team_idstring(10)optional
TeamIdentifier from the same file.
ios.environmentproduction | sandboxoptional
The aps-environment value.
android.fcm_project_idstringoptional
The Firebase project that issued the token (from /sdk/config).
web.p256dh · web.authbase64urlconditional
The browser subscription keys (from PushSubscription.toJSON()). Required for web; accepted only from a page on one of the app's sites.
external_user_idstring | nulloptional
Your user id. Send null on logout.
user_signaturehexconditional
Required with external_user_id while identity verification is on. How to compute it
tagsobjectoptional
Up to 20 tags for targeting; keys [A-Za-z0-9_.-], short scalar values.
locale · timezone · app_version · os_version · device_model · sdk_versionstringoptional
Shown in the merchant portal.
A token belongs to exactly one app. If another app registers it (e.g. another store's APK installed over yours), the device moves there and its user and tags are wiped — your notifications never reach another store's customer.
Request
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"
  }'
201 Created
{
  "device": {
    "installation_id": "3f0c9a2e-6b1d-4c8e-9f7a-2d5e8b1c4a90",
    "platform": "android",
    "status": "active",
    "external_user_id": "42"
  }
}
403 Forbidden
{
  "error": {
    "code": "invalid_user_signature",
    "message": "user_signature does not match external_user_id."
  }
}
Mobile app

Unsubscribe

POST/sdk/devices/unsubscribeApp Key — mobile app

When the user turns notifications off inside the app. The device stops receiving until it registers again.

FieldDescription
installation_iduuidrequired
The same id used to register.
Request
curl -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" }'
200 OK
{
  "unsubscribed": true
}
Mobile app

Report a notification open

POST/sdk/eventsApp Key — mobile app

When the user taps a notification. Counted once per device and message, shown in the "opened" counter.

FieldDescription
type"opened"required
Event type.
message_idULIDrequired
The kd_message_id from the payload.
installation_iduuidrequired
Device installation id.
Request
curl -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"
  }'
202 Accepted
{
  "accepted": true
}
Web

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+).

  1. In the merchant portal: your app → Web, switch website notifications on and add your domain (or from your backend with PUT /app/web).
  2. Create /kdpush-sw.js at the root of your site with this one line:
Node.js
importScripts('https://push.kd-app.com/sdk/web/v1/kdpush-sw.js');
  1. Add the SDK script to every page, before </head>:
Text
<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

Node.js
<!-- 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' })
Every browser sends the 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

Web settings

PUT/app/webServer Key — your backend

For 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.

FieldDescription
enabledbooleanrequired
Switch web on or off.
originsstring[] ≤ 20conditional
Sites allowed to subscribe visitors, e.g. shop.example.com (stored as https://…).
icon_urlhttps URLoptional
Icon of every notification.
default_urlURLoptional
Click destination when a message has no web link.
prompt{enabled, delay_seconds, title, text}optional
The permission card.
Request
curl -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"}'
200 OK
{
  "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
    }
  }
}
Platform backend

Send a notification

POST/messagesServer Key — your backend

Sends (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.

FieldDescription
bodystring ≤ 2000required
Notification text.
titlestring ≤ 255optional
Title.
urlstringoptional
Opened on tap: an https:// link or an in-app link such as neonstore://app/1234.
image_urlhttps URLoptional
Large image (Android; iOS needs an extension in the app).
dataobject ≤ 30optional
Extra data, delivered as strings. Keys starting with kd_ are reserved.
audienceobjectoptional
{"type":"all"} (default) · users + external_user_ids · devices + installation_ids · tags + tags — up to 1000 ids.
platforms["ios","android","web"]optional
Limit to some platforms, or omit for all.
scheduled_atISO 8601optional
Schedule within the next 30 days.
ttl_secondsintoptional
How long to keep it for offline devices (default 3 days, max 28).
priorityhigh | normaloptional
Default high.
collapse_idstring ≤ 64optional
A newer notification with the same id replaces the older one on the device.

Headers

FieldDescription
Idempotency-Keystring ≤ 128optional
Strongly recommended. Why?
Requires an active merchant subscription, otherwise 402 subscription_inactive. Without one nothing works: no runtime config (/sdk/config), no device registration and no app settings changes from your backend.
Request
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" }
  }'
202 Accepted
{
  "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
  }
}
402 Payment Required
{
  "error": {
    "code": "subscription_inactive",
    "message": "No active KD Push subscription for this account."
  }
}
Platform backend

Get a notification

GET/messages/{id}Server Key — your backend

Status and counters. See the message object for every field.

Request
curl https://push.kd-app.com/api/v1/messages/01JB2QA4N7C2X9V5B8M3K6P1RD \
  -H "Authorization: Bearer kdp_live_…"
200 OK
{
  "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"
  }
}
Platform backend

List notifications

GET/messagesServer Key — your backend

Your app's history, newest first.

FieldDescription
statusstringoptional
scheduled · queued · sending · sent · failed · canceled
per_page1–100optional
Default 20.
Request
curl "https://push.kd-app.com/api/v1/messages?status=sent&per_page=20" \
  -H "Authorization: Bearer kdp_live_…"
200 OK
{
  "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
  }
}
Platform backend

Cancel a notification

POST/messages/{id}/cancelServer Key — your backend

Cancels a scheduled, queued or sending notification. Batches not yet sent stop immediately.

Request
curl -X POST https://push.kd-app.com/api/v1/messages/01JB2QA4N7C2X9V5B8M3K6P1RD/cancel \
  -H "Authorization: Bearer kdp_live_…"
200 OK
{
  "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
  }
}
409 Conflict
{
  "error": {
    "code": "not_cancelable",
    "message": "Only scheduled, queued or sending messages can be canceled."
  }
}
Platform backend

App & subscription

GET/appServer Key — your backend

Verify 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.

Request
curl https://push.kd-app.com/api/v1/app \
  -H "Authorization: Bearer kdp_live_…"
200 OK
{
  "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
    }
  }
}
Platform backend

Android package

PUT/app/androidServer Key — your backend

For 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.

FieldDescription
packagestringrequired
For example com.store.app.
If the merchant chose their own Firebase project for this app and it is registered for another package, the call is refused with 409 package_mismatch and firebase_package names the registered one — so Android notifications never stop silently.
Request
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"}'
200 OK
{
  "app": {
    "id": "01JB2Q6V8K4Y3N5M7R9T1W0XZC",
    "android_package": "com.store.app"
  }
}
409 Conflict
{
  "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"
  }
}
Platform backend

Device stats

GET/devices/statsServer Key — your backend

Active devices per platform.

Request
curl https://push.kd-app.com/api/v1/devices/stats \
  -H "Authorization: Bearer kdp_live_…"
200 OK
{
  "devices": {
    "active": 1240,
    "ios": 800,
    "android": 440,
    "web": 0
  }
}
Connect

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).

  1. We register your platform and give you a client_id and client_secret, with the exact redirect URIs allowed.
  2. Send the merchant to /portal/connect/authorize with the parameters below.
  3. They sign in or create an account (prefilled from what you sent), pick or create the app, and allow.
  4. We send them back to redirect_uri?code=…&state=… — or error=access_denied if they decline.
  5. Your backend trades the code for the keys with POST /connect/token.

Authorize parameters

FieldDescription
response_typecoderequired
Always code.
client_idstringrequired
Your platform's id.
redirect_urihttps URLrequired
Must match a registered URI exactly.
statestring ≤ 255required
A random value you keep and check on return.
code_challengebase64urlrequired
BASE64URL(SHA256(code_verifier)); keep the code_verifier (43–128 chars) on your side.
code_challenge_methodS256required
Always S256.
app_namestring ≤ 120optional
Name for a new app if the merchant has none.
ios_bundle_idstringoptional
The matching app is offered first, and the id is registered with Apple automatically.
android_packagestringoptional
The package the Android app is built with; an existing app without one learns it.
external_refstring ≤ 191optional
Your id for the store; reconnecting the same store replaces its previous key.
external_labelstring ≤ 255optional
What the merchant sees (e.g. the store domain).
hint_name · hint_email · hint_company · hint_phonestringoptional
Prefill the sign-up form.
Check state 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.
Request
// 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));
Connect

Exchange the code

POST/connect/tokenclient_id + client_secret
FieldDescription
grant_typeauthorization_coderequired
Fixed.
client_id · client_secretstringrequired
Your platform credentials.
codestringrequired
From the redirect.
redirect_uristringrequired
The same URI the request used.
code_verifierstringrequired
The value you kept.

You 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.

The Server Key is the connection: revoking it from either side ends the link, and reconnecting the same store replaces it rather than piling up keys.
Request
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": "…"
  }'
200 OK
{
  "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"
    }
  }
}
400 Bad Request
{
  "error": {
    "code": "invalid_grant",
    "message": "Authorization code already used."
  }
}
Connect

Disconnect

POST/connect/revokeServer Key — your backend

The 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.

Request
curl -X POST https://push.kd-app.com/api/v1/connect/revoke \
  -H "Authorization: Bearer kdp_live_…"
200 OK
{
  "revoked": true
}
Reference

The message object

scheduled→queued→sending→sent·failedcanceled
FieldDescription
idULIDoptional
Notification id.
statusstringoptional
sent once at least one device accepted it, failed when none did (reason in error).
sourceapi | portal | adminoptional
Where it was sent from.
counts.targetedintoptional
Devices targeted.
counts.sentintoptional
Accepted by Apple/Google for delivery.
counts.invalidintoptional
Dead devices (app uninstalled, …) — excluded automatically afterwards.
counts.failedintoptional
Failed for other reasons after retries.
counts.openedintoptional
Reported opens.
failures[{provider, code, http_status, count}]optional
Why some devices did not get it, most common first: Apple's or Firebase's code as returned (e.g. TopicDisallowed), the HTTP status and how many devices.
created_at · started_at · finished_at · scheduled_atISO 8601optional
Lifecycle timestamps.
Guide

User 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.

Formula
user_signature = hex( HMAC-SHA256( external_user_id , identity_secret ) )
On your backend
// 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];
The secret is in the portal: app → Keys → Identity secret. Never ship it inside the app.
Guide

What the app receives

iOS — APNs
{
  "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"
}
Android — FCM
{
  "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.

Flutter
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
});
Guide

Platform integration checklist

  1. 1The merchant creates the app in the portal and pastes the App Key and Server Key into your platform panel.
  2. 2Your platform builds the app with the App Key inside — the app calls /sdk/config, then registers the device.
  3. 3After login your backend hands the app a user_signature for the user id.
  4. 4Your platform sends automatic notifications (new app, update) with one Idempotency-Key per event.
  5. 5When a catalog is connected for the first time, do not notify per app — send one digest.
  6. 6Check subscription.can_send via /app before turning automatic notifications on.
Tools

Postman

Ready-made Postman collectionv2.1Every request with examples and responses, auth pre-wired — add your keys and run in seconds.
  1. Download the collection and import it in Postman (Import → choose the file).
  2. In the collection variables set app_key, server_key and a test device token in device_token.
  3. Run Register device, then Send a notification — the id is saved to message_id for the follow-up requests.
  4. Optional: set external_user_id and identity_secret and the collection computes user_signature itself.