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/moyasar
  • POST /webhooks/paymob
  • POST /webhooks/mobimatter
  • POST /webhooks/notifications/:channel/:provider

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). CF-Connecting-IP, set by Cloudflare, takes precedence when present. 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
X-Audit-Client-IPOptionalIP addressSent by the admin portal's server-side API client only: the browser's IP, recorded on audit rows ahead of every other source. Same validation as X-Forwarded-For, and like it, not authenticated: any caller can set it
X-Audit-Client-UAOptionalStringSent by the admin portal's server-side API client only: the browser's user agent, recorded on audit rows ahead of User-Agent. Not authenticated
User-AgentOptionalStringClient user-agent (for consent audit trail)
Idempotency-KeyOptional on POST /admin/notificationsNonblank string, 1 to 255 charactersReplays an administrative notification request for 24 hours, scoped to the sender

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_*)
Auth surfaceIn-memory, per process100 requests / 15 min (configurable via AUTH_RATE_LIMIT_*): per IP on /auth/register, /auth/login, /auth/verify-otp and /auth/magic-link, per signed-in user on /auth/change-phone and /auth/verify-change-phone

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 ​

Error messages and display fields are localized via Accept-Language. The notification inbox is the exception, rendering copy from each recipient's stored locale.

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 ​

Payment creation endpoints use an idempotencyKey body 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.

Notification Header Convention ​

POST /admin/notifications instead accepts an optional Idempotency-Key header. It is scoped to the sending administrator and exact key. Keys are nonblank, 1 to 255 characters, and are not trimmed.

RuleBehavior
Same sender, key, and validated bodyReplays the stored 202 body for 24 hours after that response was built
Same sender and key, different validated body409 NOTIFICATION_IDEMPOTENCY_CONFLICT
Same key used by another senderSeparate request
Expired keyStarts a new send
No keyNo request replay protection

Fingerprinting follows validation and defaults, but precedes channel narrowing. Object key order is ignored and array order is preserved. The replay body is acceptance, not current delivery progress. An unfinished response reserves its key for 24 hours from insertion. Recording the response restarts that window. Incomplete acceptance can retry the same batch handoff or return 503 SERVICE_UNAVAILABLE with Retry-After: 1. See notification idempotency for concurrency and recovery behavior.


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 an existing user on its matching client (always 200)
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; email: null removes it)
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 or activation-records area)Admin dashboard activation records with filters and related order/package/operator/user contact data
GET/activations/records/countsYes (super_admin or activation-records area)Failed and stuck activation attention counts with the configured stuck threshold

Payments ​

MethodPathAuthDescription
GET/payment-returnPublicBuilt-in neutral bilingual return page
POST/payments/:id/confirmRequiredVerify owned Moyasar or Paymob payment server-side
POST/payments/create-intentYesCreate payment intent (Mada, Visa, Mastercard)
POST/payments/apple-payYesCreate Apple Pay payment
POST/payments/stcpayYesCreate STC Pay SDK checkout via selected provider
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, locale, identityType; email: null removes it)
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 or staff-users area)List manageable administration users
POST/admin/usersYes (super_admin or staff-users area, manage)Provision an administration user with role and assigned areas
PATCH/admin/users/:idYes (super_admin, or per target: staff-users, company-people or company-onboarding, manage)Update a user's role, areas, or active state
DELETE/admin/users/:idYes (super_admin, or per target: staff-users, company-people or company-onboarding, manage)Permanently remove an administration user
POST/admin/users/:id/resend-inviteYes (super_admin, or per target: staff-users, company-people or company-onboarding, manage)Re-send the sign-in invitation

Customers ​

MethodPathAuthDescription
GET/admin/customers/searchYes (customers area)Exact-match customer search by phone, email, ICCID, receipt number, order id, or provider order id
GET/admin/customers/:idYes (customers area)Six-part customer record with viewer capabilities
GET/admin/customers/:id/identityYes (customers and identity-detail areas)Identity check detail per activation, audited as customer.identity_viewed
GET/admin/customers/:id/historyYes (customers area)Customer audit history
POST/admin/officer/activations/:activationId/resend-deliveryYes (officer area)Resend an activation eSIM delivery email (202 { sent, sequence })
POST/admin/officer/store-orders/:orderId/resend-deliveryYes (officer area)Resend a store order eSIM delivery email (202 { sent, sequence })

