Skip to content

Activation System API Reference

The Activation System API is a RESTful service built with Hono on Node.js, providing SIM card activation services for Saudi Arabian telecom operators. All endpoints return JSON and support bilingual error messages (Arabic/English) via the Accept-Language header.

Base URL

EnvironmentURL
Developmenthttp://localhost:3001
Productionhttps://<api-production-origin>

Interactive Docs

Explore the API interactively with Scalar UI at:

EnvironmentURL
Developmenthttp://localhost:3001/docs
Productionhttps://<api-production-origin>/docs

OpenAPI 3.1 spec available at /openapi.json.


Authentication

The API uses JWT-based authentication with a two-phase flow:

  1. Register or Login — triggers OTP delivery to phone/email
  2. Verify OTP — creates a session and returns credentials

Session Strategies

Client TypeDetectionToken DeliverySubsequent Requests
MobileX-Client-Type: mobile or Flutter/Dart/Android/iPhone User-Agent patternsJWT in response body → session.tokenAuthorization: Bearer <token>
WebX-Client-Type: web or defaulthttpOnly cookie activation-sys-sessionCookie sent automatically

Public Endpoints (No Auth Required)

  • GET /health
  • POST /auth/register
  • POST /auth/login
  • POST /auth/verify-otp
  • POST /auth/magic-link
  • GET /store/products
  • GET /store/destinations
  • GET /store/products/:id
  • GET /store/status
  • GET /store/device-compatibility
  • POST /activations/validate-iccid
  • POST /webhooks/stripe
  • POST /webhooks/mobimatter

Protected Endpoints

All other endpoints require a valid JWT. The middleware extracts userId, appRole, and identityType from the token — these are never accepted from the request body (security: T-04-06, T-05-10, T-06-07).


Common Headers

HeaderRequiredValuesDescription
AuthorizationProtected routesBearer <jwt>JWT token from /auth/verify-otp
Accept-LanguageOptionalen (default), arLanguage for error messages and display fields
Content-TypePOST/PUT/PATCHapplication/jsonRequest body format
X-Client-TypeOptionalweb, mobileOverrides automatic client type detection
X-Forwarded-ForOptionalIP addressClient IP (for consent audit trail). The first entry is used, and a value that is not a plain IPv4 or IPv6 address is recorded as empty rather than rejecting the request
User-AgentOptionalStringClient user-agent (for consent audit trail)

Error Response Format

All errors follow a consistent bilingual structure:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "path": "phone", "message": "Phone number must be at least 10 digits" }
    ]
  }
}

The message field is automatically localized based on Accept-Language. See Error Codes Reference for the complete error catalog.


Rate Limiting

EnvironmentBackendLimit
AllIn-memory, per process100 requests / 15 min / IP (configurable via RATE_LIMIT_*)

The counters live in process memory — Redis-backed counters for multi-instance deploys are a known deferred hardening item, not current behaviour.

Response headers:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetWindow reset timestamp (Unix epoch)

When rate limit is exceeded, the API returns 429 with:

json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests, please try again later"
  }
}

Bilingual Responses

All error messages and display fields are localized via Accept-Language.

English (Accept-Language: en):

json
{
  "error": {
    "code": "AUTH_INVALID_OTP",
    "message": "Invalid OTP code"
  }
}

Arabic (Accept-Language: ar):

json
{
  "error": {
    "code": "AUTH_INVALID_OTP",
    "message": "رمز التحقق غير صحيح"
  }
}

Package and operator listings return name and description in the requested language instead of both nameEn/nameAr pairs.


Idempotency

All payment endpoints require an idempotencyKey field for safe retries without duplicate charges:

RuleBehavior
Same key + same parametersReturns existing payment (idempotent)
Same key + different parametersReturns 402 PAYMENT_IDEMPOTENCY_CONFLICT

Example:

json
{
  "orderId": "b3c4d5e6-f7a8-9012-bcde-f12345678901",
  "method": "mada",
  "idempotencyKey": "pay-2026-0430-ahmed-001"
}

The amount field is never accepted from the request body — it is derived from the order's stored totalAmount and currency in the database per security requirement T-05-04.


Pagination

Activation history and admin activation records use keyset (cursor-based) pagination for deterministic ordering.

ParameterTypeDefaultDescription
cursorstring(none)Omit for first page; use nextCursor from previous response
limitinteger20Records per page (max 100)

First page:

GET /activations/history?limit=10

Admin dashboard records use the same pagination pattern:

GET /activations/records?limit=10&status=activated

Response:

