Skip to content

Activations API

SIM activation lifecycle — validate ICCID, create activation, verify identity, track status, and expose admin dashboard records.

SIM Types

Every activation carries a simType (physical | esim). It discriminates what the iccid field holds:

simTypeThe iccid field carriesFormat rule
physical (default)19-digit ICCID^\d{19}$ per CITC standard
esimLPA activation codeGSMA SGP.22 §4.1 — LPA: prefix optional, positional $-separated fields (3–6 segments), max 255 chars, e.g. LPA:1$SMDP.EXAMPLE.COM$04386-AGYFT-A74Y8

Requests without simType default to physical, so pre-eSIM clients are unaffected.

eSIM semantics (reseller model): activated means the profile was issued and delivered — the QR email is sent and the payload is available on the owner-scoped endpoints. Installing the profile is the user's step with the device OS and is not tracked. Once issued, the operator's payload is stored on the activation (esimProfile), and an operator-assigned profile ICCID replaces the stored serial when provided. The activation lane's esimProfile never carries an iccidSuffix — that field appears only on store-issued profiles (see docs/api/store.md). esimProfile stays null and the QR endpoint returns 404 until the activation reaches activated — the payload is never delivered while payment or processing is pending, even though the submitted code is stored earlier. The whole eSIM surface is gated by the FEATURE_ESIM env flag — when off, esim-typed requests fail with ACTIVATION_ESIM_DISABLED (422). Operator plugins must also declare supportsEsim: true; otherwise eSIM validation and B2C create fail with ACTIVATION_ESIM_UNSUPPORTED_OPERATOR (422). B2B seats are physical-only for now (same disabled code, see POST /activations).

Activation Status Flow

pending → in_progress → activated
                      ↘ failed
pending → cancelled

The lifecycle is identical for both SIM types.

Identity Status Flow (Nafath)

waiting → completed
        ↘ rejected
        ↘ expired
        ↘ error

POST /activations/validate-iccid

Validate a SIM serial. The operator plugin is the truth source for validity.

This endpoint does not require authentication — ICCID validation is a pre-check before creating an activation.

Request Body

FieldTypeRequiredDescription
iccidstringYes19-digit ICCID for physical, LPA activation code for esim (see SIM Types)
simTypestringNophysical (default) or esim

Query Parameters

ParameterTypeDefaultDescription
operatorstringmockOperator slug to validate against (stc, mobily, zain, mock)

Example Request

POST /activations/validate-iccid?operator=stc
Content-Type: application/json
json
{
  "iccid": "8996601212345678901"
}

eSIM variant:

json
{
  "iccid": "LPA:1$SMDP.EXAMPLE.COM$04386-AGYFT-A74Y8",
  "simType": "esim"
}

Response

200 OK — Valid ICCID:

json
{
  "valid": true,
  "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "operatorSlug": "stc",
  "simType": "physical"
}

200 OK — Invalid ICCID:

json
{
  "valid": false,
  "simType": "physical",
  "error": "ICCID not recognized by operator"
}

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Serial fails the simType's format rule (19 digits for physical, LPA grammar for esim)
ACTIVATION_ESIM_DISABLED422simType: esim while the FEATURE_ESIM gate is off
ACTIVATION_ESIM_UNSUPPORTED_OPERATOR422simType: esim requested against an operator plugin that did not explicitly declare supportsEsim: true
OPERATOR_NOT_FOUND404Requested operator slug is not registered — operator not supported (use a different operator)
ACTIVATION_ICCID_INVALID422ICCID validation failed against the operator plugin
ACTIVATION_FAILED422General activation failure
OPERATOR_UNAVAILABLE502Operator reachable but the validation call failed — a retry may help
OPERATOR_TIMEOUT504Operator API timed out

POST /activations

Create a new SIM activation order. Creates order + order item + activation in a single transaction.

Requires authentication.

Per T-04-06: userId is extracted from the JWT context — never from the request body.

