Skip to main content

Mobile push notifications

Hand-off for the iOS and Android app. The backend already sends a push when an in-app notification is created. The app’s job is to obtain the Firebase Cloud Messaging token and tell the backend which user is logged in on that install.

Base URL:

https://events.pana.space/api

Authenticated calls use the access token from login:

Authorization: Bearer <access>
Content-Type: application/json

How the token works​

The FCM token identifies the app install, not the user. Firebase creates it. Login does not create a new one, and the same phone keeps the same token when a different person logs in.

The backend stores a mapping: one token belongs to one user at a time. One user can have many tokens (phone and tablet).

EventWhat the app does
App install / Firebase readyAsk Firebase for the token. Do not call the backend yet.
Login succeedsPOST /api/v1/devices/ with that token
Firebase rotates the token while logged inPOST /api/v1/devices/ again with the new token. If you still have the previous token, DELETE it first.
LogoutDELETE /api/v1/devices/ while the access token is still valid, then call logout

Do not generate a token per user. Do not register before login. Registering does not replay old notifications. The inbox API below is how the app loads history.

1. Login​

POST /api/v1/users/auth/login/
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret"
}

200

{
"access": "<jwt>",
"refresh": "<jwt>"
}

The response also includes the user profile fields. Keep access for the calls below. Keep refresh for logout.

2. Register the device​

Call this after every successful login, and again on onTokenRefresh while the user is still logged in.

POST /api/v1/devices/
Authorization: Bearer <access>
Content-Type: application/json
{
"fcm_token": "fcm-registration-token",
"platform": "ios"
}

platform is ios or android (any letter case; stored lowercase). fcm_token is required, trimmed, and at most 512 characters.

If this token is already stored for someone else, the row is reassigned to the current user. That is how a shared phone switches accounts.

200

{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"platform": "ios",
"last_active_at": "2026-10-08T14:30:00.123456+00:00"
}

The same 200 is returned for a first registration and for a repeat call.

400

{ "detail": "fcm_token is required." }
{ "detail": "platform must be ios or android." }

401 when the bearer token is missing or expired.

{ "detail": "Authentication credentials were not provided." }

3. Unregister on logout​

Call this before logout, with the same access token and the token currently on the device.

DELETE /api/v1/devices/
Authorization: Bearer <access>
Content-Type: application/json
{
"fcm_token": "fcm-registration-token"
}

204 empty body. The row is deleted only when it belongs to the current user.

404 when this user has no row for that token (already removed, or never registered).

{ "detail": "Not found." }

400 and 401 use the same detail shape as register.

Then log out:

POST /api/v1/users/auth/logout/
Authorization: Bearer <access>
Content-Type: application/json
{
"refresh": "<refresh token from login>"
}

200

{ "message": "Logged out successfully." }

If unregister is skipped, the next person who opens the app on that phone can receive the previous user’s pushes until someone else logs in and registers the same token.

4. What a push looks like​

A push is sent only when an in-app notification is created or updated for that user (the App channel is on for that category). Email-only preferences do not produce a push. A failed push does not block the in-app notification.

Firebase notification payload:

FieldSource
notification.titleNotification title (max 200 characters)
notification.bodyDescription, or the title if there is no description (max 1000 characters)

data values are all strings. Empty string means “none”.

KeyMeaning
notification_idUUID of the in-app notification
workspace_idWorkspace the event belongs to
categorytask, lead, demo_booking, event, expense, or admin
verbShort action name, for example assigned
target_urlIn-app path to open, for example /leads/<id>. May be empty.
target_typeRecord type. May be empty.
target_idUUID of that record. Empty string when there is no record.

Example data:

{
"notification_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"workspace_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"category": "lead",
"verb": "assigned",
"target_url": "/leads/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"target_type": "lead",
"target_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}

Use workspace_id and target_url to open the right screen. The device record itself is not tied to one workspace.

5. Inbox (history, not push)​

Push does not include notifications that already exist. Load those from the workspace inbox.

GET /api/v1/{workspace_id}/notifications/
Authorization: Bearer <access>
GET /api/v1/{workspace_id}/notifications/unread-count/

Mark one read:

PATCH /api/v1/{workspace_id}/notifications/{notification_id}/read/

Checklist​

  1. Configure the app with the same Firebase project the backend uses.
  2. On login, read the FCM token and POST /api/v1/devices/.
  3. On token refresh, register the new token (and delete the old one if you still have it).
  4. On logout, DELETE /api/v1/devices/ before POST /api/v1/users/auth/logout/.
  5. On a received push, route with workspace_id and target_url.
  6. Load older items from GET /api/v1/{workspace_id}/notifications/.