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
| Environment | URL |
|---|---|
| Development | http://localhost:3001 |
| Production | https://<api-production-origin> |
Interactive Docs
Explore the API interactively with Scalar UI at:
| Environment | URL |
|---|---|
| Development | http://localhost:3001/docs |
| Production | https://<api-production-origin>/docs |
OpenAPI 3.1 spec available at /openapi.json.
Authentication
The API uses JWT-based authentication with a two-phase flow:
- Register or Login — triggers OTP delivery to phone/email
- Verify OTP — creates a session and returns credentials
Session Strategies
| Client Type | Detection | Token Delivery | Subsequent Requests |
|---|---|---|---|
| Mobile | X-Client-Type: mobile or Flutter/Dart/Android/iPhone User-Agent patterns | JWT in response body → session.token | Authorization: Bearer <token> |
| Web | X-Client-Type: web or default | httpOnly cookie activation-sys-session | Cookie sent automatically |
Public Endpoints (No Auth Required)
GET /healthPOST /auth/registerPOST /auth/loginPOST /auth/verify-otpPOST /auth/magic-linkGET /store/productsGET /store/destinationsGET /store/products/:idGET /store/statusGET /store/device-compatibilityPOST /activations/validate-iccidPOST /webhooks/moyasarPOST /webhooks/paymobPOST /webhooks/mobimatterPOST /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
| Header | Required | Values | Description |
|---|---|---|---|
Authorization | Protected routes | Bearer <jwt> | JWT token from /auth/verify-otp |
Accept-Language | Optional | en (default), ar | Language for error messages and display fields |
Content-Type | POST/PUT/PATCH | application/json | Request body format |
X-Client-Type | Optional | web, mobile | Overrides automatic client type detection |
X-Forwarded-For | Optional | IP address | Client 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-IP | Optional | IP address | Sent 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-UA | Optional | String | Sent 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-Agent | Optional | String | Client user-agent (for consent audit trail) |
Idempotency-Key | Optional on POST /admin/notifications | Nonblank string, 1 to 255 characters | Replays an administrative notification request for 24 hours, scoped to the sender |
Error Response Format
All errors follow a consistent bilingual structure:
{
"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
| Environment | Backend | Limit |
|---|---|---|
| All | In-memory, per process | 100 requests / 15 min / IP (configurable via RATE_LIMIT_*) |
| Auth surface | In-memory, per process | 100 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:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Window reset timestamp (Unix epoch) |
When rate limit is exceeded, the API returns 429 with:
{
"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):
{
"error": {
"code": "AUTH_INVALID_OTP",
"message": "Invalid OTP code"
}
}Arabic (Accept-Language: ar):
{
"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:
| Rule | Behavior |
|---|---|
| Same key + same parameters | Returns existing payment (idempotent) |
| Same key + different parameters | Returns 402 PAYMENT_IDEMPOTENCY_CONFLICT |
Example:
{
"orderId": "b3c4d5e6-f7a8-9012-bcde-f12345678901",
"method": "mada",
"idempotencyKey": "pay-2026-0430-ahmed-001"
}The
amountfield is never accepted from the request body — it is derived from the order's storedtotalAmountandcurrencyin 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.
| Rule | Behavior |
|---|---|
| Same sender, key, and validated body | Replays the stored 202 body for 24 hours after that response was built |
| Same sender and key, different validated body | 409 NOTIFICATION_IDEMPOTENCY_CONFLICT |
| Same key used by another sender | Separate request |
| Expired key | Starts a new send |
| No key | No 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | (none) | Omit for first page; use nextCursor from previous response |
limit | integer | 20 | Records per page (max 100) |
First page:
GET /activations/history?limit=10Admin dashboard records use the same pagination pattern:
GET /activations/records?limit=10&status=activatedResponse:
{
"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=10API Endpoints Overview
Health
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health | No | API, 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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register | No | Register new user with phone/email, name, and consents; sends OTP |
| POST | /auth/login | No | Send OTP to an existing user on its matching client (always 200) |
| POST | /auth/verify-otp | No | Verify OTP and create session (returns JWT) |
| POST | /auth/magic-link | No | Redeem a B2B roster invite token and create a session |
| POST | /auth/consents | Yes | Record the 3 required PDPL consents for a magic-link-redeemed user |
| POST | /auth/logout | Yes | Invalidate session (clear cookie or blocklist JWT) |
| GET | /auth/session | Yes | Get current user session details |
| GET | /auth/me | Yes | Get authenticated user profile with decrypted PII |
| PUT | /auth/me | Yes | Update user profile (name, email; email: null removes it) |
| POST | /auth/change-phone | Yes | Send OTP to new phone number |
Operators
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /operators | No | List all active telecom operators |
Packages
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /packages | No | List packages with optional filters |
| GET | /packages/search | No | Full-text search with PostgreSQL tsquery + trigram fallback |
| GET | /packages/compare | No | Side-by-side comparison of 2–5 packages |
| GET | /packages/:id | No | Single package detail with B2B pricing and operator info |
| GET | /packages/:id/availability | No | Real-time availability check via operator plugin (30s cache) |
eSIM Store
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /store/destinations | No | Public destination aggregates with scope, optional country/region, planCount, and minimum fromPrice converted to presentment currency |
| GET | /store/products | No | Browse active global eSIM store products with destination filters, presentment pricing, retailUsd|productId cursor pagination, and { data, total, nextCursor, hasMore, currency } response envelope |
| GET | /store/status | No | Report FEATURE_ESIM_STORE status for public UX gating; returns { enabled: boolean } and Cache-Control: public, max-age=60 |
| GET | /store/device-compatibility | No | Advisory 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/:id | No | Get one active store product with presentment pricing |
| POST | /store/device-acks | Yes | Record an advisory device-readiness or carrier-lock acknowledgement for the authenticated user; warning-only evidence, no user-facing read surface |
| POST | /store/orders | Yes | Create a pending store order for an eSIM product. Pay with /payments/create-intent using the returned orderId |
| GET | /store/orders | Yes | List the caller's store orders with issued eSIM payloads when available, { data, total, nextCursor, hasMore }, and full-precision timestamptz|orderId cursors |
| GET | /store/orders/:id | Yes | Get one owner-scoped store order and its issued eSIM payload when available |
| GET | /store/orders/:id/esim-qr | Yes | Owner-scoped QR PNG for an issued store eSIM profile |
Activations
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /activations/validate-iccid | No | Validate a SIM serial via operator plugin — 19-digit ICCID (physical) or LPA activation code (esim) per simType |
| POST | /activations | Yes | Create activation order with package selection (simType discriminates physical vs esim) |
| POST | /activations/:id/verify-identity | Yes | Initiate Nafath identity verification |
| GET | /activations/:id/verify-identity | Yes | Check identity verification status |
| GET | /activations/:id/status | Yes | Get activation + identity status with estimated timing (carries simType + esimProfile) |
| GET | /activations/:id/esim-qr | Yes | Owner-scoped install QR PNG for an issued eSIM profile |
| GET | /activations/history | Yes | Paginated activation history for authenticated user (items carry simType + esimProfile) |
| GET | /activations/records | Yes (super_admin or activation-records area) | Admin dashboard activation records with filters and related order/package/operator/user contact data |
| GET | /activations/records/counts | Yes (super_admin or activation-records area) | Failed and stuck activation attention counts with the configured stuck threshold |
Payments
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /payment-return | Public | Built-in neutral bilingual return page |
| POST | /payments/:id/confirm | Required | Verify owned Moyasar or Paymob payment server-side |
| POST | /payments/create-intent | Yes | Create payment intent (Mada, Visa, Mastercard) |
| POST | /payments/apple-pay | Yes | Create Apple Pay payment |
| POST | /payments/stcpay | Yes | Create STC Pay SDK checkout via selected provider |
| GET | /payments/:id/receipt | Yes | Get owner-scoped payment receipt with bilingual messages |
Profile
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /profile | Yes | Get authenticated user profile aggregate |
| PUT | /profile | Yes | Update allowed profile fields (name, email, preferredCurrency, locale, identityType; email: null removes it) |
| GET | /profile/export | Yes | PDPL data portability export, including activation eSIM fields and eSIM store orders |
| DELETE | /profile | Yes | Request account deletion (soft delete with retention) |
Admin Users
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/users | Yes (super_admin or staff-users area) | List manageable administration users |
| POST | /admin/users | Yes (super_admin or staff-users area, manage) | Provision an administration user with role and assigned areas |
| PATCH | /admin/users/:id | Yes (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/:id | Yes (super_admin, or per target: staff-users, company-people or company-onboarding, manage) | Permanently remove an administration user |
| POST | /admin/users/:id/resend-invite | Yes (super_admin, or per target: staff-users, company-people or company-onboarding, manage) | Re-send the sign-in invitation |
Customers
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/customers/search | Yes (customers area) | Exact-match customer search by phone, email, ICCID, receipt number, order id, or provider order id |
| GET | /admin/customers/:id | Yes (customers area) | Six-part customer record with viewer capabilities |
| GET | /admin/customers/:id/identity | Yes (customers and identity-detail areas) | Identity check detail per activation, audited as customer.identity_viewed |
| GET | /admin/customers/:id/history | Yes (customers area) | Customer audit history |
| POST | /admin/officer/activations/:activationId/resend-delivery | Yes (officer area) | Resend an activation eSIM delivery email (202 { sent, sequence }) |
| POST | /admin/officer/store-orders/:orderId/resend-delivery | Yes (officer area) | Resend a store order eSIM delivery email (202 { sent, sequence }) |
Audit History
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/audit-logs | Yes (super_admin or audit area) | Filter and paginate the audit log |
Notifications
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /notifications | Yes | Own visible inbox with cursor pagination |
| GET | /notifications/count | Yes | Own visible unread count |
| POST | /notifications/:id/read | Yes | Mark one owned notification read |
| POST | /notifications/read-all | Yes | Mark visible unread notifications read |
| POST | /notifications/seen | Yes | Mark 1 to 200 owned notification IDs seen atomically |
| POST | /notifications/:id/archive | Yes | Archive one owned notification |
| GET | /notifications/opt-outs | Yes | Channel registry and own opt-outs |
| PUT | /notifications/opt-outs | Yes | Replace own channel opt-outs |
| POST | /admin/notifications | Yes (super_admin or notifications area, manage) | Accept a custom or templated immediate or scheduled send |
| GET | /admin/notifications/recipients | Yes (super_admin or notifications area) | Search recipients for the composer's audience picker |
| GET | /admin/notifications/batches | Yes (super_admin or notifications area) | Filter and paginate batches |
| GET | /admin/notifications/batches/:id | Yes (super_admin or notifications area) | Batch summary, delivery counts, and paginated failures |
| POST | /admin/notifications/batches/:id/cancel | Yes (super_admin or notifications area, manage) | Cancel a batch before execution starts |
| GET | /admin/notifications/delivery-rate | Yes (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).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /b2b/company | Yes (b2b_admin) | Resolve the caller's own company (null if not yet registered) |
| POST | /b2b/company | Yes (b2b_admin) | Register a company for the caller (→ pending) |
| PATCH | /b2b/company/payment-mode | Yes (company admin) | Pick or switch the company's payment mode (prepaid/flexible), locked once a contract exists |
| GET | /b2b/roster | Yes (company member) | List the company's beneficiaries |
| GET | /b2b/roster/export | Yes (company member) | CSV export of the roster + link status |
| POST | /b2b/roster/stage | Yes (company admin) | Stage CSV roster rows (partial accept per row) |
| PATCH | /b2b/roster/:rosterId | Yes (company admin) | Change or clear one beneficiary's package assignment |
| DELETE | /b2b/roster/:rosterId | Yes (company admin) | Remove one beneficiary — roster row, link history and their account |
| POST | /b2b/roster/links/send | Yes (company admin) | Mint and deliver activation invite links |
| POST | /b2b/roster/links/export | Yes (company admin) | Mint invite links and return the raw links for self-distribution |
| POST | /b2b/roster/links/reissue-expired | Yes (company admin) | Re-mint every expired link for the company |
| POST | /b2b/roster/links/revoke | Yes (company admin) | Revoke the selected beneficiaries' open invitations (partial accept per row) |
| GET | /b2b/catalog | Yes (company member) | B2B-priced package catalog for the quote wizard |
| GET | /b2b/quotes | Yes (company member) | List the company's quotes |
| GET | /b2b/quotes/:id | Yes (company member) | Quote detail |
| POST | /b2b/quotes | Yes (company admin) | Create a quote draft |
| PATCH | /b2b/quotes/:id | Yes (company admin) | Update a draft quote |
| POST | /b2b/quotes/:id/submit | Yes (company admin) | Submit a draft for review |
| POST | /b2b/quotes/:id/accept | Yes (company admin) | Accept a countered quote version |
| POST | /b2b/quotes/:id/reject | Yes (company admin) | Decline a countered quote version, with an optional reason |
| GET | /b2b/contract | Yes (company member) | The company's active contract |
| GET | /b2b/readiness | Yes (company member) | Contract/payment/roster readiness gate |
| GET | /b2b/dashboard | Yes (company member) | Portal dashboard overview |
| GET | /b2b/billing | Yes (company member) | Billing/charges summary |
| GET | /b2b/billing/export | Yes (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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/companies | Yes (b2b area) | List companies |
| POST | /admin/companies/invites | Yes (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-admins | Yes (super_admin or company-onboarding area) | Invited company admins who have not registered their company yet |
| GET | /admin/companies/:id | Yes (b2b area) | Company detail |
| PATCH | /admin/companies/:id | Yes (b2b area, manage) | Edit a company's CR number, name, and contact details |
| GET | /admin/companies/:id/users | Yes (super_admin or company-people area) | The company's portal users with their company role |
| GET | /admin/companies/:id/history | Yes (b2b area) | Company audit history |
| POST | /admin/companies/:id/status | Yes (b2b area, manage) | Approve, reject, suspend, reactivate, archive, or unarchive a company |
| GET | /admin/quotes | Yes (b2b area) | List quotes across all companies |
| GET | /admin/quotes/catalog | Yes (b2b area) | Catalog for the counter-quote line-item picker |
| GET | /admin/quotes/:id | Yes (b2b area) | Quote detail |
| POST | /admin/quotes/:id/start-review | Yes (b2b area, manage) | Move a submitted quote into review |
| POST | /admin/quotes/:id/counter | Yes (b2b area, manage) | Counter a quote with a new linked version |
| POST | /admin/quotes/:id/reject | Yes (b2b area, manage) | Reject a quote |
| POST | /admin/quotes/:id/confirm-contract | Yes (b2b area, manage) | Accept the quote and create its contract |
| GET | /admin/contracts/company/:companyId | Yes (b2b area) | Company's active contract |
| GET | /admin/contracts/company/:companyId/readiness | Yes (b2b area) | Contract/payment/roster readiness gate |
| GET | /admin/contracts/company/:companyId/balance | Yes (b2b area) | Flexible funding balance (funded/drawn/committed/available + active lines) |
| POST | /admin/contracts/:id/payments | Yes (b2b area, manage) | Record a payment (with document) — prepaid pay-in-full or a flexible funding/top-up amount |
| GET | /admin/contracts/:id/payments | Yes (b2b area) | List payment records (metadata only) |
| GET | /admin/contracts/:id/payments/:paymentId/document | Yes (b2b area, manage) | Download a payment's supporting document |
| GET | /admin/links | Yes (b2b area) | Cross-company magic-link delivery oversight |
| GET | /admin/links/export | Yes (b2b area) | CSV export of magic-link oversight rows |
Settings
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/settings/organization | Yes (super_admin) | Get the seller-of-record settings used on generated PDFs |
| PATCH | /admin/settings/organization | Yes (super_admin) | Update the seller-of-record settings |
| GET | /admin/settings/fx-rates | Yes (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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/store/providers | Yes (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/:id | Yes (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/products | Yes (super_admin) | List all store products, including inactive rows, provider linkage, isActiveCustomized, and { data, total, nextCursor, hasMore } pagination |
| PATCH | /admin/store/products/:id | Yes (super_admin) | Update store product overrides, active state, and customization flags |
| GET | /admin/store/orders | Yes (super_admin or store area) | List all store orders with stuck, issuance state, provider, age, and user filters |
| GET | /admin/store/orders/:id | Yes (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/counts | Yes (super_admin or store area) | Store order attention counts using the list filters |
| GET | /admin/store/orders/:id/history | Yes (super_admin or store area) | Store order audit history |
| POST | /admin/store/orders/:id/requeue | Yes (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/sync | Yes (super_admin) | Queue a manual eSIM store catalog sync (202 queued, 503 STORE_SYNC_UNAVAILABLE when enqueue fails) |
Documents
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /documents/:type/:entityId.pdf | Yes | Render and download a generated PDF (receipt or contract-payment document); access is authorized per document type |
Compliance
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /compliance/retention/sweep | Yes (super_admin) | Execute PDPL data retention sweep |
| POST | /compliance/breach-report | Yes (super_admin) | Generate SDAIA breach notification report |
Webhooks
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /webhooks/moyasar | No (body secret verified) | Handle Moyasar payment webhook events |
| POST | /webhooks/paymob | No (HMAC verified) | Handle Paymob transaction processed callbacks |
| POST | /webhooks/mobimatter | No (signature verified) | Reconcile delayed eSIM order completion |
| POST | /webhooks/notifications/:channel/:provider | No (signature verified) | Persist and enqueue email or SMS delivery callbacks |
Detailed Endpoint Documentation
| Section | Document |
|---|---|
| Authentication | auth.md |
| Activations | activations.md |
| Payments | payments.md |
| Packages | packages.md |
| eSIM Store | store.md |
| Operators | operators.md |
| Profile | profile.md |
| Notifications | notifications.md |
| Admin Users | admin-users.md |
| Customers | customers.md |
| Audit History | audit.md |
| B2B Portal | b2b.md |
| B2B Admin | b2b-admin.md |
| Settings | settings.md |
| Documents | documents.md |
| Compliance | compliance.md |
| Webhooks | webhooks.md |
| Error Codes | errors.md |
SIM Limits by Identity Type (CITC)
Per CITC regulations, users are limited by identity type:
| Identity Type | Max SIMs |
|---|---|
citizen | 10 |
resident | 2 |
visitor | 1 |
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.