B2B branch: when the JWT user belongs to a company, the same endpoint activates their assigned seat instead. Prepaid resolves the package and price from the roster's assigned contract line; flexible resolves them from the roster's own packageId + unitPriceSar snapshot instead (a flexible contract holds no lines). Either way the snapshot price is charged to the company — the order lands paid with a synthetic corporate payment — gated on a redeemed invite link, one-seat-one-SIM, and — for flexible contracts — the company's available balance. See the 409 codes below.

eSIM: with simType: esim the iccid field carries the LPA activation code. The code is decomposed and stored at create time; the operator refreshes the payload at issuance (settlement). The resolved operator plugin must declare supportsEsim: true, otherwise the request fails with ACTIVATION_ESIM_UNSUPPORTED_OPERATOR (422). Corporate (B2B) users cannot create esim activations yet — ACTIVATION_ESIM_DISABLED (422).

Request Body

FieldTypeRequiredDescription
iccidstringYes19-digit ICCID for physical, LPA activation code for esim (see SIM Types)
packageIdstringYesUUID of the selected package
simTypestringNophysical (default) or esim

Example Request

POST /activations
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
json
{
  "iccid": "8996601212345678901",
  "packageId": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e"
}

Response

201 Created

json
{
  "activationId": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "orderId": "d4e5f6a7-b8c9-0123-defa-456789012345",
  "status": "pending",
  "identityStatus": "waiting",
  "simType": "physical"
}

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Serial fails the simType's format rule, or missing packageId
ACTIVATION_CODE_INVALID422eSIM: the operator rejected the activation code
ACTIVATION_ESIM_DISABLED422eSIM while the FEATURE_ESIM gate is off, or an esim attempt by a corporate (B2B) user
ACTIVATION_ESIM_UNSUPPORTED_OPERATOR422eSIM requested against an operator plugin that did not explicitly declare supportsEsim: true
ACTIVATION_FAILED422Failed to create activation (e.g., SIM limit exceeded)
NOT_FOUND404Package not found
AUTH_REQUIRED401Missing or invalid authentication token
COMPANY_NOT_APPROVED403B2B: the seat's company is not currently approved. Re-checked here because the redeem-time gate cannot cover a seat redeemed before a suspension — suspension must block new provisioning, and this is the draw moment on flexible pay
ACTIVATION_PACKAGE_NOT_ON_CONTRACT409B2B: no roster entry, no assigned contract line, requested package ≠ assigned line, or the contract is not active
ACTIVATION_LINK_NOT_REDEEMED409B2B: the roster entry's invite link was never redeemed — OTP login alone cannot activate a staged seat
ACTIVATION_SEAT_ALREADY_USED409B2B: the seat already has a pending/in-progress/activated SIM (failed/cancelled allow retry)
COMPANY_BALANCE_EXCEEDED409B2B flexible: this seat's charge would exceed the company's available balance (funded minus drawn)

POST /activations/:id/verify-identity

Initiate Nafath identity verification for a pending activation. The operator plugin handles the Nafath integration.

Requires authentication.

Per T-04-09: Returns an error if the activation is not in pending status (hard gate). Per T-04-07: Users can only verify identity for their own activations.

Path Parameters

ParameterTypeDescription
idstringUUID of the activation

Request Body

FieldTypeRequiredDescription
identityTypestringYescitizen, resident, or visitor
idNumberstringYesNational ID or Iqama number (5–20 characters)

Example Request

POST /activations/c3d4e5f6-a7b8-9012-cdef-345678901234/verify-identity
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
json
{
  "identityType": "citizen",
  "idNumber": "1234567890"
}

Response

200 OK

json
{
  "transId": "nafath-trans-abc123",
  "randomCode": "4",
  "status": "waiting"
}

The randomCode is the Nafath verification code the user must confirm in the Nafath app. The status indicates the current verification state.

Error Responses

CodeStatusCondition
ACTIVATION_NOT_FOUND404Activation ID not found or not owned by user
IDENTITY_ERROR422Activation not in pending status, or identity verification initiation failed
AUTH_REQUIRED401Missing or invalid authentication token

