List Contact Devices
List every device a contact has registered, including revoked and expired ones.
Use this to answer support questions — "why didn't this customer get the notification?" — by checking device status, permission state, and whether the Arsel SDK is embedded.
Endpoint
GET /push/contacts/:contact_id/devices
Returns: 200 OK
Path Parameters
| Parameter | Type | Description |
|---|---|---|
contact_id | string | Arsel contact UUID |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
platform | string | all | Filter to android, ios, or web. |
status | string | all | Filter to ACTIVE, EXPIRED, REVOKED, or FAILED. Defaults to every status, revoked and expired included. |
limit | integer | 20 | Items per page, 1–100. |
after | string | — | Return items after this id (exclusive). |
before | string | — | Return items before this id (exclusive). |
See Pagination for how cursor paging works.
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <your-api-key> | Yes |
Response
{
"object": "list",
"has_more": false,
"data": [
{
"id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a",
"installation_id": "install-abc123",
"platform": "android",
"vendor": "fcm",
"status": "ACTIVE",
"revoke_reason": null,
"device_token_masked": "fzXn…J3x9",
"enablement_status": "AUTHORIZED",
"sdk_enabled": true,
"sdk_version": "android/1.2.0",
"app_version": "3.4.1",
"device_model": "Pixel 8",
"device_timezone": "Asia/Riyadh",
"device_locale": "ar-SA",
"last_used_at": "2026-06-01T12:00:00.000Z",
"last_seen_at": "2026-06-01T12:00:00.000Z",
"created_at": "2026-05-20T09:14:00.000Z"
}
]
}
| Field | Type | Description |
|---|---|---|
id | string | Device ID (UUIDv7) |
installation_id | string | The natural key. Correlate on this across every push route. |
platform | string | android, ios, web |
vendor | string | fcm, apns, web_push |
status | string | ACTIVE, EXPIRED, REVOKED, FAILED |
revoke_reason | string | null | user_opt_out, contact_opt_out, dead_token, or admin |
device_token_masked | string | null | First and last few characters only |
enablement_status | string | null | OS permission last reported by the device |
sdk_enabled | boolean | Whether the Arsel SDK is embedded on this device |
sdk_version | string | null | e.g. android/1.2.0 |
last_used_at | string | null | Last time Arsel sent to this device |
last_seen_at | string | null | Last time the device checked in |
The raw device token is never returned
device_token_masked shows only the first and last few characters. The full token is a sendable credential — anyone holding it and your Firebase project could push to that device — so Arsel never returns it, not even to the account that supplied it. Correlate on installation_id instead.
Reading revoke_reason
| Value | Meaning | Reversible? |
|---|---|---|
user_opt_out | The user opted out in your app | No — durable |
contact_opt_out | Revoked via Unregister Device | No — durable |
dead_token | The transport rejected the token permanently (uninstall, ~270 days inactive, or the token belongs to a different Firebase sender) | Yes — a successful re-register clears it |
admin | Revoked by Arsel staff | — |
Examples
- cURL
- JavaScript
- Python
- C#
- PHP
curl "https://api.arsel.sa/v1/push/contacts/01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a/devices?status=ACTIVE&limit=50" \
-H "Authorization: Bearer $ARSEL_API_KEY"
const response = await fetch("https://api.arsel.sa/v1/push/contacts/01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a/devices?status=ACTIVE&limit=50", {
headers: {
Authorization: `Bearer ${process.env.ARSEL_API_KEY}`,
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.name}: ${data.message}`);
}
console.log(data);
import os
import requests
response = requests.get(
"https://api.arsel.sa/v1/push/contacts/01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a/devices?status=ACTIVE&limit=50",
headers={"Authorization": f"Bearer {os.environ['ARSEL_API_KEY']}"},
)
response.raise_for_status()
print(response.json())
using System.Net.Http.Headers;
var client = new HttpClient(); // share one instance across requests
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ARSEL_API_KEY"));
var response = await client.GetAsync("https://api.arsel.sa/v1/push/contacts/01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a/devices?status=ACTIVE&limit=50");
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)response.StatusCode}: {body}");
Console.WriteLine(body);
<?php
$ch = curl_init("https://api.arsel.sa/v1/push/contacts/01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a/devices?status=ACTIVE&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ARSEL_API_KEY"),
],
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new Exception("{$data['name']}: {$data['message']}");
}
print_r($data);
Error Responses
- 404 Not Found
- 401 Unauthorized
{
"status_code": 404,
"name": "not_found",
"message": "Contact not found"
}
{
"status_code": 401,
"name": "unauthorized",
"message": "Invalid API Key"
}