Devices API
Register and manage device tokens for push notifications
Overview
The Devices API allows you to register and manage device tokens for push notifications. When users install your mobile app, you register their device token with Flameup to enable push messaging. Web push is supported alongside iOS and Android.
devices:register or devices:write. Listing devices accepts devices:read or devices:write. devices:register deliberately cannot list — see the table below.devices:* wildcard and full access * also work.| Scope | Register / Unregister | List devices |
|---|---|---|
devices:register | Yes | No |
devices:write | Yes | Yes |
devices:read | No | Yes |
devices:register for keys embedded in a mobile app. It lets the app enrol the device it is running on without being able to enumerate every device token in the workspace — an isolation devices:write cannot provide. See the Flutter guide.Supported Platforms
Flameup supports push notifications on three platforms:
iOS
Apple Push Notification service (APNs) via Firebase Cloud Messaging
Android
Firebase Cloud Messaging (FCM) for Android devices
Web
Web Push API via Firebase Cloud Messaging
Device Token Object
A device token in Flameup has the following structure:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "866911cd-f4a2-4aff-99b1-3ba383f33c3e",
"person_id": "550e8400-e29b-41d4-a716-446655440001",
"device_token": "fMhT8K9x...(FCM token)",
"platform": "ios",
"device_name": "iPhone 15 Pro",
"device_model": "iPhone16,1",
"os_version": "17.2",
"app_version": "2.1.0",
"is_active": true,
"is_valid": true,
"last_used_at": "2024-01-20T14:45:00Z",
"registration_count": 3,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-20T14:45:00Z"
}
Request Fields
These are the fields you send when registering a device. Note that they are not the same as the response fields shown above.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | FCM/APNs token from the device. Must be 10–4096 characters |
platform | string | Yes | ios, android or web |
device_name | string | No | Human-readable device name |
device_model | string | No | Device model identifier |
os_version | string | No | Operating system version |
app_version | string | No | Your app's version |
token, but the API returns it as device_token. Sending device_token in a registration request fails with 400 — the field is ignored and the required token is then missing.userId you send to /identify and /track. It is not the Flameup person UUID. Pass it in the URL path; an identifier in the request body is ignored.Register a Device
Register a device token to enable push notifications for a user.
Endpoint: POST /api/v1/people/{external_id}/devices
// After obtaining FCM token in your iOS app
async function registerIOSDevice(externalId, fcmToken, deviceInfo) {
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${externalId}/devices`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
token: fcmToken,
platform: 'ios',
device_name: deviceInfo.name, // e.g., "John's iPhone"
device_model: deviceInfo.model, // e.g., "iPhone16,1"
os_version: deviceInfo.osVersion, // e.g., "17.2"
app_version: deviceInfo.appVersion // e.g., "2.1.0"
})
}
);
return response.json();
}
Swift Example (getting the FCM token):
import FirebaseMessaging
Messaging.messaging().token { token, error in
if let token = token {
// Send this token to your backend
registerDevice(fcmToken: token)
}
}
// After obtaining FCM token in your Android app
async function registerAndroidDevice(externalId, fcmToken, deviceInfo) {
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${externalId}/devices`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
token: fcmToken,
platform: 'android',
device_name: deviceInfo.name,
device_model: deviceInfo.model, // e.g., "Pixel 8 Pro"
os_version: deviceInfo.osVersion, // e.g., "14"
app_version: deviceInfo.appVersion
})
}
);
return response.json();
}
Kotlin Example (getting the FCM token):
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
// Send this token to your backend
registerDevice(fcmToken = token)
}
}
pk_live_ public key holding
devices:register — never a ws_live_ secret key. Treat it as published.
If you would rather publish nothing, proxy registration through your own
server and keep the secret key there.// PUBLIC_KEY is a pk_live_ key with devices:register. It ships to the
// browser; that is expected. A public key is limited to the append-only
// scopes — events:write, devices:register, people:identify — and can never
// hold a read scope.
async function registerWebDevice(externalId, fcmToken) {
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${externalId}/devices`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${PUBLIC_KEY}`
},
body: JSON.stringify({
token: fcmToken,
platform: 'web',
device_name: navigator.userAgent.includes('Chrome')
? 'Chrome Browser'
: 'Web Browser',
os_version: navigator.platform,
app_version: '1.0.0'
})
}
);
return response.json();
}
Web Example (getting the FCM token):
import { getToken } from 'firebase/messaging';
const token = await getToken(messaging, {
vapidKey: 'your-vapid-key'
});
// Send this token to your backend
await registerWebDevice(currentUserId, token);
curl -X POST "https://api.flameup.ai/api/v1/people/{external_id}/devices" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"token": "fMhT8K9x...your_fcm_token",
"platform": "ios",
"device_name": "iPhone 15 Pro",
"device_model": "iPhone16,1",
"os_version": "17.2",
"app_version": "2.1.0"
}'
response = requests.post(
f'https://api.flameup.ai/api/v1/people/{external_id}/devices',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
},
json={
'token': fcm_token,
'platform': 'ios',
'device_name': 'iPhone 15 Pro',
'device_model': 'iPhone16,1',
'os_version': '17.2',
'app_version': '2.1.0'
}
)
device = response.json()
Response (200 OK):
{
"success": true,
"device_id": "550e8400-e29b-41d4-a716-446655440000",
"message": "Device registered successfully"
}
When the external_id is not yet a known person, registration creates one
— a minimal profile with just that identifier — and the response then carries
"person_created": true. Its absence means the person already existed; seeing
it on an identifier you believed established means you found a typo, not a new
user.
device_id, not the full device object. Use the List Devices endpoint to retrieve full device details.Registration moves the token
Registration is an atomic upsert keyed on (workspace, token). If the token is
already registered — to anyone — the existing row is reassigned to the person
in the URL, reactivated (is_active and is_valid back to true), and its
registration_count bumped. This is the mechanic the login/logout flow relies
on: when a different user logs in on the same handset, re-registering the
device's token rebinds the device to them, and the previous user stops
receiving pushes on it.
Registration errors:
| Status | When |
|---|---|
400 | token outside 10–4096 chars, or platform not ios/android/web |
404 | The external_id is whitespace-only, or equals an existing person's Flameup UUID — deliberately refused so devices never bind to raw UUIDs |
409 | Cannot register device for inactive person |
Note what is not on that list: an unknown external_id is not an error —
registration creates the person, as above.
List Devices
Retrieve all devices for a person.
Endpoint: GET /api/v1/people/{external_id}/devices
This endpoint returns all devices for the person. It takes no query parameters and is not paginated.
// Get all devices for a user
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${externalId}/devices`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
const { devices } = await response.json();
console.log(`User has ${devices.length} registered devices`);
response = requests.get(
f'https://api.flameup.ai/api/v1/people/{external_id}/devices',
headers={'Authorization': f'Bearer {API_KEY}'}
)
devices = response.json()['devices']
curl "https://api.flameup.ai/api/v1/people/{external_id}/devices" \
-H "Authorization: Bearer your_api_key"
Response (200 OK):
{
"success": true,
"devices": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "866911cd-f4a2-4aff-99b1-3ba383f33c3e",
"person_id": "550e8400-e29b-41d4-a716-446655440001",
"device_token": "fMhT8K9x...",
"platform": "ios",
"is_active": true,
"is_valid": true,
"last_used_at": "2024-01-20T14:45:00Z",
"registration_count": 3,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-20T14:45:00Z"
}
],
"count": 1
}
The count field is the number of devices returned. Devices are ordered by last_used_at (most recently used first), then by created_at descending.
Update a Device
Update device information (e.g., when the app version changes).
Simply call the register endpoint again with the same token and updated fields:
// Re-register with updated info
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${externalId}/devices`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
token: existingToken,
platform: 'ios',
app_version: '2.2.0',
os_version: '17.3'
})
}
);
# Re-register with updated info
response = requests.post(
f'https://api.flameup.ai/api/v1/people/{external_id}/devices',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
},
json={
'token': existing_token,
'platform': 'ios',
'app_version': '2.2.0',
'os_version': '17.3'
}
)
curl -X POST "https://api.flameup.ai/api/v1/people/{external_id}/devices" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{"token": "existing_fcm_token", "platform": "ios", "app_version": "2.2.0", "os_version": "17.3"}'
Delete a Device
Stop pushes to a device token (e.g., when user logs out or uninstalls).
Endpoint: DELETE /api/v1/people/{external_id}/devices/{token}
Delete is a soft invalidate: the row is kept with is_active and
is_valid set false, it still appears in List Devices, and re-registering
the same token reactivates it. That is what makes logout → login on the same
handset work without a fresh FCM token.
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${externalId}/devices/${encodeURIComponent(deviceToken)}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
if (response.ok) {
console.log('Device unregistered');
}
import urllib.parse
response = requests.delete(
f'https://api.flameup.ai/api/v1/people/{external_id}/devices/{urllib.parse.quote(device_token, safe="")}',
headers={'Authorization': f'Bearer {API_KEY}'}
)
curl -X DELETE "https://api.flameup.ai/api/v1/people/{external_id}/devices/{device_token}" \
-H "Authorization: Bearer your_api_key"
Token Lifecycle
Device tokens have a lifecycle that Flameup manages automatically:
Registration
Token is registered when user enables notifications or installs the app.
is_active: trueis_valid: true
Active Use
Token is used for push delivery. last_used_at is updated with each use.
Token Refresh
FCM may issue a new token. Re-register with the new token. Deduplication is by token, not by device: the rotated token inserts a new row, and the old token's row stays active until a delivery failure invalidates it or cleanup reaps it. FCM stops delivering to rotated-away tokens, so the stale row is harmless — but expect to see both in List Devices for a while.
Invalidation
If push delivery fails (uninstall, revoked permissions), token is marked:
is_valid: falseis_active: false
Handling Token Refresh
FCM tokens can change. Handle token refresh in your app:
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?) {
guard let token = fcmToken else { return }
// Re-register with Flameup
FlameupAPI.registerDevice(token: token)
}
}
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
// Re-register with Flameup
FlameupAPI.registerDevice(token)
}
}
import { getMessaging, getToken } from 'firebase/messaging';
const messaging = getMessaging();
// FCM rotates web tokens. Re-register the new one against the same
// person, using the helper from the registration tab above.
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.type === 'TOKEN_REFRESH') {
const newToken = await getToken(messaging, { vapidKey: 'your-vapid-key' });
await registerWebDevice(externalId, newToken);
}
});
Registering a refreshed token does not strand the old one: registration upserts on the token, and a token FCM has rotated away from stops being delivered to.
Best Practices
Register the device token after user login to associate it with the correct
person. Register with your own user id — the same identifier you send
to /identify — never the Flameup UUID, which the register route refuses:
async function onUserLogin(userId) {
const fcmToken = await getFCMToken();
await flare.identifyUser(userId);
await flare.registerDevice(userId, fcmToken);
}
If the token was previously registered to another user on this handset, registration rebinds it — see Registration moves the token.
Troubleshooting
| Issue | Solution |
|---|---|
| Token not registered | Check the token is valid (10–4096 chars) and the external_id is not whitespace or a Flameup UUID. An unknown id is not the cause — registration creates the person |
| Wrong platform | Ensure platform matches the device (ios/android/web) |
| Invalid token | Token may have expired; request a fresh token from FCM |
| User not receiving | Check is_active and is_valid are both true |
| Notification blocked | User may have disabled notifications in device settings |
403 Insufficient permissions | The API key lacks the scope for that route (devices:register/devices:write to register, devices:read/devices:write to list). Keys created in the dashboard cannot have any of them yet — create one via the API keys endpoint |
404 on the request itself | You are calling a workspace-scoped path. The API-key route is not workspace-scoped; see below |
Using the wrong request shape
The most common failure is a request built against the older workspace-scoped shape. It returns 404 (no such route) rather than a helpful validation error:
# WRONG — this route does not exist for API keys
curl -X POST "https://api.flameup.ai/api/v1/workspaces/{workspace_id}/devices" \
-H "Authorization: Bearer {API_KEY}" \
-d '{"person_id": "...", "device_token": "...", "platform": "android"}'
Three things differ from the correct call:
| Wrong | Correct |
|---|---|
Workspace in the URL: /workspaces/{workspace_id}/devices | No workspace in the URL: /people/{external_id}/devices. The workspace is resolved from the API key |
person_id in the JSON body | Your external_id in the URL path. An identifier in the body is ignored |
device_token field | token field. Required, 10–4096 characters |
# CORRECT
curl -X POST "https://api.flameup.ai/api/v1/people/user_12345/devices" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {API_KEY}" \
-d '{
"token": "eM_WXDy1SCWh9Op_tY46B3:APA91b...",
"platform": "android",
"device_name": "Pixel 7",
"device_model": "GVU6C",
"os_version": "15",
"app_version": "2.2.39"
}'
/api/v1/workspaces/{workspace_id}/people/{person_id}/devices) does exist, but it is authenticated with a dashboard session, not an API key — and it addresses the person by their Flameup UUID, not by external_id. A non-UUID person_id there returns 400 Invalid person ID. API-key integrations should always use the workspace-agnostic /api/v1/people/{external_id}/devices form shown above.