GET /activations/:id/verify-identity

Check the current status of an identity verification request. Handles all 5 Nafath states: waiting, completed, rejected, expired, error.

Requires authentication.

Per T-04-07: Users can only check their own activations.

Path Parameters

ParameterTypeDescription
idstringUUID of the activation

Example Request

GET /activations/c3d4e5f6-a7b8-9012-cdef-345678901234/verify-identity
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response

200 OK — Verification waiting:

json
{
  "status": "waiting",
  "verifiedAt": null
}

200 OK — Verification completed:

json
{
  "status": "completed",
  "verifiedAt": "2026-05-05T08:30:00.000Z"
}

200 OK — Verification rejected:

json
{
  "status": "rejected",
  "verifiedAt": null
}

200 OK — Verification expired:

json
{
  "status": "expired",
  "verifiedAt": null
}

200 OK — Verification error:

json
{
  "status": "error",
  "verifiedAt": null
}

Error Responses

CodeStatusCondition
ACTIVATION_NOT_FOUND404Activation ID not found or not owned by user
IDENTITY_ERROR422Identity verification status check failed
AUTH_REQUIRED401Missing or invalid authentication token

GET /activations/:id/status

Get the activation status combined with identity verification status and estimated activation timing.

Requires authentication.

Per STAT-05: Includes estimatedActivationSeconds hint. Per T-04-07: Users can only see their own activations.

Path Parameters

ParameterTypeDescription
idstringUUID of the activation

Example Request

GET /activations/c3d4e5f6-a7b8-9012-cdef-345678901234/status
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response

200 OK

json
{
  "id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "iccid": "8996601212345678901",
  "simType": "physical",
  "esimProfile": null,
  "status": "in_progress",
  "identityStatus": "completed",
  "estimatedActivationSeconds": 120,
  "createdAt": "2026-05-05T08:00:00.000Z",
  "updatedAt": "2026-05-05T08:30:00.000Z"
}

For an activated eSIM, esimProfile carries the delivery payload (null for physical rows and for any eSIM activation that has not reached activated):

json
{
  "simType": "esim",
  "esimProfile": {
    "smdpAddress": "SMDP.DEV.SKYTE.SA",
    "matchingId": "SKYTE-DEV-0001",
    "activationCodeRaw": "LPA:1$SMDP.DEV.SKYTE.SA$SKYTE-DEV-0001",
    "qrUrl": "/activations/c3d4e5f6-a7b8-9012-cdef-345678901234/esim-qr"
  }
}

Error Responses

CodeStatusCondition
ACTIVATION_NOT_FOUND404Activation ID not found or not owned by user
ACTIVATION_FAILED422Failed to retrieve activation status
AUTH_REQUIRED401Missing or invalid authentication token

GET /activations/:id/esim-qr

Serves the install QR PNG for an activated eSIM, generated on demand from the stored activation code (LPA:-prefixed per GSMA SGP.22 §4.1). Never stored — Cache-Control: private, no-store. Returns 404 until the activation reaches activated — the payload is never delivered while payment or processing is pending.

Requires authentication. Owner-scoped through the same lookup as /:id/status (T-04-07).

Path Parameters

ParameterTypeDescription
idstringUUID of the activation

Example Request

GET /activations/c3d4e5f6-a7b8-9012-cdef-345678901234/esim-qr
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response

200 OKimage/png binary body.

Error Responses

CodeStatusCondition
ACTIVATION_NOT_FOUND404Activation not found, not owned by user, physical, or no issued profile yet
ACTIVATION_ESIM_DISABLED422FEATURE_ESIM gate is off
AUTH_REQUIRED401Missing or invalid authentication token

GET /activations/history

Paginated activation history for the authenticated user. Uses keyset (cursor-based) pagination for deterministic ordering.

Requires authentication.

Per T-06-04: History query scoped by JWT userId (ownership guard). No cross-user lookup.

Query Parameters

