Nubatel Developer API

One REST API over the same Nubatel backend the website uses: the same inventory, the same official selling prices, the same coupons and the same automatic fulfilment. Every order created here appears in the Nubatel admin dashboard and in the sales and profit reports, tagged with the sales channel of the key that created it.

Base URL: https://nubatel.com/api/public/v1

Authentication

Every request carries the API key issued to your integration. Keys are shown once when they are created; if one is lost, Nubatel issues a replacement and the old key stops working immediately. Keep the key on your server or in secure app storage — never in public source code.

Authorization: Bearer nb_XXXXXXXXXXXX.YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY

Each key has its own permissions (scopes), its own per-minute request limit and its own sales channel. A key can only read the orders it created.

Customer accounts (mobile app)

The API key identifies the app; a customer token identifies the person. Customers sign up and sign in with email and password against Nubatel accounts, then the app sends the returned token as an extra header on every customer-scoped call:

Authorization: Bearer nb_XXXXXXXXXXXX.YYYY…      // the app key
X-Customer-Token: eyJhbGciOi…                    // the signed-in customer
POST/customers/signupcustomers:auth
Creates an account. Nubatel emails a confirmation link; until it is clicked, sign-in returns 403 email_not_confirmed. The reply includes needs_confirmation.
POST /api/public/v1/customers/signup
{ "email": "sara@example.com", "password": "•••••••••", "full_name": "Sara Ahmed", "language": "en" }
POST/customers/signincustomers:auth
Returns customer and session (access_token, refresh_token, expires_in). Store the tokens in secure device storage.
POST/customers/refreshcustomers:auth
Exchanges refresh_token for a fresh access token, keeping the customer signed in.
POST/customers/password-resetcustomers:auth
Sends the reset email. Always replies { "sent": true }, never revealing whether the account exists.
GET/customers/mecustomers:auth
The signed-in customer's profile. POST the same path to update name, phone or language.

When a customer token is present, POST /orders links the order to that customer (and can fill the name, email, phone and language from their profile), and GET /orders, GET /orders/{id}, /esim, /status and /cancel are restricted to that customer's own orders across every channel they bought on. Without a token, buying still works exactly as before as a guest purchase. A missing or expired token on /customers/me returns 401 customer_unauthorized.

Catalogue, prices and availability

GET/catalog/countriescatalog:read
Every country with at least one package on sale, with the lowest price and how many packages are in stock.
GET/catalog/packages?country=Oman&in_stock=truecatalog:read
All packages for a country. Out-of-stock packages are returned too, with in_stock: false.
GET/catalog/availability?country=Oman&package_code=oman_unlimited_30dcatalog:read
Live availability of one package, read from the shared inventory at that moment.
{
  "packages": [
    {
      "package_code": "oman_unlimited_30d",
      "country": "Oman",
      "data_label": "Unlimited",
      "is_unlimited": true,
      "validity_days": 30,
      "price": { "amount_minor": 2512, "currency": "OMR" },
      "available_count": 6,
      "in_stock": true
    }
  ]
}

Amounts are always integer minor units of the stated currency (2512 OMR minor = 25.120 OMR). OMR, BHD, KWD and TND use three decimals; most other currencies use two.

Creating an order

POST/ordersorders:write
Reserves one eSIM from the shared inventory and returns a hosted payment link. The reservation is released automatically if the payment is not completed.
POST /api/public/v1/orders
{
  "country": "Oman",
  "package_code": "oman_unlimited_30d",
  "customer_name": "Sara Ahmed",
  "customer_email": "sara@example.com",
  "customer_phone": "+96890000000",
  "customer_language": "en",
  "coupon_code": "AWB50"
}
201 Created
{
  "order": {
    "id": "0f3c…",
    "reference": "9a41…",
    "status": "awaiting_payment",
    "payment_status": "awaiting_card_payment",
    "fulfillment_status": "pending",
    "sales_channel": "mobile_app",
    "amounts": {
      "official_price_minor": 2512,
      "price_minor": 2512,
      "discount_minor": 1256,
      "service_fee_minor": 38,
      "total_due_minor": 1294,
      "currency": "OMR"
    },
    "payment_url": "https://nubatel.com/pay/…",
    "payment_expires_at": "2026-09-16T10:00:00.000Z"
  }
}

