Register Device
Register a device (or browser) against a contact so it can receive push notifications, or refresh one that already exists.
Use this when you are migrating existing device tokens from another provider, or when your app cannot embed a client SDK. Apps that embed an SDK do not call this — the SDK registers automatically.
Endpoint
POST /push/devices
Returns: 200 OK
How it behaves
This is an upsert on (organization, installation_id). It is safe to call on every app launch — a repeat call refreshes the device token and attributes rather than creating a duplicate.
The contact must already exist. Unlike Send Event, registering a device never creates a contact.
A device revoked by Unregister Device — or by the user opting out in your app — stays revoked. Re-registering does not resurrect it; the response simply reports opted_out: true.
This is deliberate: re-registration happens constantly — on every app launch — and a revocation that any of those calls could undo would silently override the user's choice. For what happens across an app reinstall, see Unregister Device.
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <your-api-key> | Yes |
Content-Type | application/json | Yes |
Body Parameters
Identifying the contact
| Parameter | Type | Required | Description |
|---|---|---|---|
contact_id | string | Conditional | Arsel contact UUID. Mutually exclusive with email/phone_number. |
email | string | Conditional | Contact email. Matched case-insensitively. |
phone_number | string | Conditional | Contact phone in E.164 format. |
Identifying the device
| Parameter | Type | Required | Description |
|---|---|---|---|
installation_id | string | Yes | Your stable identifier for this app installation. This, not the device token, is the natural key — see below. |
platform | string | Yes | android, ios, or web. |
apns_environment | string | No | production or sandbox. iOS only, defaults to production. Send sandbox for debug and simulator builds — a token minted by one Apple host is rejected by the other, and nothing in the token itself says which. The SDKs report this for you. |
installation_id and not the device tokenFCM rotates device tokens, and auto-invalidates them after roughly 270 days of inactivity. If the token were the key, every rotation would look like a new device — and an opt-out recorded against the old token would be lost.
installation_id is whatever value stays stable for one app installation on one device. When migrating tokens from another provider and you have no such value, derive one deterministically from the token you are importing (for example a SHA-256 of it) so that re-running the import is idempotent.
Transport credentials
| Parameter | Type | Required for | Description |
|---|---|---|---|
device_token | string | android, ios | The FCM registration token on android; the APNs device token on ios. Max 4096 characters. |
endpoint | string | web | Web Push endpoint URL. Max 2048 characters. |
p256dh_key | string | web | Web Push P-256 ECDH public key. |
auth_key | string | web | Web Push auth secret. |
Device attributes
All optional, all used for targeting and diagnostics.
| Parameter | Type | Description |
|---|---|---|
enablement_status | string | OS notification permission as your app last observed it: AUTHORIZED, DENIED, NOT_DETERMINED, PROVISIONAL, UNAUTHORIZED. Omit rather than guess. |
sdk_version | string | Arsel SDK build on the device, e.g. android/1.2.0. Supply only if the SDK is embedded — see the warning below. |
app_version | string | Your app's version. |
os_version | string | Device OS version. |
device_model | string | e.g. Pixel 8. |
device_manufacturer | string | e.g. Google. |
device_timezone | string | IANA zone, e.g. Asia/Riyadh. Cannot be backfilled later — send it. |
device_locale | string | BCP-47, e.g. ar-SA. |
sdk_version for imported devicessdk_version is what marks a device as instrumented. Sending it for a device with no embedded Arsel SDK tells Arsel to expect delivery and engagement reports that will never arrive.
Omitting it is what makes a migrated device work correctly: the notification renders natively on the device with no SDK involved.
PROVISIONAL is not a denialOn iOS, PROVISIONAL means notifications are delivered quietly to Notification Centre without ever showing a prompt. Recording those devices as not-permitted undercounts your reachable audience.
{
"email": "john.doe@example.com",
"installation_id": "install-abc123",
"platform": "android",
"device_token": "fzXn2…J3x9",
"enablement_status": "AUTHORIZED",
"device_timezone": "Asia/Riyadh",
"device_locale": "ar-SA"
}
Response
{
"id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a",
"installation_id": "install-abc123",
"status": "ACTIVE",
"contact_id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a",
"opted_out": false,
"device_secret": "…"
}
| Field | Type | Description |
|---|---|---|
id | string | Device ID (UUIDv7) |
installation_id | string | Echoed back |
status | string | ACTIVE, EXPIRED, REVOKED, or FAILED |
contact_id | string | The contact the device is now bound to |
opted_out | boolean | true when a durable opt-out kept the device revoked despite this call |
suppressed | boolean | true when the device stayed retired because this call replayed a token the push service has already rejected as dead. Re-registering a dead token never revives it — send the new token the device was issued. |
device_secret | string | Returned exactly once, at creation. Present only on the call that created the device record. |
device_secret is shown onceOnly its SHA-256 is stored, so it can never be retrieved again. If your app embeds the SDK, relay this value to the app — it is the device's proof of possession when reporting delivery and engagement. If you are importing devices without an SDK, you can ignore it.
Examples
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/push/devices" \
-H "Authorization: Bearer be_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "john.doe@example.com",
"installation_id": "install-abc123",
"platform": "android",
"device_token": "fzXn2...J3x9"
}'
const response = await fetch("https://api.arsel.sa/v1/push/devices", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer be_your_api_key",
},
body: JSON.stringify({
email: "john.doe@example.com",
installation_id: "install-abc123",
platform: "android",
device_token: "fzXn2...J3x9",
}),
});
const device = await response.json();
console.log(device.status, device.opted_out);
import requests
response = requests.post(
"https://api.arsel.sa/v1/push/devices",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer be_your_api_key",
},
json={
"email": "john.doe@example.com",
"installation_id": "install-abc123",
"platform": "android",
"device_token": "fzXn2...J3x9",
},
)
device = response.json()
print(device["status"], device["opted_out"])
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer be_your_api_key");
var payload = new
{
email = "john.doe@example.com",
installation_id = "install-abc123",
platform = "android",
device_token = "fzXn2...J3x9"
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.arsel.sa/v1/push/devices", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
<?php
$ch = curl_init("https://api.arsel.sa/v1/push/devices");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer be_your_api_key"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"email" => "john.doe@example.com",
"installation_id" => "install-abc123",
"platform" => "android",
"device_token" => "fzXn2...J3x9"
]));
$response = curl_exec($ch);
echo $response;
curl_close($ch);
Migrating devices from another provider
Because Arsel sends under your own sender identity — your Firebase project on Android, your APNs key on iOS — the tokens you already hold stay valid. There is no re-registration and no app release required.
An ios device must carry an APNs device token. If your previous provider relayed iOS through Firebase, your export holds FCM registration tokens instead — those mean nothing to Apple and cannot be imported as iOS devices. Those users re-register on their own once they open a build carrying the Arsel SDK.
- Export your existing tokens, each with the contact identifier it belongs to.
- For each one, call this endpoint with
installation_idderived deterministically from the token, and omitsdk_version. - The device is immediately sendable, rendering notifications natively.
- When you later ship an app build embedding the Arsel SDK, it adopts the imported row rather than creating a second one.
For anything more than a handful of devices, use Bulk Register Devices instead of looping this route — it takes 1,000 per call, resolves contacts by external_id, and reports per-row errors you can reconcile against your export.
Error Responses
- 400 Credentials
- 400 Contact
- 404 Contact
- 422 Validation
- 401 Unauthorized
The transport credentials do not match the platform — e.g. platform: "web" with no endpoint:
{
"status_code": 400,
"name": "bad_request",
"message": "Missing transport credentials for the supplied platform."
}
{
"status_code": 400,
"name": "bad_request",
"message": "Provide at least one of contact_id, email, or phone_number."
}
Registering a device never creates a contact, so an unknown identifier is an error:
{
"status_code": 404,
"name": "not_found",
"message": "No contact in this organization matches the supplied reference."
}
{
"status_code": 422,
"name": "validation_error",
"message": "Validation failed"
}
{
"status_code": 401,
"name": "unauthorized",
"message": "Invalid or missing API key"
}