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…" }
  }'
FieldRequiredNotes
appId, appTokenyesYour 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.
deviceIdyesYour stable per-install key. Max 200 chars. Choose it once and keep it.
platformyes"ios", "android" or "web".
subscriberIdnoYour logged-in user's id (max 256 chars, numbers accepted and coerced). Omit it for an anonymous device.
appVersionnoTrimmed, capped at 50 chars.
timezonenoIANA zone, trimmed, capped at 60 chars.
environmentno"production" (default), "staging" or "development" — see Environments.
frameworknoA label for your stack ("Flutter", "Unity (C#)", …), max 60 chars.
tokens.apnsTokenone of the threeiOS only. Hex device token, 64+ characters. Aliases: apns_token, apnToken.
tokens.fcmTokenone of the threeAndroid only. FCM registration token, 32+ characters. Alias: fcm_token.
webPushone of the threeThe 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 appVersion alone never wipes the subscriber id or the environment. Re-registering without subscriberId does 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: apnsToken from an ios device, fcmToken from an android device, webPush from a web device — anything else is 400 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):

CodeMeaning
invalid_bodyThe body is not a JSON object.
missing_fieldappId, deviceId or platform is missing.
invalid_fieldappId is not a positive integer.
invalid_platformplatform is not ios, android or web.
invalid_device_id / invalid_subscriber_idToo long or wrong type.
invalid_tokenstokens is not an object like { apnsToken, fcmToken }.
no_tokens_providedNo native token and no webPush subscription was sent.
invalid_token_formatToken does not match its shape, or is over 400 chars.
invalid_web_push_endpointwebPush.endpoint is missing, not a URL, not https, or over 1000 chars.
invalid_web_push_keysp256dh is not a base64url 65-byte key, or auth is not a base64url 16-byte secret.
invalid_web_pushwebPush is not an object, or expirationTime is malformed.
token_platform_mismatchAn APNs token from an Android device, an FCM token from an iOS device, or a webPush from a non-web device.
unsupported_token_typeAn Expo push token was sent — use the universal tokens instead.
invalid_environmentenvironment is not production, staging or development.
invalid_frameworkframework is not a string of at most 60 characters.
environment_disabled403 — that environment is switched off for this app; nothing was written.
app_not_foundThe 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.