# Quickstart

Register an APNs, FCM or browser push token with the Native Notify API — Swift, Kotlin, Jetpack Compose, React Native, Expo, Flutter and web examples — then send your first push notification.

Register a device and send yourself a push notification. About 10 minutes on web or Android; the first iOS setup adds Apple-console time. Everything is plain HTTPS — no SDK.

## 1. Create your app

*About 2 minutes.* Create an app in the [dashboard](https://dashboard.nativenotify.com), then open **App Settings → App keys**. Every request carries that app's **app id** and **app token** — in the body for POSTs, in the URL for GETs.

## 2. Save your push credentials

*About 5 minutes with the keys in hand — web push needs none, so web-only setups can skip straight to step 3.*

Real sends go out with **your** push credentials: an **FCM v1 service-account JSON** for Android and an **Apple APNs `.p8` key** (with its Key ID, your Team ID and the app's bundle id) for iOS. Save them under **App Settings → Push credentials**. Each save is verified live against Apple and Google. Where to get each one: [Push Credentials](/docs/push/credentials).

## 3. Register a device

*About 5 minutes — this is the step your app runs at launch.*

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

#### iOS

```json
{
  "appId": 123,
  "appToken": "yourAppToken",
  "deviceId": "A1B2C3D4-install-key",
  "platform": "ios",
  "tokens": { "apnsToken": "0c9d8e…" }
}
```

#### Android

```json
{
  "appId": 123,
  "appToken": "yourAppToken",
  "deviceId": "A1B2C3D4-install-key",
  "platform": "android",
  "tokens": { "fcmToken": "d7Fk2…" }
}
```

#### Web

```json
{
  "appId": 123,
  "appToken": "yourAppToken",
  "deviceId": "A1B2C3D4-install-key",
  "platform": "web",
  "tokens": {
    "webPush": {
      "endpoint": "https://fcm.googleapis.com/fcm/send/…",
      "keys": { "p256dh": "BNc…", "auth": "k9X…" }
    }
  }
}
```

Registration stores two things for the device: the **device id** you generate once and keep, and the platform's **push token** — APNs on iOS, FCM on Android, a `PushSubscription` on the web. The tabs above show the exact `platform` and `tokens` shape for each. Registration is an idempotent upsert — call it on every app launch and whenever the token changes.

### Examples

#### Swift

Ask for permission, let APNs hand you the device token, and register it:

```swift title="AppDelegate.swift"
import UIKit
import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
            guard granted else { return }
            DispatchQueue.main.async { application.registerForRemoteNotifications() }
        }
        return true
    }

    // APNs delivers the device token here.
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let apnsToken = deviceToken.map { String(format: "%02x", $0) }.joined()
        Task { await register(apnsToken: apnsToken) }
    }

    func register(apnsToken: String) async {
        var request = URLRequest(url: URL(string: "https://app.nativenotify.com/api/universal/device/register")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try? JSONSerialization.data(withJSONObject: [
            "appId": 123,
            "appToken": "yourAppToken",
            "deviceId": DeviceIdentity.current,   // a stable id you persist (Keychain, UserDefaults)
            "platform": "ios",
            "tokens": ["apnsToken": apnsToken]
        ])
        _ = try? await URLSession.shared.data(for: request)   // 201 = registered
    }
}
```

The full Swift walkthrough — including the stable `deviceId` and tap handling — is the [Swift recipe](/docs/push/recipes/swift).

#### Kotlin

Firebase Cloud Messaging hands you the token, `onNewToken` gives you every rotation:

```kotlin title="NativeNotifyPush.kt"
import com.google.firebase.messaging.FirebaseMessagingService
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import org.json.JSONObject
import java.util.UUID

class NativeNotifyPush : FirebaseMessagingService() {

    override fun onNewToken(token: String) = register(token)

    private fun register(fcmToken: String) {
        val prefs = getSharedPreferences("native-notify", MODE_PRIVATE)
        val deviceId = prefs.getString("deviceId", null)
            ?: UUID.randomUUID().toString().also { prefs.edit().putString("deviceId", it).apply() }

        val body = JSONObject()
            .put("appId", 123)
            .put("appToken", "yourAppToken")
            .put("deviceId", deviceId)          // the same id on every launch
            .put("platform", "android")
            .put("tokens", JSONObject().put("fcmToken", fcmToken))

        val request = Request.Builder()
            .url("https://app.nativenotify.com/api/universal/device/register")
            .post(body.toString().toRequestBody("application/json".toMediaType()))
            .build()

        OkHttpClient().newCall(request).enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) { response.close() }   // 201 = registered
            override fun onFailure(call: Call, e: IOException) { /* retry on the next launch */ }
        })
    }
}
```

Android pushes arrive as FCM **data** messages, so your app posts the notification itself — the [Kotlin recipe](/docs/push/recipes/kotlin) shows the handler.

#### Jetpack Compose

Same Firebase token as Kotlin — the Compose differences are the Android 13+ permission prompt and a composable entry point:

```kotlin title="HomeScreen.kt"
import android.Manifest
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import com.google.firebase.messaging.FirebaseMessaging
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import org.json.JSONObject

@Composable
fun HomeScreen() {
    val context = LocalContext.current

    // Android 13+ will not show a notification until the permission is granted.
    val askPermission = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { }

    LaunchedEffect(Unit) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            askPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
        FirebaseMessaging.getInstance().token.addOnSuccessListener { fcmToken ->
            register(context, fcmToken)
        }
    }
}

private fun register(context: Context, fcmToken: String) {
    val prefs = context.getSharedPreferences("native-notify", Context.MODE_PRIVATE)
    val deviceId = prefs.getString("deviceId", null)
        ?: UUID.randomUUID().toString().also { prefs.edit().putString("deviceId", it).apply() }

    val body = JSONObject()
        .put("appId", 123)
        .put("appToken", "yourAppToken")
        .put("deviceId", deviceId)          // the same id on every launch
        .put("platform", "android")
        .put("tokens", JSONObject().put("fcmToken", fcmToken))

    val request = Request.Builder()
        .url("https://app.nativenotify.com/api/universal/device/register")
        .post(body.toString().toRequestBody("application/json".toMediaType()))
        .build()

    OkHttpClient().newCall(request).enqueue(object : Callback {
        override fun onResponse(call: Call, response: Response) { response.close() }   // 201 = registered
        override fun onFailure(call: Call, e: IOException) { /* retry on the next launch */ }
    })
}
```

The Gradle packages and the full screen wiring are in the [Jetpack Compose recipe](/docs/push/recipes/jetpack-compose).

#### React Native

`@react-native-firebase/messaging` gives you the token and every rotation:

```js title="push.js"
import messaging from "@react-native-firebase/messaging";
import { Platform } from "react-native";
import { getOrCreateDeviceId } from "./device-id";   // persist one id (AsyncStorage, Keychain)

export async function registerDevice() {
  await messaging().requestPermission();

  // iOS: the raw APNs token (null until APNs has delivered it).
  // Android: the FCM registration token.
  const token = Platform.OS === "ios"
    ? await messaging().getAPNSToken()
    : await messaging().getToken();
  if (!token) return;

  await fetch("https://app.nativenotify.com/api/universal/device/register", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      appId: 123,
      appToken: "yourAppToken",
      deviceId: await getOrCreateDeviceId(),
      platform: Platform.OS,          // "ios" | "android"
      tokens: Platform.OS === "ios" ? { apnsToken: token } : { fcmToken: token },
    }),
  });   // 201 = registered
}

// Every rotation registers the new token against the same deviceId.
messaging().onTokenRefresh(() => registerDevice());
```

Packages (`@react-native-firebase/app`, `@react-native-firebase/messaging`) and the Notifee display handler: [Bare React Native recipe](/docs/push/recipes/react-native).

#### Expo

`expo-notifications` returns the platform's own token — the exact one Native Notify sends to:

```js title="push.js"
import * as Notifications from "expo-notifications";
import { getOrCreateDeviceId } from "./device-id";   // persist one id (AsyncStorage)

export async function registerDevice() {
  const { type, data: token } = await Notifications.getDevicePushTokenAsync();
  // type "ios"     → the raw APNs token
  // type "android" → the FCM registration token

  await fetch("https://app.nativenotify.com/api/universal/device/register", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      appId: 123,
      appToken: "yourAppToken",
      deviceId: await getOrCreateDeviceId(),
      platform: type,                  // "ios" | "android"
      tokens: type === "ios" ? { apnsToken: token } : { fcmToken: token },
    }),
  });   // 201 = registered
}
```

Needs a development or EAS build — Expo Go cannot receive real pushes. Install with `npx expo install expo-notifications`; full walkthrough: [Expo recipe](/docs/push/recipes/expo).

#### Flutter

`firebase_messaging` hands you the token, and `onTokenRefresh` gives you every rotation:

```dart title="push.dart"
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:http/http.dart' as http;

Future<void> registerDevice() async {
  await FirebaseMessaging.instance.requestPermission();

  // iOS: the raw APNs token (null until APNs has delivered it).
  // Android: the FCM registration token.
  final String? token = Platform.isIOS
      ? await FirebaseMessaging.instance.getAPNSToken()
      : await FirebaseMessaging.instance.getToken();
  if (token == null) return;

  final response = await http.post(
    Uri.parse('https://app.nativenotify.com/api/universal/device/register'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'appId': 123,
      'appToken': 'yourAppToken',
      'deviceId': await getOrCreateDeviceId(),   // persist one id (shared_preferences)
      'platform': Platform.isIOS ? 'ios' : 'android',
      'tokens': Platform.isIOS ? {'apnsToken': token} : {'fcmToken': token},
    }),
  );   // 201 = registered

  // Every rotation registers the new token against the same deviceId.
  FirebaseMessaging.instance.onTokenRefresh.listen((next) => registerDevice());
}
```

Packages (`flutter pub add firebase_core firebase_messaging http`) and the Android display handler: [Flutter recipe](/docs/push/recipes/flutter).

#### Web

Ask for permission from a click, subscribe the browser, and register the subscription. The web snippet authenticates with the app's **publishable web key** (`nnweb_…`, from **App Settings → App keys**) — it is safe in page source, and the app token is not:

```js title="web-push.js"
const API = "https://app.nativenotify.com";
const appId = 123;
const webKey = "nnweb_…";   // publishable web key — register/deregister this one browser

export async function registerWebPush() {
  const permission = await Notification.requestPermission();   // must be called from a click
  if (permission !== "granted") return;

  await navigator.serviceWorker.register("/sw.js");   // the worker below
  const registration = await navigator.serviceWorker.ready;

  const { vapid } = await fetch(`${API}/api/universal/web-push/keys/${appId}/${webKey}`).then((r) => r.json());
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapid.publicKey),
  });

  await fetch(`${API}/api/universal/device/register`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      appId,
      appToken: webKey,                  // the web key rides in the appToken field
      deviceId: getOrCreateDeviceId(),
      platform: "web",
      webPush: subscription.toJSON(),
    }),
  });
}

function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
```

Serve this service worker at your site root (`/sw.js`) so its scope covers the site:

```js title="public/sw.js"
self.addEventListener("push", (event) => {
  const payload = event.data ? event.data.json() : {};
  event.waitUntil(
    self.registration.showNotification(payload.title || "Notification", {
      body: payload.body || "",
      image: payload.image,      // the send's bigPictureURL, when one was set
      data: payload.data || {},  // your pushData (plus nn_notification_id)
    })
  );
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  const url = new URL(event.notification.data?.url || "/", self.location.origin).href;
  event.waitUntil(clients.openWindow(url));
});
```

Web push needs HTTPS (localhost is the dev exception), and on iPhone/iPad the site must be **added to the Home Screen** before Safari can receive push. The [Web Push Recipe](/docs/push/web-push) covers rotation, deep links and the rest.

## 4. Send yourself a test push

*About 1 minute.* Send to the device you just registered and read the honest per-transport result:

#### cURL

```bash
curl -X POST https://app.nativenotify.com/api/universal/test-send \
  -H "Content-Type: application/json" \
  -d '{
    "appId": 123,
    "appToken": "yourAppToken",
    "title": "Hello from Native Notify",
    "message": "If you can read this, APNs/FCM accepted it.",
    "deviceId": "YOUR_DEVICE_ID"
  }'
```

#### Node.js

```js
const res = await fetch("https://app.nativenotify.com/api/universal/test-send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    appId: 123,
    appToken: "yourAppToken",
    title: "Hello from Native Notify",
    message: "If you can read this, APNs/FCM accepted it.",
    deviceId: "YOUR_DEVICE_ID",
  }),
});
const result = await res.json();
console.log(result.result, "via", result.transport);   // "accepted" via "apns" — or the reason, in plain English
```

#### Python

```python
import requests

result = requests.post(
    "https://app.nativenotify.com/api/universal/test-send",
    json={
        "appId": 123,
        "appToken": "yourAppToken",
        "title": "Hello from Native Notify",
        "message": "If you can read this, APNs/FCM accepted it.",
        "deviceId": "YOUR_DEVICE_ID",
    },
    timeout=30,
).json()

print(result["result"], "via", result["transport"])  # "accepted" via "apns" — or the reason, in plain English
```

## 5. Send to everyone

*About 1 minute.* Reach every registered device — change the audience:

#### cURL

```bash
curl -X POST https://app.nativenotify.com/api/universal/notifications/send \
  -H "Content-Type: application/json" \
  -d '{
    "appId": 123,
    "appToken": "yourAppToken",
    "title": "Hello everyone",
    "message": "Our first push notification.",
    "audience": { "type": "all" }
  }'
```

#### Node.js

```js
const res = await fetch("https://app.nativenotify.com/api/universal/notifications/send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    appId: 123,
    appToken: "yourAppToken",
    title: "Hello everyone",
    message: "Our first push notification.",
    audience: { type: "all" },
  }),
});
const send = await res.json();
console.log(send.accepted, "accepted of", send.attempted);
```

#### Python

```python
import requests

send = requests.post(
    "https://app.nativenotify.com/api/universal/notifications/send",
    json={
        "appId": 123,
        "appToken": "yourAppToken",
        "title": "Hello everyone",
        "message": "Our first push notification.",
        "audience": {"type": "all"},
    },
    timeout=30,
).json()

print(send["accepted"], "accepted of", send["attempted"])
```

## Stuck?

| Symptom                                             | Likely cause                                                                                                                                                                                                                                          |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`, invalid app id or token                      | The `appToken` is wrong — or a publishable web key (`nnweb_…`) was used from a native device. The web key belongs only in the `appToken` field of a `platform: "web"` registration.                                                                   |
| `400 invalid_token_format`                          | The token isn't the raw APNs or FCM string. Expo push tokens are rejected by this API — pass the native token (`getDevicePushTokenAsync()`); see the [Expo recipe](/docs/push/recipes/expo).                                                          |
| `400 token_platform_mismatch`                       | `platform` and the token field disagree: `apnsToken` → `ios`, `fcmToken` → `android`, `webPush` → `web`.                                                                                                                                              |
| test-send refused with a `reasonCode`               | The transport rejected it — nearly always the credential from step 2 (bundle id, team id, or a sandbox/production mix-up). [Verify Delivery](/docs/push/verification) reads every reason.                                                             |
| Accepted, but nothing shows on Android              | FCM delivers **data** messages, so your app displays them itself — see the display section of your platform guide: [Kotlin](/docs/push/recipes/kotlin), [Jetpack Compose](/docs/push/recipes/jetpack-compose), [Flutter](/docs/push/recipes/flutter). |
| Accepted, but nothing shows on iPhone or iPad (web) | Safari delivers web push only to sites **added to the Home Screen** first.                                                                                                                                                                            |
| `403 plan_upgrade_required`                         | The plan doesn't include that audience — Premium allows one subscriber or `"all"`, not groups or device lists. See [Plans & Billing](/docs/billing).                                                                                                  |

## Next steps

- [Device Registration](/docs/push/registration) — every field, response and error code.
- [Send Notifications](/docs/push/sending) — audiences, rich fields and the send report.
- [Verify Delivery](/docs/push/verification) — test sends, device counts and credential checks.
- [Framework Recipes](/docs/push/recipes) — Swift, Kotlin, Jetpack Compose, Expo, bare React Native and Flutter walkthroughs.
- [Notification inbox](/docs/push/web-inbox) — read and manage each device's inbox, with unread counts.
- [Analytics](/docs/analytics) — active users, screens, sessions and push opens.