json
{
  "items": [
    {
      "activationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "iccid": "8996601212345678901",
      "status": "activated",
      "packageNameEn": "STC 50 SAR Plan",
      "packageNameAr": "باقة STC ٥٠ ريال",
      "operatorNameEn": "Saudi Telecom Company",
      "operatorNameAr": "شركة الاتصالات السعودية",
      "estimatedActivationSeconds": null,
      "createdAt": "2026-04-28T10:30:00Z",
      "updatedAt": "2026-04-28T10:35:00Z"
    }
  ],
  "nextCursor": "2026-04-28T10:30:00.000Z|a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "hasMore": true
}

Next page:

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

API Endpoints Overview

Health

MethodPathAuthDescription
GET/healthNoAPI, database, and Redis connectivity check

fxRatesAgeHours is included only when FEATURE_ESIM_STORE is enabled. When present, it is the finite age in hours of the latest FX batch, or null when no batch has been fetched yet; when the feature is disabled, the property is omitted.

Auth

MethodPathAuthDescription
POST/auth/registerNoRegister new user with phone/email, name, and consents; sends OTP
POST/auth/loginNoSend OTP to existing user's phone/email
POST/auth/verify-otpNoVerify OTP and create session (returns JWT)
POST/auth/magic-linkNoRedeem a B2B roster invite token and create a session
POST/auth/consentsYesRecord the 3 required PDPL consents for a magic-link-redeemed user
POST/auth/logoutYesInvalidate session (clear cookie or blocklist JWT)
GET/auth/sessionYesGet current user session details
GET/auth/meYesGet authenticated user profile with decrypted PII
PUT/auth/meYesUpdate user profile (name, email)
POST/auth/change-phoneYesSend OTP to new phone number

Operators

MethodPathAuthDescription
GET/operatorsNoList all active telecom operators

Packages

MethodPathAuthDescription
GET/packagesNoList packages with optional filters
GET/packages/searchNoFull-text search with PostgreSQL tsquery + trigram fallback
GET/packages/compareNoSide-by-side comparison of 2–5 packages
GET/packages/:idNoSingle package detail with B2B pricing and operator info
GET/packages/:id/availabilityNoReal-time availability check via operator plugin (30s cache)

eSIM Store

MethodPathAuthDescription
GET/store/destinationsNoPublic destination aggregates with scope, optional country/region, planCount, and minimum fromPrice converted to presentment currency
GET/store/productsNoBrowse active global eSIM store products with destination filters, presentment pricing, retailUsd|productId cursor pagination, and { data, total, nextCursor, hasMore, currency } response envelope
GET/store/statusNoReport FEATURE_ESIM_STORE status for public UX gating; returns { enabled: boolean } and Cache-Control: public, max-age=60
GET/store/device-compatibilityNoAdvisory eSIM device list from a versioned hard-coded constant; public, ungated by the store flag, client-side matching (exact beats prefix, unknown warns, never blocks), Cache-Control: public, max-age=3600
GET/store/products/:idNoGet one active store product with presentment pricing
POST/store/device-acksYesRecord an advisory device-readiness or carrier-lock acknowledgement for the authenticated user; warning-only evidence, no user-facing read surface
POST/store/ordersYesCreate a pending store order for an eSIM product. Pay with /payments/create-intent using the returned orderId
GET/store/ordersYesList the caller's store orders with issued eSIM payloads when available, { data, total, nextCursor, hasMore }, and full-precision timestamptz|orderId cursors
GET/store/orders/:idYesGet one owner-scoped store order and its issued eSIM payload when available
GET/store/orders/:id/esim-qrYesOwner-scoped QR PNG for an issued store eSIM profile

Activations

MethodPathAuthDescription
POST/activations/validate-iccidNoValidate a SIM serial via operator plugin — 19-digit ICCID (physical) or LPA activation code (esim) per simType
POST/activationsYesCreate activation order with package selection (simType discriminates physical vs esim)
POST/activations/:id/verify-identityYesInitiate Nafath identity verification
GET/activations/:id/verify-identityYesCheck identity verification status
GET/activations/:id/statusYesGet activation + identity status with estimated timing (carries simType + esimProfile)
GET/activations/:id/esim-qrYesOwner-scoped install QR PNG for an issued eSIM profile
GET/activations/historyYesPaginated activation history for authenticated user (items carry simType + esimProfile)
GET/activations/recordsYes (super_admin)Admin dashboard activation records with filters and related order/package/operator/user contact data

Payments

