Developers · API v1.0.0
Ionic (Capacitor) Integration
This guide is for Android and iOS apps built with Ionic + Capacitor (Angular, React, or Vue — it doesn't matter). For the general API reference, see the Overview.
Supported versions
| Component | Minimum | Recommended |
|---|---|---|
| Capacitor | 6 | latest |
@capacitor/push-notifications |
6.x | latest |
| Ionic Framework | 7 | latest |
| Android | 6.0 (API 23) | targetSdk 35+ |
| iOS | 13.0 | 15.0+ |
Legacy Cordova-based Ionic projects are not officially supported; we recommend migrating to Capacitor. PWA/web push is not supported in 1.0.0.
How it works
NotiPilot 1.0.0 delivers notifications through the Expo Push Service. Your app:
- Gets the native token with
@capacitor/push-notifications(Android: FCM token, iOS: APNs token), - Converts this token into an Expo Push Token using Expo's token service,
- Registers the Expo Push Token with NotiPilot via
register-device.
1. Prerequisites (one time)
- Android: Add your Android app in the Firebase console and place the
google-services.jsonfile inandroid/app/. - iOS: Enable the Push Notifications capability in Apple Developer and create an APNs
.p8key (steps: iOS guide). - Create a project on expo.dev, note its project ID, and upload the FCM V1 service account JSON (Android) and the APNs
.p8key (iOS) under Credentials. - Add your app in the NotiPilot dashboard: Expo Project ID, Expo Access Token, Android Package, iOS Bundle ID (=
appIdincapacitor.config.ts).
2. Installation
npm install @capacitor/push-notifications @capacitor/preferences @capacitor/app @capacitor/device
npx cap sync
iOS: In Xcode (npx cap open ios), go to App target → Signing & Capabilities and add Push Notifications and Background Modes → Remote notifications. Then add the following to ios/App/App/AppDelegate.swift:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}
Android: minSdkVersion = 23 must be set in android/variables.gradle.
capacitor.config.ts:
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'Example',
webDir: 'dist',
plugins: {
PushNotifications: { presentationOptions: ['badge', 'sound', 'alert'] },
},
};
3. src/notipilot.ts
import { Capacitor } from '@capacitor/core';
import { PushNotifications, type Token, type ActionPerformed } from '@capacitor/push-notifications';
import { Preferences } from '@capacitor/preferences';
import { App } from '@capacitor/app';
import { Device } from '@capacitor/device';
const NOTIPILOT_BASE_URL = 'https://app.notipilot.com/api/v1';
const APP_ID = 'YOUR-EXPO-PROJECT-ID'; // "Expo Project ID" from the dashboard
const UID_KEY = 'notipilot_device_uid';
type Attributes = Record<string, string | number | boolean | null>;
async function deviceUid(): Promise<string> {
const { value } = await Preferences.get({ key: UID_KEY });
if (value) return value;
const uid = crypto.randomUUID();
await Preferences.set({ key: UID_KEY, value: uid });
return uid;
}
async function post(path: string, body: object, attempt = 0): Promise<any> {
const res = await fetch(`${NOTIPILOT_BASE_URL}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json().catch(() => ({}));
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
await new Promise((r) => setTimeout(r, (Number(json.retry_after) || 2 ** attempt) * 1000));
return post(path, body, attempt + 1);
}
if (!res.ok) console.warn('[NotiPilot]', res.status, json.error, json.errors ?? json.message);
return json;
}
async function toExpoPushToken(nativeToken: string, uid: string): Promise<string> {
const isIOS = Capacitor.getPlatform() === 'ios';
const { id: bundleId } = await App.getInfo();
const res = await fetch('https://exp.host/--/api/v2/push/getExpoPushToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: isIOS ? 'apns' : 'fcm',
deviceId: uid.toLowerCase(),
development: isIOS && import.meta.env.DEV, // iOS debug builds use the APNs sandbox
appId: bundleId,
deviceToken: nativeToken,
projectId: APP_ID,
}),
});
if (!res.ok) throw new Error(`Expo token exchange failed: ${res.status}`);
return (await res.json()).data.expoPushToken;
}
/** Call on app launch. */
export async function initNotiPilot(extraAttributes: Attributes = {}) {
if (!Capacitor.isNativePlatform()) return;
await PushNotifications.addListener('registration', async (token: Token) => {
try {
const uid = await deviceUid();
const expoToken = await toExpoPushToken(token.value, uid);
const { languageCode } = await Device.getLanguageCode();
const { version } = await App.getInfo();
const region = new Intl.Locale(navigator.language).region ?? null;
await post('/register-device', {
app_id: APP_ID,
device_uid: uid,
token: expoToken,
platform: Capacitor.getPlatform(), // 'ios' | 'android'
provider: 'expo',
attributes: {
locale: languageCode.slice(0, 2), // "tr"
country: region, // "TR"
app_version: version,
...extraAttributes, // e.g. { city: 'Istanbul' }
},
});
} catch (e) {
console.warn('[NotiPilot] register failed', e);
}
});
await PushNotifications.addListener('registrationError', (err) =>
console.warn('[NotiPilot] native registration error', err),
);
// Notification tapped — custom data sent from the dashboard
await PushNotifications.addListener('pushNotificationActionPerformed', (action: ActionPerformed) => {
const raw = action.notification.data?.body;
const data = typeof raw === 'string' ? safeJson(raw) : raw ?? action.notification.data;
// e.g. data.screen === 'product' → router.navigate(...)
});
if (Capacitor.getPlatform() === 'android') {
await PushNotifications.createChannel({ id: 'default', name: 'General', importance: 5 });
}
let perm = await PushNotifications.checkPermissions();
if (perm.receive === 'prompt') perm = await PushNotifications.requestPermissions();
if (perm.receive === 'granted') await PushNotifications.register();
}
/** Call when the user signs in. */
export async function identify(externalId: string, attributes?: Attributes) {
return post('/identify-device', {
app_id: APP_ID,
device_uid: await deviceUid(),
external_id: externalId,
...(attributes ? { attributes } : {}),
});
}
function safeJson(s: string) {
try { return JSON.parse(s); } catch { return {}; }
}
import.meta.env.DEVis for Vite. If you use Angular, use!environment.production; in Webpack-based projects, useprocess.env.NODE_ENV !== 'production'.
Calling PushNotifications.register() on every app launch fires the registration event again, so token changes are automatically forwarded to NotiPilot.
4. Usage
Angular (app.component.ts):
constructor() { initNotiPilot({ city: 'Istanbul' }); }
React (App.tsx):
useEffect(() => { initNotiPilot({ city: 'Istanbul' }); }, []);
Vue (App.vue):
onMounted(() => initNotiPilot({ city: 'Istanbul' }));
When the user signs in: await identify(user.id);
Checklist
-
google-services.jsonadded (Android), Push capabilities enabled (iOS) - Capacitor push methods added to iOS
AppDelegate.swift - FCM V1 and APNs credentials uploaded to the Expo project
- Expo Project ID in the dashboard =
APP_ID, Bundle ID / Package =capacitor.config.ts→appId -
countryand, if possible,citysent as attributes
Common issues
| Symptom | Solution |
|---|---|
The registration event never fires (iOS) |
The AppDelegate.swift methods are missing or the capability is disabled. |
| The app crashes on Android | google-services.json is missing. |
Token conversion returns 4xx |
Check the projectId and appId (bundle ID) values. |
404 unknown_app |
The app_id is not registered in the dashboard. |