Audit History ​

MethodPathAuthDescription
GET/admin/audit-logsYes (super_admin or audit area)Filter and paginate the audit log

Notifications ​

MethodPathAuthDescription
GET/notificationsYesOwn visible inbox with cursor pagination
GET/notifications/countYesOwn visible unread count
POST/notifications/:id/readYesMark one owned notification read
POST/notifications/read-allYesMark visible unread notifications read
POST/notifications/seenYesMark 1 to 200 owned notification IDs seen atomically
POST/notifications/:id/archiveYesArchive one owned notification
GET/notifications/opt-outsYesChannel registry and own opt-outs
PUT/notifications/opt-outsYesReplace own channel opt-outs
POST/admin/notificationsYes (super_admin or notifications area, manage)Accept a custom or templated immediate or scheduled send
GET/admin/notifications/recipientsYes (super_admin or notifications area)Search recipients for the composer's audience picker
GET/admin/notifications/batchesYes (super_admin or notifications area)Filter and paginate batches
GET/admin/notifications/batches/:idYes (super_admin or notifications area)Batch summary, delivery counts, and paginated failures
POST/admin/notifications/batches/:id/cancelYes (super_admin or notifications area, manage)Cancel a batch before execution starts
GET/admin/notifications/delivery-rateYes (super_admin or notifications area)Current delivery outcomes grouped over a creation-time window

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 and listing portal users needs the company-people or company-onboarding area as well. An archived company is read-only: its writes return 409 COMPANY_ARCHIVED.

MethodPathAuthDescription
GET/admin/companiesYes (b2b area)List companies
POST/admin/companies/invitesYes (super_admin, or company-people manage with a companyId, else company-onboarding manage)Invite a portal user (company admin or viewer)
GET/admin/companies/pending-adminsYes (super_admin or company-onboarding area)Invited company admins who have not registered their company yet
GET/admin/companies/:idYes (b2b area)Company detail
PATCH/admin/companies/:idYes (b2b area, manage)Edit a company's CR number, name, and contact details
GET/admin/companies/:id/usersYes (super_admin or company-people area)The company's portal users with their company role
GET/admin/companies/:id/historyYes (b2b area)Company audit history
POST/admin/companies/:id/statusYes (b2b area, manage)Approve, reject, suspend, reactivate, archive, or unarchive 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 or store area)List all store orders with stuck, issuance state, provider, age, and user filters
GET/admin/store/orders/:idYes (super_admin or store area)One store order with the list row shape plus statusEvents, its issuance timeline oldest first; 404 STORE_ORDER_NOT_FOUND when unknown
GET/admin/store/orders/countsYes (super_admin or store area)Store order attention counts using the list filters
GET/admin/store/orders/:id/historyYes (super_admin or store area)Store order audit history
POST/admin/store/orders/:id/requeueYes (super_admin or store area, manage)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/moyasarNo (body secret verified)Handle Moyasar payment webhook events
POST/webhooks/paymobNo (HMAC verified)Handle Paymob transaction processed callbacks
POST/webhooks/mobimatterNo (signature verified)Reconcile delayed eSIM order completion
POST/webhooks/notifications/:channel/:providerNo (signature verified)Persist and enqueue email or SMS delivery callbacks

Detailed Endpoint Documentation ​

SectionDocument
Authenticationauth.md
Activationsactivations.md
Paymentspayments.md
Packagespackages.md
eSIM Storestore.md
Operatorsoperators.md
Profileprofile.md
Notificationsnotifications.md
Admin Usersadmin-users.md
Customerscustomers.md
Audit Historyaudit.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.

Payment plugin administration ​

Owner-only GET/PATCH /admin/plugins/payment reads the checkout (Mock/Moyasar/Paymob selection and each gateway's sandbox/production profiles) and toggles accepting new payments. PATCH /admin/plugins/payment/moyasar and /admin/plugins/payment/paymob save each gateway's settings; POST /admin/plugins/payment/{gateway}/test checks saved credentials without charging. The older /admin/plugins/moyasar routes still work but are deprecated. See settings and checkout contract.

Internal documentation - Activation System