MethodPathAuthDescription
POST/payments/create-intentYesCreate payment intent (Mada, Visa, Mastercard, Stripe)
POST/payments/apple-payYesCreate Apple Pay payment
POST/payments/stcpayYesInitiate STC Pay direct payment
GET/payments/:id/receiptYesGet owner-scoped payment receipt with bilingual messages

Profile

MethodPathAuthDescription
GET/profileYesGet authenticated user profile aggregate
PUT/profileYesUpdate allowed profile fields (name, email, preferredCurrency)
GET/profile/exportYesPDPL data portability export, including activation eSIM fields and eSIM store orders
DELETE/profileYesRequest account deletion (soft delete with retention)

Admin Users

MethodPathAuthDescription
GET/admin/usersYes (super_admin)List manageable administration users
POST/admin/usersYes (super_admin)Provision an administration user with role and assigned areas
PATCH/admin/users/:idYes (super_admin)Update a user's role, areas, or active state
DELETE/admin/users/:idYes (super_admin)Permanently remove an administration user
POST/admin/users/:id/resend-inviteYes (super_admin)Re-send the sign-in invitation

B2B Portal

Company-facing routes, mounted at /b2b. All require b2b_admin; roster/catalog/quote/contract/dashboard/billing routes additionally require an approved company (admin for writes, admin or member for reads).

MethodPathAuthDescription
GET/b2b/companyYes (b2b_admin)Resolve the caller's own company (null if not yet registered)
POST/b2b/companyYes (b2b_admin)Register a company for the caller (→ pending)
PATCH/b2b/company/payment-modeYes (company admin)Pick or switch the company's payment mode (prepaid/flexible), locked once a contract exists
GET/b2b/rosterYes (company member)List the company's beneficiaries
GET/b2b/roster/exportYes (company member)CSV export of the roster + link status
POST/b2b/roster/stageYes (company admin)Stage CSV roster rows (partial accept per row)
PATCH/b2b/roster/:rosterIdYes (company admin)Change or clear one beneficiary's package assignment
DELETE/b2b/roster/:rosterIdYes (company admin)Remove one beneficiary — roster row, link history and their account
POST/b2b/roster/links/sendYes (company admin)Mint and deliver activation invite links
POST/b2b/roster/links/exportYes (company admin)Mint invite links and return the raw links for self-distribution
POST/b2b/roster/links/reissue-expiredYes (company admin)Re-mint every expired link for the company
POST/b2b/roster/links/revokeYes (company admin)Revoke the selected beneficiaries' open invitations (partial accept per row)
GET/b2b/catalogYes (company member)B2B-priced package catalog for the quote wizard
GET/b2b/quotesYes (company member)List the company's quotes
GET/b2b/quotes/:idYes (company member)Quote detail
POST/b2b/quotesYes (company admin)Create a quote draft
PATCH/b2b/quotes/:idYes (company admin)Update a draft quote
POST/b2b/quotes/:id/submitYes (company admin)Submit a draft for review
POST/b2b/quotes/:id/acceptYes (company admin)Accept a countered quote version
POST/b2b/quotes/:id/rejectYes (company admin)Decline a countered quote version, with an optional reason
GET/b2b/contractYes (company member)The company's active contract
GET/b2b/readinessYes (company member)Contract/payment/roster readiness gate
GET/b2b/dashboardYes (company member)Portal dashboard overview
GET/b2b/billingYes (company member)Billing/charges summary
GET/b2b/billing/exportYes (company member)CSV export of charges

B2B Admin

Internal oversight routes, mounted at /admin. Reads require super_admin or an admin with View on the b2b area; writes require Manage on b2b. Inviting a portal user is super_admin-only.

MethodPathAuthDescription
GET/admin/companiesYes (b2b area)List companies
POST/admin/companies/invitesYes (super_admin)Invite a portal user (company admin or viewer)
GET/admin/companies/:idYes (b2b area)Company detail
POST/admin/companies/:id/statusYes (b2b area, manage)Approve, reject, suspend, or reactivate a company
GET/admin/quotesYes (b2b area)List quotes across all companies
GET/admin/quotes/catalogYes (b2b area)Catalog for the counter-quote line-item picker
GET/admin/quotes/:idYes (b2b area)Quote detail
POST/admin/quotes/:id/start-reviewYes (b2b area, manage)Move a submitted quote into review
POST/admin/quotes/:id/counterYes (b2b area, manage)Counter a quote with a new linked version
POST/admin/quotes/:id/rejectYes (b2b area, manage)Reject a quote
POST/admin/quotes/:id/confirm-contractYes (b2b area, manage)Accept the quote and create its contract
GET/admin/contracts/company/:companyIdYes (b2b area)Company's active contract
GET/admin/contracts/company/:companyId/readinessYes (b2b area)Contract/payment/roster readiness gate
GET/admin/contracts/company/:companyId/balanceYes (b2b area)Flexible funding balance (funded/drawn/committed/available + active lines)
POST/admin/contracts/:id/paymentsYes (b2b area, manage)Record a payment (with document) — prepaid pay-in-full or a flexible funding/top-up amount
GET/admin/contracts/:id/paymentsYes (b2b area)List payment records (metadata only)
GET/admin/contracts/:id/payments/:paymentId/documentYes (b2b area, manage)Download a payment's supporting document
GET/admin/linksYes (b2b area)Cross-company magic-link delivery oversight
GET/admin/links/exportYes (b2b area)CSV export of magic-link oversight rows

