Num Corporate API — v1
For travel companies, agencies and resellers issuing eSIMs programmatically.
Status: sandbox is open. Any approved account can integrate today with a numk_test_ key: free play credit, demo eSIMs, identical behaviour to live. Live keys, live credit and real eSIMs are switched on per account once pricing and settlement are agreed — ask us.
Base URL:
https://numesim.uk/v1
(The raw Functions URL https://us-central1-tinger-92db3.cloudfunctions.net/corporateApi/v1 also works and will keep working.)
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:
| Prefix | Mode | Money | eSIMs |
|---|---|---|---|
numk_test_ | Sandbox | Play credit, topped up free | Demo profiles, fake ICCID, no vendor order |
numk_live_ | Live | Real credit | Real 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:
| Gateway | Who the traveller pays | Effect on your credit |
|---|---|---|
| Stripe (ours) | Num | None. We keep wholesale and credit your margin back as commission |
| Your own — SSLCommerz etc. | You | Debited 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 } }
| Status | Meaning |
|---|---|
400 | Malformed request — missing field, bad country code |
401 | Missing, unknown or malformed key |
402 | Insufficient credit. detail carries the shortfall |
403 | Key is not a corporate key, account suspended, or mode mismatch |
404 | No such order, plan or path |
412 | Gateway not configured for this mode |
429 | Rate limited — 120 requests per minute per key |
502 | Vendor failed. Your credit has already been refunded |
500 | Our 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:
- Switch countries off. A disabled country returns
404from/v1/catalogand403from/v1/orders, so a booking engine cannot offer something you do not sell. - Switch individual packages off. Small data bundles are support load rather than revenue for some resellers; turn them off and they disappear from the catalogue entirely.
- Set your markup, globally or per country. A country override wins over 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
| Field | Required | Notes |
|---|---|---|
planId | yes | From /v1/catalog |
country | yes | Two letters, uppercase |
funding | no | credit (default) or checkout |
customerEmail | no | For checkout, prefills the payment page |
retailMinor | no | For checkout, what the traveller pays. Defaults to your configured retail |
successUrl / cancelUrl | no | For checkout |
gateway | no | For 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.
| Status | Meaning |
|---|---|
provisioning | Charged, waiting on the carrier |
awaiting_payment | Checkout open, not yet paid |
pending | Carrier has the order, profile not released |
active | Ready. activationCode present |
failed | Not 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 | |
|---|---|
topup | Credit added |
order | eSIM charged |
refund | Provisioning failed, charge reversed |
commission | Your margin on a Stripe-settled checkout order |
adjustment | Manual 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
- 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. - Configure your own gateway under Payments if you want us to host your checkout. Sandbox credentials first.
- Ask us to enable live. We will confirm pricing and settlement.
- 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
- No webhooks yet. Poll
GET /v1/orders/{id}. Order-ready callbacks are planned; polling has no delivery guarantees to get wrong in the meantime. - No cancellation. An issued eSIM cannot be un-issued. Failed orders refund automatically; anything else, talk to us.
- Gateway credentials are held on your account record today. Adequate for 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.
- One currency. Everything settles in USD. Your traveller can pay in local currency through your own gateway; what you owe us is USD.