Device Registration
Register and deregister devices with the framework-agnostic endpoint — deviceId, platform, APNs/FCM tokens, subscriber ids, and the exact rules and error codes.
Registration tells Native Notify "this device can receive pushes, on this token". Call it on every app launch and whenever the push token changes — it is an idempotent upsert, so repeats are cheap and never create a duplicate.
POST https://app.nativenotify.com/api/universal/device/register
Get the device token
The token always comes from the platform — never from Native Notify. Install the package for your stack, get the token, then register it with the call below.
No package to install — APNs is part of UIKit. Ask for permission, register with APNs, and the app delegate hands you the hex device token:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
guard granted else { return }
DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }
}
// APNs answers here with the device token:
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let apnsToken = deviceToken.map { String(format: "%02x", $0) }.joined()
// → register it as tokens.apnsToken
}
Full walkthrough: Swift recipe.
Request
curl -X POST https://app.nativenotify.com/api/universal/device/register \
-H "Content-Type: application/json" \
-d '{
"appId": 123,
"appToken": "yourAppToken",
"deviceId": "A1B2C3D4-install-key",
"platform": "ios",
"subscriberId": "user_8241",
"appVersion": "2.4.0",
"timezone": "America/New_York",
"tokens": { "apnsToken": "0c9d8e…" }
}'
| Field | Required | Notes |
|---|---|---|
appId, appToken | yes | Your app's pair from the dashboard. For a web device you may send the app's publishable web key (nnweb_…) in the appToken field instead — see App Keys. |
deviceId | yes | Your stable per-install key. Max 200 chars. Choose it once and keep it. |
platform | yes | "ios", "android" or "web". |
subscriberId | no | Your logged-in user's id (max 256 chars, numbers accepted and coerced). Omit it for an anonymous device. |
appVersion | no | Trimmed, capped at 50 chars. |
timezone | no | IANA zone, trimmed, capped at 60 chars. |
environment | no | "production" (default), "staging" or "development" — see Environments. |
framework | no | A label for your stack ("Flutter", "Unity (C#)", …), max 60 chars. |
tokens.apnsToken | one of the three | iOS only. Hex device token, 64+ characters. Aliases: apns_token, apnToken. |
tokens.fcmToken | one of the three | Android only. FCM registration token, 32+ characters. Alias: fcm_token. |
webPush | one of the three | The browser PushSubscription JSON — { endpoint, keys: { p256dh, auth }, expirationTime? } — with platform: "web". Alias: web_push. See Web Push. |
At least one token is required. Token values are capped at 400 characters (web endpoints at 1000), and Expo push tokens are rejected here (unsupported_token_type) — that is the Expo path.
Response — 201
{
"ok": true,
"device": {
"appId": 123,
"deviceId": "A1B2C3D4-install-key",
"platform": "ios",
"subscriberId": "user_8241",
"appVersion": "2.4.0",
"timezone": "America/New_York",
"environment": "production",
"createdAt": "2026-09-20T14:02:11.000Z",
"lastSeenAt": "2026-09-23T09:10:44.000Z"
},
"tokens": { "apnsToken": "registered" }
}
tokens reports, per token you sent, what happened: "registered" (not seen before), "refreshed" (already on this device) or "reassigned" (the token moved here from another device).
Rules worth knowing
- Re-registering never duplicates. The unique keys are
(app, deviceId)for devices and(app, token type, token)for tokens. - Omitted optional fields keep their stored value — sending
appVersionalone never wipes the subscriber id or the environment. Re-registering withoutsubscriberIddoes not detach the device from its subscriber; deregister it instead. - Token rotation is normal. When APNs/FCM hands you a new token, register again — same
deviceId, new value. - Platform pairing is enforced:
apnsTokenfrom aniosdevice,fcmTokenfrom anandroiddevice,webPushfrom awebdevice — anything else is400 token_platform_mismatch. - A registration clears the token's dead flag. Failure strikes are stored per device, so re-registering the same device keeps its history; a token that moves to a different device is treated as fresh.
Deregister
POST https://app.nativenotify.com/api/universal/device/deregister
curl -X POST https://app.nativenotify.com/api/universal/device/deregister \
-H "Content-Type: application/json" \
-d '{ "appId": 123, "appToken": "yourAppToken", "deviceId": "A1B2C3D4-install-key" }'
{ "ok": true, "deviceId": "A1B2C3D4-install-key", "removed": true, "tokensRemoved": 1 }
A hard delete of the device row and its tokens, and idempotent: an unknown deviceId still answers 200 with "removed": false, so a logout that runs twice never fails. Call it when a user logs out, or to stop pushes for an install.
Errors
Errors use the envelope { "error": { "code", "message", "field"? } }. Every code below answers 400, except environment_disabled (403):
| Code | Meaning |
|---|---|
invalid_body | The body is not a JSON object. |
missing_field | appId, deviceId or platform is missing. |
invalid_field | appId is not a positive integer. |
invalid_platform | platform is not ios, android or web. |
invalid_device_id / invalid_subscriber_id | Too long or wrong type. |
invalid_tokens | tokens is not an object like { apnsToken, fcmToken }. |
no_tokens_provided | No native token and no webPush subscription was sent. |
invalid_token_format | Token does not match its shape, or is over 400 chars. |
invalid_web_push_endpoint | webPush.endpoint is missing, not a URL, not https, or over 1000 chars. |
invalid_web_push_keys | p256dh is not a base64url 65-byte key, or auth is not a base64url 16-byte secret. |
invalid_web_push | webPush is not an object, or expirationTime is malformed. |
token_platform_mismatch | An APNs token from an Android device, an FCM token from an iOS device, or a webPush from a non-web device. |
unsupported_token_type | An Expo push token was sent — use the universal tokens instead. |
invalid_environment | environment is not production, staging or development. |
invalid_framework | framework is not a string of at most 60 characters. |
environment_disabled | 403 — that environment is switched off for this app; nothing was written. |
app_not_found | The app id is unknown to a dashboard-session or MCP-token caller. With an appToken in the body, an unknown app id or a wrong token answers 401 with a plain-text message, like the rest of the API. |
Registered? Prove it with a test send to that deviceId, or read the app's counts with /api/universal/health.