Num Corporate API — v1

For travel companies, agencies and resellers issuing eSIMs programmatically.

Base URL (until api.numesim.uk is in place):

https://us-central1-tinger-92db3.cloudfunctions.net/corporateApi

All paths below are relative to that. JSON in, JSON out, UTF-8.


1. Authentication

A bearer key on every request:

Authorization: Bearer numk_test_9f2c…

Keys are created in the business console at https://numesim.uk/business/ and shown once. We store only a SHA-256 hash — if a key is lost it cannot be recovered, only replaced.

There is no CORS. These endpoints are server-to-server. A key that reaches browser JavaScript is a public key, and anyone reading the page can spend your credit.

Modes

The prefix is the mode, and it is not decoration:

PrefixModeMoneyeSIMs
numk_test_SandboxPlay credit, topped up freeDemo profiles, fake ICCID, no vendor order
numk_live_LiveReal creditReal eSIMs from the carrier

The two never mix. A sandbox key cannot read a live order, spend live credit or appear in a live statement. Mode is checked against both the key prefix and the stored record on every request; if they disagree the request is refused rather than resolved in either direction.


2. Two ways to fund an order

This is the decision that shapes everything else, and it is per-order.

funding: "credit"

You have prepaid credit with us. You collect from your traveller however you already do — your own gateway, an invoice, cash at a desk, bundled into a tour price — and the order draws down your balance.

We need no integration with your payment provider at all. Your money stays your business. Any gateway works, including ones we have never heard of.

The eSIM is issued immediately and the activation code comes back in the same response.

funding: "checkout"

We return a payment link for your traveller. What happens next depends on whose merchant account it runs on:

GatewayWho the traveller paysEffect on your credit
Stripe (ours)NumNone. We keep wholesale and credit your margin back as commission
Your own — SSLCommerz etc.YouDebited at issue, exactly as a credit order. The money never reaches us

That second row is the important one. If your gateway collects, we have not been paid for that eSIM — so it comes out of credit. We are only hosting the payment page for you.

Configure your own gateway under Payments in the console. Stripe needs no configuration; it runs on our account.


3. Idempotency

POST /v1/orders requires an Idempotency-Key header. Any string unique to the attempt — a UUID, your own booking reference:

Idempotency-Key: booking-88421-attempt-1

Repeating a request with the same key returns the original order and does not charge again. Retries are normal; a money API that treats a retry as a new instruction double-charges on exactly the request the client was least sure about.

Keys are remembered per account. Reuse across different orders is a bug on your side and will return the wrong order.


4. Errors

Standard HTTP status, with a JSON body:

{ "error": "Insufficient credit.",
  "detail": { "balanceMinor": 1200, "requiredMinor": 3400, "shortfallMinor": 2200 } }
StatusMeaning
400Malformed request — missing field, bad country code
401Missing, unknown or malformed key
402Insufficient credit. detail carries the shortfall
403Key is not a corporate key, account suspended, or mode mismatch
404No such order, plan or path
412Gateway not configured for this mode
429Rate limited — 120 requests per minute per key
502Vendor failed. Your credit has already been refunded
500Our fault

On 502 the order is left at status: "failed" and a compensating refund entry appears on your statement. You are never charged for an eSIM that was not issued.

Money

Every amount is an integer in minor units of USD — 1234 is $12.34. We return the decimal string alongside it for display, never for arithmetic.


5. Endpoints

GET /v1/me

curl -H "Authorization: Bearer $KEY" \
  https://.../corporateApi/v1/me
{ "companyName": "TravelCo Ltd", "mode": "sandbox", "discountPercent": 15,
  "balance": { "amountMinor": 50000, "amount": "500.00", "currency": "USD" } }

GET /v1/catalog?country=JP

Plans available for a country, at your price.

{ "country": "JP",
  "plans": [
    { "planId": "abc123", "name": "Japan 3GB 15 days", "country": "JP",
      "dataMb": 3072, "days": 15, "topUp": true,
      "wholesale": { "amountMinor": 275, "amount": "2.75", "currency": "USD" },
      "retail":    { "amountMinor": 330, "amount": "3.30", "currency": "USD" } }
  ] }

wholesale is what we charge you: VIP-tier pricing, less your account discount — an API client buying in volume is never quoted the walk-up price. retail is your own price, calculated from the markup you set in the console.

Your catalogue is yours to shape. Under Catalogue in the console you can:

