Skip to main content
Version: 1.2

9.3 Device Tokens

Controller: DeviceTokenController · Route: api/v1/DeviceToken · GroupName: notifications · 9 endpoints · Registers and manages FCM device tokens for push notifications. Full lifecycle detail in Chapter 11 — Device Tokens.

Verb + routePermissionRequestResponse (data)
POST GetDataTableDeviceTokens.ViewAllDtParametersDtResult<DeviceTokenDto> (unwrapped)
GET GetDetails/{id}DeviceTokens.ViewDeviceTokenDetailsDto
GET GetEdit/{id}DeviceTokens.EditDeviceTokenEditDto
GET GetUserDevices/{userId}DeviceTokens.ViewDeviceTokenViewListDto[]
POST RegisterDeviceTokens.RegisterDeviceTokenEditDtoGuid
PUT Update/{id}DeviceTokens.EditDeviceTokenEditDtoGuid
DELETE Delete?restore=DeviceTokens.DeleteDeleteDto[]Guid
PUT ActiveDeviceTokens.ActiveDeleteDto[]Guid
PUT DeactiveDeviceTokens.DeactiveDeleteDto[]Guid

POST Register — the endpoint every mobile client calls on login/token refresh

Request (DeviceTokenEditDto — note there is no userId field; the server always uses the authenticated caller's own ID):

{
"id": "00000000-0000-0000-0000-000000000000",
"isActive": true,
"displayOrder": 0,
"deviceId": "a1b2c3d4-device-uuid-from-os-keychain",
"platform": 1,
"token": "fcm-token-string-from-firebase-sdk"
}

platform: Android=1, Ios=2, Web=3.

Response:

{ "succeeded": true, "message": "DeviceToken is created successfully", "data": "9c2d3e50-0000-0000-0000-000000000001" }

(or "DeviceToken is updated successfully" if an existing row for this UserId + DeviceId was reactivated/refreshed — see the upsert logic below.)

Business notes — upsert-by-(UserId, DeviceId), not simple insert: DeviceTokenService.RegisterAsync looks up any existing row (including soft-deleted ones, via IgnoreQueryFilters()) matching UserId == currentUser and DeviceId == request.DeviceId:

  • If found: updates Token, Platform, LastSeenOn = now, forces IsActive = true, and — if the row was previously soft-deleted — reverses the soft delete (Is_Deleted = false, DeletedOn/DeletedBy = null). This is exactly what happens on a normal FCM token refresh, or a user who re-registers after having removed the app.
  • If not found: creates a new row with a fresh NewId.Next().ToGuid(), UserId = currentUser, LastSeenOn = now.

UserId is a globally unique GUID, so a cross-tenant (UserId, DeviceId) collision is not possible even though this lookup uses IgnoreQueryFilters() (bypassing the tenant filter is safe here specifically because of that global uniqueness — this is not a general license to bypass tenant filters elsewhere, see .claude/rules/multitenancy.md).

Angular (web push): call Register after obtaining an FCM web token via the Firebase JS SDK, and again whenever the SDK reports a token refresh event. Flutter: call Register on app start (after login) and on FirebaseMessaging.instance.onTokenRefresh. Use a stable per-install deviceId (not the FCM token itself) so re-registration on token refresh naturally upserts rather than creating duplicate rows. Backend: feeds IDeviceTokenService.GetActiveTokensForUserAsync(userId), which is what FirebasePushNotificationService.SendToMultipleAsync (via the Push channel provider) queries at dispatch time — see §7.4.

GET GetUserDevices/{userId}

Returns only IsActive == true && Is_Deleted != true devices for the given user, ordered by LastSeenOn descending — i.e. most recently active device first.

{
"succeeded": true,
"message": null,
"data": [
{ "id": "9c2d3e50-0000-0000-0000-000000000001", "deviceId": "a1b2c3d4-device-uuid", "platform": 1, "platformName": "Android", "token": "fcm-token-string" }
]
}

Possible errors: empty array (not an error) if the user has no active devices. Related APIs: Push channel, Chapter 11.