ParameterTypeDefaultDescription
cursorstring(none)Omit for first page; use nextCursor from previous response
limitinteger20Number of records per page (1–100)
statusstring(none)Filter by activation status: pending, in_progress, activated, failed, cancelled

Example Requests

First page (default limit):

GET /activations/history
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

With limit and status filter:

GET /activations/history?limit=10&status=activated
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Next page with cursor:

GET /activations/history?cursor=2026-04-28T10:30:00.000Z|a1b2c3d4-e5f6-7890-abcd-ef1234567890&limit=10
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response

200 OK

json
{
  "items": [
    {
      "activationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "iccid": "8996601212345678901",
      "simType": "physical",
      "esimProfile": null,
      "status": "activated",
      "packageNameEn": "STC 50 SAR Plan",
      "packageNameAr": "باقة STC ٥٠ ريال",
      "operatorNameEn": "Saudi Telecom Company",
      "operatorNameAr": "شركة الاتصالات السعودية",
      "estimatedActivationSeconds": null,
      "createdAt": "2026-04-28T10:30:00.000Z",
      "updatedAt": "2026-04-28T10:35:00.000Z"
    },
    {
      "activationId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "iccid": "8996600000000000123",
      "simType": "esim",
      "esimProfile": {
        "smdpAddress": "SMDP.DEV.SKYTE.SA",
        "matchingId": "SKYTE-DEV-0001",
        "activationCodeRaw": "LPA:1$SMDP.DEV.SKYTE.SA$SKYTE-DEV-0001",
        "qrUrl": "/activations/b2c3d4e5-f6a7-8901-bcde-f12345678901/esim-qr"
      },
      "status": "activated",
      "packageNameEn": "Mobily Data 20GB",
      "packageNameAr": "موبايلي بيانات ٢٠ جيجا",
      "operatorNameEn": "Mobily",
      "operatorNameAr": "موبايلي",
      "estimatedActivationSeconds": null,
      "createdAt": "2026-04-27T14:00:00.000Z",
      "updatedAt": "2026-04-27T14:01:00.000Z"
    }
  ],
  "nextCursor": "2026-04-27T14:00:00.000Z|b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "hasMore": true
}

When no more results:

json
{
  "items": [],
  "nextCursor": null,
  "hasMore": false
}

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Invalid cursor format or limit value
AUTH_REQUIRED401Missing or invalid authentication token

GET /activations/records

Paginated activation records for the admin activation dashboard and history views. This endpoint is admin-scoped only and does not expose user-owned history semantics from GET /activations/history.

Requires authentication and appRole === "super_admin".

Ownership is derived from the authenticated admin context. Do not send userId, ownership, or body parameters. Unknown query parameters are rejected with VALIDATION_ERROR. Soft-deleted activations (deletedAt set) are excluded by default. Pass includeDeleted=true to include them. Excluding them at the source keeps cursor paging and dashboard counts accurate. Operator secrets are not exposed: operator.apiConfig and operator.apiBaseUrl are intentionally omitted. User sensitive fields are not exposed: user.idNumber, user.phoneHash, and user.emailHash are intentionally omitted.

Query Parameters