/v1/catalog and 403 from /v1/orders, so a booking engine cannot offer something you do not sell.

rather than revenue for some resellers; turn them off and they disappear from the catalogue entirely.

the default.

Prices move with the carrier catalogue. Read them at order time rather than caching them for days.


POST /v1/orders

curl -X POST \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: booking-88421" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "abc123", "country": "JP", "funding": "credit" }' \
  https://.../corporateApi/v1/orders
FieldRequiredNotes
planIdyesFrom /v1/catalog
countryyesTwo letters, uppercase
fundingnocredit (default) or checkout
customerEmailnoFor checkout, prefills the payment page
retailMinornoFor checkout, what the traveller pays. Defaults to your configured retail
successUrl / cancelUrlnoFor checkout
gatewaynoFor checkout. stripe (default) or one you have configured

funding: "credit" — 201

{ "id": "kZ9…", "status": "active", "funding": "credit", "mode": "sandbox",
  "country": "JP", "planName": "Japan 3GB 15 days",
  "charged": { "amountMinor": 275, "amount": "2.75", "currency": "USD" },
  "activationCode": "LPA:1$rsp.truphone.com$QR-G7…",
  "iccid": "8900000000012345678" }

activationCode is the LPA string. Render it as a QR for the traveller, or give it to them to enter manually under Add eSIM → Enter details manually.

funding: "checkout" — 201

{ "id": "kZ9…", "status": "awaiting_payment", "funding": "checkout",
  "reference": "NUMC-K3F91Z",
  "retail": { "amountMinor": 400, "amount": "4.00", "currency": "USD" },
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_…",
  "activationCode": null }

Send the traveller to checkoutUrl. The eSIM is issued when payment confirms; poll GET /v1/orders/{id} until status is active and activationCode is present.

The response also carries gateway and settlement. When settlement is corp the order has already been charged to your credit — your gateway is collecting from the traveller on your behalf, and that money never reaches us. When it is num, nothing was charged and your margin arrives as a commission entry once payment confirms.


GET /v1/orders · GET /v1/orders/{id}

{ "mode": "sandbox", "orders": [ { "id": "kZ9…", "status": "active", … } ] }

limit up to 100, newest first. Only orders in the key's own mode.

StatusMeaning
provisioningCharged, waiting on the carrier
awaiting_paymentCheckout open, not yet paid
pendingCarrier has the order, profile not released
activeReady. activationCode present
failedNot issued. Credit refunded

GET /v1/balance

{ "mode": "sandbox",
  "balance": { "amountMinor": 49725, "amount": "497.25", "currency": "USD" } }

GET /v1/ledger

Every movement, newest first. limit up to 100.

{ "mode": "sandbox",
  "entries": [
    { "id": "e2", "type": "order", "amountMinor": -275, "amount": "-2.75",
      "balanceAfter": "497.25", "ref": "kZ9…", "note": "Japan 3GB 15 days · JP",
      "createdAt": "2026-08-21T09:14:02.000Z" },
    { "id": "e1", "type": "topup", "amountMinor": 50000, "amount": "500.00",
      "balanceAfter": "500.00", "note": "Sandbox credit", … }
  ] }
Type
topupCredit added
ordereSIM charged
refundProvisioning failed, charge reversed
commissionYour margin on a Stripe-settled checkout order
adjustmentManual correction by us, always with a note

The ledger is append-only. Balances are derived from it and never edited directly, so the statement and the balance cannot disagree.


6. Rate limits

120 requests per minute per key. Over that returns 429; wait and retry.

The limit exists to stop a runaway polling loop, not to meter usage. If you need more for a genuine burst, ask.


7. Going live

  1. Build against a numk_test_ key. Sandbox credit is free and instant, and

demo eSIMs cost nothing — install one on a real phone to check your flow.

  1. Configure your own gateway under Payments if you want us to host your

checkout. Sandbox credentials first.

  1. Ask us to enable live. We will confirm pricing and settlement.
  2. Top up live credit in the console, create a numk_live_ key, and change

one string in your config.

Nothing else differs between the two. That is deliberate — a sandbox that behaves differently from live is a sandbox that teaches you the wrong thing.


8. Known limits, stated plainly

planned; polling has no delivery guarantees to get wrong in the meantime.

refund automatically; anything else, talk to us.

sandbox, not yet hardened for live — they move to a managed secret store before any live merchant account is enabled. We would rather say so than have you assume otherwise.

currency through your own gateway; what you owe us is USD.