Skip to content

Packages API

Browse, search, filter, and compare telecom SIM packages from Saudi operators (STC, Mobily, Zain).


GET /packages

List packages with optional filters. Returns up to 50 packages ordered by price ascending.

Query Parameters

ParameterTypeDefaultDescription
operatorIdstringFilter by operator UUID
dataMinMbintegerMinimum data amount in MB
voiceMinMinutesintegerMinimum voice minutes
validityDaysintegerExact validity in days
isActivebooleanOnly active packages (default: only active)
isPromotionalbooleanOnly promotional packages

Example Requests

All packages:

GET /packages

Filter by operator:

GET /packages?operatorId=f47ac10b-58cc-4372-a567-0e02b2c3d479

Filter by minimum data and promotional:

GET /packages?dataMinMb=5000&isPromotional=true

Combined filters:

GET /packages?operatorId=f47ac10b-58cc-4372-a567-0e02b2c3d479&dataMinMb=5000&voiceMinMinutes=100&validityDays=30

Response

200 OK

json
{
  "data": [
    {
      "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "STC 50 SAR Plan",
      "description": "5GB data + 60 min voice",
      "dataAmountMb": 5120,
      "voiceMinutes": 60,
      "smsCount": 50,
      "validityDays": 30,
      "priceSar": 49.99,
      "isPromotional": false,
      "createdAt": "2026-04-15T08:00:00.000Z",
      "updatedAt": "2026-04-15T08:00:00.000Z"
    },
    {
      "id": "8a17d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "STC 100 SAR Plan",
      "description": "10GB data + 120 min voice",
      "dataAmountMb": 10240,
      "voiceMinutes": 120,
      "smsCount": 100,
      "validityDays": 30,
      "priceSar": 99.99,
      "isPromotional": true,
      "createdAt": "2026-04-16T09:00:00.000Z",
      "updatedAt": "2026-04-16T09:00:00.000Z"
    }
  ],
  "total": 2
}

Display fields (name, description) are localized based on Accept-Language. When Accept-Language: ar, the response returns Arabic names and descriptions.

Arabic response (Accept-Language: ar):

json
{
  "data": [
    {
      "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "باقة STC ٥٠ ريال",
      "description": "٥ جيجا بيانات + ٦٠ دقيقة صوت",
      "dataAmountMb": 5120,
      "voiceMinutes": 60,
      "smsCount": 50,
      "validityDays": 30,
      "priceSar": 49.99,
      "isPromotional": false,
      "createdAt": "2026-04-15T08:00:00.000Z",
      "updatedAt": "2026-04-15T08:00:00.000Z"
    }
  ],
  "total": 1
}

Full-text search across package names and descriptions. Uses a two-phase search strategy:

  1. Primary: PostgreSQL tsquery full-text search using the search_vector column, ranked by ts_rank
  2. Fallback: If full-text returns no results, trigram similarity (% operator) on nameEn, nameAr, descriptionEn, descriptionAr, ordered by similarity()

Query Parameters

ParameterTypeRequiredDefaultDescription
qstringYesSearch query (1–200 characters)
operatorIdstringNoFilter by operator UUID
dataMinMbintegerNoMinimum data amount in MB
voiceMinMinutesintegerNoMinimum voice minutes
validityDaysintegerNoExact validity in days
isPromotionalbooleanNoOnly promotional packages

Results are limited to 20 items.

Example Requests

Simple search:

GET /packages/search?q=prepaid+data

Search with filters:

GET /packages/search?q=prepaid+data&operatorId=f47ac10b-58cc-4372-a567-0e02b2c3d479&dataMinMb=5000

Response

200 OK — Full-text match:

json
{
  "data": [
    {
      "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "Prepaid Data 10GB",
      "description": "10GB data package with unlimited social media",
      "dataAmountMb": 10240,
      "voiceMinutes": 0,
      "smsCount": 0,
      "validityDays": 30,
      "priceSar": 49.99,
      "isPromotional": false,
      "relevance": 0.3521,
      "matchedBy": "fulltext",
      "createdAt": "2026-04-15T08:00:00.000Z"
    }
  ],
  "total": 1,
  "matchedBy": "fulltext"
}

200 OK — Fuzzy (trigram) fallback:

json
{
  "data": [
    {
      "id": "8a17d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "Pre-Paid Data Pack 20GB",
      "description": "20GB data with free STC WiFi",
      "dataAmountMb": 20480,
      "voiceMinutes": 100,
      "smsCount": 50,
      "validityDays": 30,
      "priceSar": 79.99,
      "isPromotional": false,
      "matchedBy": "fuzzy",
      "createdAt": "2026-04-15T08:00:00.000Z"
    }
  ],
  "total": 1,
  "matchedBy": "fuzzy"
}

