Push notifications

Device registration

Register a device with its push token, or remove it.

POST https://app.nativenotify.com/api/universal/device/register

Call it on every app launch and whenever the push token changes. It's an upsert, so repeats never create a duplicate.

Get the device token

The token comes from the platform, never from Native Notify. Get it, then register it with the request below.

No package needed — APNs is part of UIKit. Ask for permission and APNs hands you the hex token:

AppDelegate.swift
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
}

See the 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, appTokenyesFrom the dashboard. A web device may send the publishable web key (nnweb_…) as appToken. See App keys.
deviceIdyesYour stable per-install key. Max 200 chars.
platformyes"ios", "android" or "web".
subscriberIdnoYour logged-in user's id. Max 256 chars; numbers are coerced. Omit for an anonymous device.
appVersionnoMax 50 chars.
timezonenoIANA zone, max 60 chars.
environmentno"production" (default), "staging" or "development". See Environments.
frameworknoA label for your stack, like "Flutter". Max 60 chars.
tokens.apnsTokenone token requirediOS only. Hex token, 64+ chars. Aliases: apns_token, apnToken.
tokens.fcmTokenone token requiredAndroid only. 32+ chars. Alias: fcm_token.
webPushone token requiredThe browser PushSubscription JSON — { endpoint, keys: { p256dh, auth }, expirationTime? } — with platform: "web". Alias: web_push. See Web push.

Tokens are capped at 400 characters (web endpoints at 1000). Expo push tokens are rejected with unsupported_token_type — that's 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 what happened to each token: "registered" (new), "refreshed" (already on this device) or "reassigned" (moved here from another device).

Rules

  • No duplicates. The unique keys are (app, deviceId) for devices and (app, token type, token) for tokens.
  • Omitted optional fields keep their stored value. Re-registering without subscriberId doesn't detach the device from its subscriber — deregister it instead.
  • Token rotation is normal. Register again with the same deviceId and the new token.
  • Registering clears a token's dead flag. Failure strikes are kept per device, so a token that moves to another device starts 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 }

Deregistering hard-deletes the device and its tokens. It's idempotent: an unknown deviceId answers 200 with "removed": false. Call it on logout, or to stop pushes to an install.

Errors

Errors use { "error": { "code", "message", "field"? } } and answer 400, except environment_disabled (403).

CodeMeaning
invalid_bodyThe body isn't a JSON object.
missing_fieldappId, deviceId or platform is missing.
invalid_fieldappId isn't a positive integer.
invalid_platformplatform isn't ios, android or web.
invalid_device_id, invalid_subscriber_idToo long or the wrong type.
invalid_tokenstokens isn't an object like { apnsToken, fcmToken }.
no_tokens_providedNo native token and no webPush subscription.
invalid_token_formatThe token doesn't match its shape, or is over 400 chars.
invalid_web_push_endpointwebPush.endpoint is missing, not an https URL, or over 1000 chars.
invalid_web_push_keysp256dh isn't a base64url 65-byte key, or auth isn't a base64url 16-byte secret.
invalid_web_pushwebPush isn't 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.
invalid_environmentenvironment isn't production, staging or development.
invalid_frameworkframework isn't 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 wrong token answers 401 with a plain-text message, like the rest of the API.

Check a registration with a test send to that deviceId.