Open payment_url in a browser or in-app browser. Payment is confirmed by the payment provider directly with Nubatel; the eSIM is assigned and the customer email sent only after that confirmation. Never treat a success page as proof of payment — poll the order instead.

Order status, eSIM and history

GET/orders/{id}orders:read
Payment status and fulfilment status, which are deliberately separate.
GET/orders?customer_email=sara@example.comorders:read
Order history for one customer, limited to orders created with this key.
GET/orders/{id}/esimesim:read
ICCID, activation code, a short-lived QR image link and the iPhone one-tap install link. Available once the order is fulfilled.
GET/orders/{id}/statusesim:read
Live eSIM status: activation state, validity window and remaining data where the network reports it.

Mobile app cheat sheet

The full buying flow for the Nubatel app: account → destination → package → device check → checkout → payment → fulfilment → eSIM in the account. Customer-scoped calls send both the API key (Authorization: Bearer keyId.secret) and the customer session (X-Customer-Token).

POST/customers/signupcustomers:auth
{ full_name, email, password, phone?, language? } → emails a 6-digit code and returns needs_verification: true. Then /customers/verify-email, /customers/resend-otp, /customers/signin, /customers/refresh, /customers/password-reset, /customers/logout.
POST/customers/socialcustomers:auth
{ provider: "google" | "facebook", token } — same customer record, never a duplicate account. GET /customers/social/status reports which providers are switched on.
GET/catalog/countries?scope=local|regional|globalcatalog:read
Destinations with scope, coverage, stock count and the cheapest price. GET /catalog/packages?country=Japan&scope=regional&covers=JP lists the packages, with Local / Regional / Global labels and the same official selling prices as the website.
POST/devices/checkcatalog:read
{ brand?, model } → SUPPORTED, NOT_SUPPORTED or UNKNOWN. Never present UNKNOWN as supported. GET /devices lists brands and models for a picker.
POST/ordersorders:write
Add payment_method (card_sheet, card_link or manual) and device: { brand, model, compatibility, confirmed } where confirmed is the customer ticking "my device supports eSIM and is carrier-unlocked". The response carries next_step. Coupons and ambassador codes are the same codes the website accepts.
POST/orders/{id}/payment-sheetorders:write
Returns client_secret, publishable_key, payment_intent_id, amount_minor, currency, test_mode, merchant_display_name and apple_pay_merchant_id for the Stripe PaymentSheet (card, Apple Pay, Google Pay). The app never touches card numbers. Payment counts only once the provider confirms it to Nubatel — poll /orders/{id} instead of trusting the success screen.
POST/orders/{id}/manual-paymentorders:write
{ currency, payment_method, amount_minor, reference?, notes?, receipt?: { content_type, data_base64 } } — receipt is JPG, PNG or PDF, 10 MB maximum, password-protected PDFs rejected. The order moves to admin review; on approval it is fulfilled automatically, on rejection no eSIM is issued.
GET/customers/me/esims?include_status=trueesim:read
Every eSIM issued to the signed-in customer: ICCID, branded Nubatel QR, iPhone one-tap install link, installation details and live activation, expiry and remaining data.

Test customers (set by an admin) create clearly marked TEST orders that use simulated eSIMs, never consume real stock and never reach the revenue or profit reports.

Cancelling and refunding

POST/orders/{id}/cancelorders:write
Cancels an unpaid order and returns the reserved eSIM to stock. A paid order cannot be cancelled here.
POST/orders/{id}/refundrefunds:request
Full ({"full": true}) or partial ({"amount_minor": 500}) refund through the original payment provider. The returned status is the provider's real status, never an assumed success, and a refund can never exceed the captured amount.

Errors and limits

{ "error": { "code": "out_of_stock", "message": "That package is currently out of stock." } }
  • 401 unauthorized — missing or invalid key.
  • 403 insufficient_scope / client_disabled — the key lacks that permission or has been switched off.
  • 404 not_found — no such order for this key.
  • 409 — out_of_stock, no_price, invalid_coupon, already_paid, not_fulfilled, not_refundable.
  • 429 rate_limited — above the key's per-minute limit; retry after 60 seconds.

Every request is logged permanently (key, endpoint, result, order and timing) and is visible to Nubatel in the admin dashboard.