Fuzzy results do not include a relevance field. The matchedBy field indicates the search strategy used.

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Missing q parameter or invalid filter values

GET /packages/compare

Side-by-side comparison of 2–5 packages. Includes computed pricePerGb for easy comparison.

Query Parameters

ParameterTypeRequiredDescription
idsstringYesComma-separated package UUIDs (2–5 IDs)

Example Requests

GET /packages/compare?ids=9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e,8a17d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e

3-way comparison:

GET /packages/compare?ids=id1,id2,id3

Response

200 OK

json
{
  "data": [
    {
      "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
      "name": "STC 50 SAR Plan",
      "description": "5GB data + 60 min voice",
      "operatorName": "Saudi Telecom Company",
      "dataAmountMb": 5120,
      "voiceMinutes": 60,
      "smsCount": 50,
      "validityDays": 30,
      "priceSar": 49.99,
      "pricePerGb": 9.76
    },
    {
      "id": "7f16e5b3-2d6f-4c1a-9e8d-5b7a6c4d3e2f",
      "name": "Mobily 50 SAR Plan",
      "description": "6GB data + 45 min voice",
      "operatorName": "Mobily",
      "dataAmountMb": 6144,
      "voiceMinutes": 45,
      "smsCount": 30,
      "validityDays": 30,
      "priceSar": 49.99,
      "pricePerGb": 8.14
    }
  ]
}

operatorName and name/description are localized based on Accept-Language.

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Fewer than 2 or more than 5 IDs provided

Validation error example:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Please provide 2 to 5 package IDs for comparison"
  }
}

GET /packages/:id

Single package detail including localized display fields, raw bilingual fields for edit forms, customization flags, B2B pricing, and operator information.

Path Parameters

ParameterTypeDescription
idstringUUID of the package

Example Request

GET /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e

Response

200 OK

json
{
  "data": {
    "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
    "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "name": "STC 50 SAR Plan",
    "description": "5GB data + 60 min voice",
    "nameEn": "STC 50 SAR Plan",
    "nameAr": "باقة STC ٥٠ ريال",
    "descriptionEn": "5GB data + 60 min voice",
    "descriptionAr": "٥ جيجا بيانات + ٦٠ دقيقة صوت",
    "dataAmountMb": 5120,
    "voiceMinutes": 60,
    "smsCount": 50,
    "validityDays": 30,
    "priceSar": 49.99,
    "priceSarB2b": 44.99,
    "isActive": true,
    "isPromotional": false,
    "isNameCustomized": false,
    "isPriceCustomized": false,
    "operatorOriginal": {
      "nameEn": "STC 50 SAR Plan",
      "nameAr": "باقة STC ٥٠ ريال",
      "descriptionEn": "5GB data + 60 min voice",
      "descriptionAr": "٥ جيجا بيانات + ٦٠ دقيقة صوت",
      "priceSar": 49.99
    },
    "operator": {
      "name": "Saudi Telecom Company",
      "slug": "stc"
    },
    "createdAt": "2026-04-15T08:00:00.000Z",
    "updatedAt": "2026-04-15T08:00:00.000Z"
  }
}

name, description, and operator name are localized based on Accept-Language. nameEn, nameAr, descriptionEn, and descriptionAr are raw bilingual fields for admin editing. priceSarB2b is null for packages without B2B pricing. operatorOriginal holds the operator's current value for each customizable field — present even when a field is customized, so the editor can show what an override replaced and what a revert restores. Its fields are null until the first sync. It is returned only to authenticated super_admin callers (it reveals the pre-markup price) and is omitted from the public response — the example above shows the admin view.

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Invalid package ID
PACKAGE_NOT_FOUND404Package not found or not active

PATCH /packages/:id

Update package customization fields. Requires authentication and super_admin role.