Settings

MethodPathAuthDescription
GET/admin/settings/organizationYes (super_admin)Get the seller-of-record settings used on generated PDFs
PATCH/admin/settings/organizationYes (super_admin)Update the seller-of-record settings
GET/admin/settings/fx-ratesYes (super_admin)Read the daily-fetched FX rates ({ base: 'USD', rates: [{currency, ratePerUsd, fetchedAt, source}] }); read-only — rates are never set manually

Admin eSIM Store

MethodPathAuthDescription
GET/admin/store/providersYes (super_admin)List store providers (eSIM operators rows) with each provider's latest health reading (null when its plugin is not registered)
PATCH/admin/store/providers/:idYes (super_admin)Audited provider enable/disable toggle (store.provider_toggled) with immediate storefront effect. eSIM rows only — 404 STORE_PROVIDER_NOT_FOUND otherwise
GET/admin/store/productsYes (super_admin)List all store products, including inactive rows, provider linkage, isActiveCustomized, and { data, total, nextCursor, hasMore } pagination
PATCH/admin/store/products/:idYes (super_admin)Update store product overrides, active state, and customization flags
GET/admin/store/ordersYes (super_admin)List all store orders, newest-first on (createdAt, orderId); adds owner userId, failedReason, and issuedAt; accepts stuck=true (status = paid AND issued_at IS NULL) and the same { data, total, nextCursor, hasMore } envelope
GET/admin/store/orders/:idYes (super_admin)One store order with the same shape as the list row; 404 STORE_ORDER_NOT_FOUND when unknown
POST/admin/store/orders/:id/requeueYes (super_admin)Re-enqueue a stuck store order fulfillment (202 { queued: true }, audited as store.order_requeued; 409 STORE_ORDER_NOT_REQUEUEABLE when not stuck, 503 STORE_REQUEUE_UNAVAILABLE when Redis is unavailable)
POST/admin/store/syncYes (super_admin)Queue a manual eSIM store catalog sync (202 queued, 503 STORE_SYNC_UNAVAILABLE when enqueue fails)

Documents

MethodPathAuthDescription
GET/documents/:type/:entityId.pdfYesRender and download a generated PDF (receipt or contract-payment document); access is authorized per document type

Compliance

MethodPathAuthDescription
POST/compliance/retention/sweepYes (super_admin)Execute PDPL data retention sweep
POST/compliance/breach-reportYes (super_admin)Generate SDAIA breach notification report

Webhooks

MethodPathAuthDescription
POST/webhooks/stripeNo (signature verified)Handle Stripe payment webhook events

Detailed Endpoint Documentation

SectionDocument
Authenticationauth.md
Activationsactivations.md
Paymentspayments.md
Packagespackages.md
eSIM Storestore.md
Operatorsoperators.md
Profileprofile.md
Admin Usersadmin-users.md
B2B Portalb2b.md
B2B Adminb2b-admin.md
Settingssettings.md
Documentsdocuments.md
Compliancecompliance.md
Webhookswebhooks.md
Error Codeserrors.md

SIM Limits by Identity Type (CITC)

Per CITC regulations, users are limited by identity type:

Identity TypeMax SIMs
citizen10
resident2
visitor1

Web Client Surfaces

The web client type applies to browser clients such as apps/admin and apps/web-b2c. Web sessions use the activation-sys-session httpOnly cookie. Mobile clients continue to request JWT delivery in the response body with X-Client-Type: mobile or mobile user-agent detection.

The public landing site (apps/site) should remain unauthenticated unless a future feature explicitly requires API access. If it calls the API, add the deployed origin to CORS_ORIGINS and keep all secrets server-side.

Internal documentation - Activation System