ParameterTypeDefaultDescription
cursorstring(none)Omit for first page; use nextCursor from previous response. Format: `createdAt
limitinteger20Number of records per page (1-100)
statusstring(none)Filter by activation status: pending, in_progress, activated, failed, cancelled
operatorIduuid(none)Filter by activation operator ID
packageIduuid(none)Filter by activation package ID
fromISO datetime(none)Inclusive lower bound for activations.createdAt
toISO datetime(none)Inclusive upper bound for activations.createdAt; must be greater than or equal to from
includeDeletedbooleanfalseWhether to include soft-deleted activations (deletedAt set). When false, deleted rows are excluded from the page and the pagination cursor, so they do not shrink pages or skew counts.

Rejected Parameters

ParameterReason
userIdUser ownership is not accepted from clients for this admin endpoint
Any unknown query parameterThe query schema is strict to prevent accidental ownership or privilege semantics
Request bodyThis is a GET endpoint; filters are query parameters only

Example Requests

First page:

GET /activations/records
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Filtered dashboard query:

GET /activations/records?limit=10&status=activated&operatorId=f47ac10b-58cc-4372-a567-0e02b2c3d479&from=2026-06-01T00:00:00.000Z&to=2026-06-02T00:00:00.000Z
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Next page with cursor:

GET /activations/records?cursor=2026-06-01T10:00:00.000Z|c3d4e5f6-a7b8-9012-cdef-345678901234&limit=10
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Include soft-deleted records (e.g. for an audit view):

GET /activations/records?includeDeleted=true&limit=10
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response

200 OK

json
{
  "items": [
    {
      "activationId": "c3d4e5f6-a7b8-9012-cdef-345678901234",
      "orderItemId": "d4e5f6a7-b8c9-0123-defa-456789012345",
      "userId": "e5f6a7b8-c9d0-1234-efab-567890123456",
      "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "iccid": "8996601212345678901",
      "simType": "physical",
      "packageId": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "status": "activated",
      "identityStatus": "completed",
      "identityRef": "nafath-trans-abc123",
      "assignedOfficerId": null,
      "activatedAt": "2026-06-01T10:02:00.000Z",
      "failedReason": null,
      "retainUntil": null,
      "deletionRequestedAt": null,
      "deletedAt": null,
      "createdAt": "2026-06-01T10:00:00.000Z",
      "updatedAt": "2026-06-01T10:01:00.000Z",
      "order": {
        "id": "a7b8c9d0-e1f2-3456-abcd-789012345678",
        "userId": "e5f6a7b8-c9d0-1234-efab-567890123456",
        "companyId": null,
        "status": "completed",
        "totalAmountSar": "99.00",
        "createdAt": "2026-06-01T10:00:00.000Z",
        "updatedAt": "2026-06-01T10:01:00.000Z"
      },
      "orderItem": {
        "id": "d4e5f6a7-b8c9-0123-defa-456789012345",
        "orderId": "a7b8c9d0-e1f2-3456-abcd-789012345678",
        "packageId": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
        "iccid": "8996601212345678901",
        "quantity": 1,
        "unitPriceSar": "99.00"
      },
      "user": {
        "id": "e5f6a7b8-c9d0-1234-efab-567890123456",
        "name": "Ahmed Al-Rashid",
        "email": "ahmed@example.com",
        "phone": "+966501234567"
      },
      "package": {
        "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
        "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "nameEn": "STC 99 SAR Plan",
        "nameAr": "باقة STC ٩٩ ريال",
        "descriptionEn": "Monthly data and voice package",
        "descriptionAr": "باقة شهرية للبيانات والمكالمات",
        "dataAmountMb": 20480,
        "voiceMinutes": 500,
        "smsCount": 100,
        "validityDays": 30,
        "priceSar": "99.00",
        "priceSarB2b": null,
        "isActive": true,
        "isPromotional": false,
        "isNameCustomized": false,
        "isPriceCustomized": false,
        "createdAt": "2026-05-01T08:00:00.000Z",
        "updatedAt": "2026-05-01T08:05:00.000Z"
      },
      "operator": {
        "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "slug": "stc",
        "nameEn": "Saudi Telecom Company",
        "nameAr": "شركة الاتصالات السعودية",
        "isActive": true,
        "logoUrl": "https://example.com/stc.png",
        "createdAt": "2026-04-01T08:00:00.000Z",
        "updatedAt": "2026-04-01T08:05:00.000Z"
      }
    }
  ],
  "nextCursor": null,
  "hasMore": false
}

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Invalid limit, status, UUID, datetime range, cursor format, or unknown/ownership query parameter
AUTH_REQUIRED401Missing or invalid authentication token
AUTH_FORBIDDEN403Authenticated user is not super_admin
ACTIVATION_FAILED422Failed to retrieve activation records

Internal documentation - Activation System