This endpoint edits only fields that are safe for super-admin customization. Operator-owned package spec fields such as data amount, minutes, SMS count, validity, operator ID, and promotional state are not editable here. isActive is editable (sets isActiveCustomized — the sync's CASE guard preserves admin deactivations and reactivations). Every successful update writes an immutable audit entry with actor, client metadata, requested fields, and before/after customization values.

Path Parameters

ParameterTypeDescription
idstringUUID of the package

Request Body

FieldTypeDescription
nameEnstringEnglish package name. Sets isNameCustomized to true.
nameArstringArabic package name. Sets isNameCustomized to true.
descriptionEnstring | nullEnglish package description. Sets isNameCustomized to true.
descriptionArstring | nullArabic package description. Sets isNameCustomized to true.
priceSarnumberRetail price. Sets isPriceCustomized to true. At most 99,999,999.99 (the numeric(10,2) column ceiling).
priceSarB2bnumber | nullB2B price. Does not use a customization flag because operator sync does not overwrite it. At most 99,999,999.99 (the numeric(10,2) column ceiling).
isNameCustomizedfalseClear text customization ownership. Operator text values return on the next package sync.
isPriceCustomizedfalseClear retail price customization ownership. Operator price returns on the next package sync.
isActivebooleanSet the active status. Sets isActiveCustomized to true so the package sync never overrides this decision.
isActiveCustomizedfalseClear active-status customization ownership. The next package sync may reactivate or deactivate the row.

The request body must include at least one field. Unknown fields are rejected.

Do not combine a field edit with clearing its matching flag in the same request. For example, nameEn with isNameCustomized: false is rejected because it is ambiguous.

Example Requests

Customize bilingual text:

http
PATCH /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "nameEn": "Skyte STC 50 SAR Plan",
  "nameAr": "باقة سكايتي STC ٥٠ ريال",
  "descriptionEn": "5GB data + 60 min voice",
  "descriptionAr": "٥ جيجا بيانات + ٦٠ دقيقة صوت"
}

Customize retail price:

http
PATCH /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "priceSar": 47.99
}

Set or clear B2B price:

http
PATCH /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "priceSarB2b": null
}

Revert to operator-owned values on next sync:

http
PATCH /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "isNameCustomized": false,
  "isPriceCustomized": false
}

Reactivate a previously-deactivated package:

http
PATCH /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "isActive": true
}

Setting isActive also sets isActiveCustomized to true so the next sync does not revert the reactivation. To hand ownership back to the sync engine, send isActiveCustomized: false in a separate request. The PATCH endpoint works on inactive packages (unlike GET /packages/:id which returns 404 for inactive rows).

Response

200 OK

Returns the same expanded detail shape as GET /packages/:id after the update.

json
{
  "data": {
    "id": "9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e",
    "operatorId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "name": "Skyte STC 50 SAR Plan",
    "description": "5GB data + 60 min voice",
    "nameEn": "Skyte STC 50 SAR Plan",
    "nameAr": "باقة سكايتي STC ٥٠ ريال",
    "descriptionEn": "5GB data + 60 min voice",
    "descriptionAr": "٥ جيجا بيانات + ٦٠ دقيقة صوت",
    "dataAmountMb": 5120,
    "voiceMinutes": 60,
    "smsCount": 50,
    "validityDays": 30,
    "priceSar": 47.99,
    "priceSarB2b": null,
    "isActive": true,
    "isPromotional": false,
    "isNameCustomized": true,
    "isPriceCustomized": true,
    "operatorOriginal": {
      "nameEn": "STC 50 SAR Plan",
      "nameAr": "باقة STC ٥٠ ريال",
      "descriptionEn": "5GB data + 60 min voice",
      "descriptionAr": "٥ جيجا بيانات + ٦٠ دقيقة صوت",
      "priceSar": 49.99
    },
    "operator": {
      "name": "Saudi Telecom Company",
      "slug": "stc"
    },
    "createdAt": "2026-04-15T08:00:00.000Z",
    "updatedAt": "2026-04-15T08:05:00.000Z"
  }
}

Error Responses

CodeStatusCondition
VALIDATION_ERROR400Invalid package ID, empty body, unknown field, non-editable field, or edit-plus-revert conflict
AUTH_REQUIRED401Missing or invalid authentication token
AUTH_FORBIDDEN403Authenticated user is not super_admin
PACKAGE_NOT_FOUND404Package not found or not active

GET /packages/:id/availability

Real-time availability check via the operator plugin. Results are cached in Redis for 30 seconds.

Path Parameters

ParameterTypeDescription
idstringUUID of the package

Example Request

GET /packages/9b18d6a4-3e7f-4a2b-8c5d-6f7a8b9c0d1e/availability

Response

200 OK — Available:

json
{
  "data": {
    "available": true,
    "estimatedActivationMinutes": 5,
    "degraded": false
  },
  "cached": false
}

200 OK — Cached result:

json
{
  "data": {
    "available": true,
    "estimatedActivationMinutes": 5,
    "degraded": false
  },
  "cached": true
}

200 OK — Out of stock:

json
{
  "data": {
    "available": false,
    "reason": "out_of_stock",
    "degraded": false
  }
}

200 OK — Operator unavailable (degraded mode):

json
{
  "data": {
    "available": null,
    "reason": "operator_unavailable",
    "degraded": true
  }
}

200 OK — Operator health check failed:

json
{
  "data": {
    "available": null,
    "reason": "unavailable",
    "degraded": true
  }
}

When degraded: true, the availability is unknown (available: null) because the operator API is not reachable.

Error Responses

CodeStatusCondition
PACKAGE_NOT_FOUND404Package not found

Internal documentation - Activation System