EN

Developer API — Sell Turnaa eSIMs

Turnaa B2B API

Sell Turnaa travel eSIMs from your own platform. You buy at wholesale from a prepaid wallet; we provision real eSIMs through our carrier network and return the QR / activation payload to you in seconds — over a simple REST API.

https://esim.turnaa.cloud/wp-json/turnaa/v1
Get your API key →

Quickstart

Three steps from zero to your first eSIM:

  1. Get a keyapply for a B2B account. You start in sandbox, so you can build the whole integration before funding anything.
  2. Call the API — every request carries your key as a Bearer token (below).
  3. Go live — top up your wallet and we flip your key to live; the same calls now provision real eSIMs.
shellKEY="tb2b_sk_xxxxxxxxxxxxxxxxxxxx"

# list Japan plans with your wholesale price
curl -s -H "Authorization: Bearer $KEY" \
  "https://esim.turnaa.cloud/wp-json/turnaa/v1/b2b/catalog?country=jp&days=7"

Conventions

Item Value
Base URL https://esim.turnaa.cloud/wp-json/turnaa/v1
Protocol HTTPS only · REST · JSON request & response bodies
Auth Authorization: Bearer <api_key> on every request
Money All amounts are USD, decimal. currency is always "USD".
Country ISO 3166-1 alpha-2, lowercase (e.g. jp, it, us)
Mode sandbox or live — a property of your key, returned by /balance
IDs plan_id is a stable integer; use it for catalog lookups and orders

Authentication

Send your API key as a Bearer token on every request. Keys are shown once in your dashboard and stored only as a hash — roll a new one anytime (the old key stops working immediately).

httpAuthorization: Bearer tb2b_sk_xxxxxxxxxxxxxxxxxxxx
Failure HTTP When
unauthorized 401 Missing or invalid Bearer key
forbidden 403 Key valid but the partner account is paused, or is not a B2B API account

Errors

Errors use standard HTTP status codes and a JSON body with a machine-readable code:

json{ "code": "insufficient_funds",
  "message": "Wallet balance too low — top up required",
  "data": { "status": 402 } }
HTTP code Meaning
400 invalid_plan Unknown plan_id, or the plan is not orderable
400 missing_country A required parameter was not supplied
401 unauthorized Missing or invalid API key
402 insufficient_funds Wallet balance below the order total — top up
403 forbidden Partner account paused or not a B2B API account
404 not_found Order not found (or not owned by your account)
500 supplier_unavailable Supplier client not reachable — nothing charged
502 supplier_error Carrier-side failure — the order is refunded automatically

Rate limits & fair use

Poll an order at most once every 3 seconds. The catalog changes only a few times a day — cache it on your side for ~6 hours rather than fetching per page view. Bulk orders should send one request per plan_id with a qty rather than many parallel calls.

GET /b2b/catalog

Every orderable plan with your wholesale price and the suggested retail price.

Query param Type Description
country string optional ISO alpha-2 filter (e.g. jp). Omit for the full catalog.
days integer optional Only plans whose validity is ≥ this many days
limit integer optional Max rows (default & cap 500)
shellcurl -s -H "Authorization: Bearer $KEY" \
  "https://esim.turnaa.cloud/wp-json/turnaa/v1/b2b/catalog?country=jp&days=7"
json[
  {
    "plan_id": 3732,
    "name": "Japan · Unlimited · 1 day",
    "data_mb": 3072,
    "duration_days": 1,
    "unlimited": true,
    "partner_price": 3.54,
    "retail_price": 9.45,
    "currency": "USD"
  }
]
Field Type Description
plan_id integer Stable plan identifier — pass to /orders
name string Human-readable plan name
data_mb integer Data allowance in MB (per-day for unlimited plans)
duration_days integer Validity in days
unlimited boolean true for daily-unlimited (fair-use) plans
partner_price number Your wholesale price per unit (debited from your wallet)
retail_price number Suggested retail — your margin is the difference

GET /b2b/suggest

Trip-aware plan picker: give a destination and the trip dates (or a day count) and we return the plans that fit, cheapest first, each flagged fits_trip.

Query param Type Description
country string required ISO alpha-2 destination
days integer optional Trip length in days
start, end date optional YYYY-MM-DD — used to compute trip days (inclusive) when days is absent
shellcurl -s -H "Authorization: Bearer $KEY" \
  "https://esim.turnaa.cloud/wp-json/turnaa/v1/b2b/suggest?country=it&start=2026-08-01&end=2026-08-09"
json{
  "trip_days": 9,
  "fit": [
    { "plan_id": 812, "name": "Italy · 3 GB · 15 days",
      "duration_days": 15, "partner_price": 4.92, "retail_price": 11.99,
      "unlimited": false, "currency": "USD", "fits_trip": true }
  ],
  "all": [ /* every plan for the country, fits_trip true/false */ ]
}

GET /b2b/balance

Your prepaid wallet balance and the current mode of your key.

json{ "balance": 250.00, "currency": "USD", "mode": "live" }

POST /b2b/orders

Order one or more eSIMs for a plan. The unit price is debited from your wallet; the response carries the eSIM activation payload.

Body field Type Description
plan_id integer required From the catalog
qty integer optional 1–50 (default 1)
idempotency_key string recommended ≤ 64 chars — replaying the same key returns the original order instead of charging again
customer_email string optional Stored on the order for your reconciliation
shellcurl -s -X POST \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"plan_id":3732,"qty":1,"idempotency_key":"order-abc-123"}' \
  "https://esim.turnaa.cloud/wp-json/turnaa/v1/b2b/orders"
json{
  "id": 4821,
  "status": "completed",
  "mode": "live",
  "unit_price": 3.54,
  "total": 3.54,
  "currency": "USD",
  "balance": 246.46,
  "payload": {
    "esims": [
      {
        "iccid": "89371034000059xxxxx",
        "activation_code": "LPA:1$rsp.example.com$XXXXXXXX",
        "qr_note": "Encode activation_code as a QR for the customer"
      }
    ]
  }
}
When the carrier needs a moment, the order returns "status": "processing" with an empty esims array — poll GET /orders/{id} every few seconds (usually < 30 s) until it is completed. If allocation ultimately fails, the order becomes failed and the full amount is refunded to your wallet automatically.
esims[] field Type Description
iccid string The eSIM’s ICCID
activation_code string LPA activation string — render it as a QR for the traveler

GET /b2b/orders/{id}

Fetch a single order (only your own). Use it to poll a processing order until the payload.esims array is populated.

shellcurl -s -H "Authorization: Bearer $KEY" \
  "https://esim.turnaa.cloud/wp-json/turnaa/v1/b2b/orders/4821"

GET /b2b/ledger

Your wallet statement — every movement, newest first.

Query param Type Description
limit integer optional Rows to return (default 20, cap 100)
json[
  { "id": 5, "type": "debit", "amount": 3.54, "balance_after": 246.46,
    "note": "Order #4821", "order_id": 4821, "created_at": "2026-07-15 09:12:04" }
]
type Meaning
credit Wallet top-up
debit An order charge
refund A failed order returned to your wallet
adjustment Manual correction by Turnaa

Order lifecycle

status Meaning
completed eSIMs allocated — payload.esims holds ICCID + activation code
processing Carrier is allocating — poll the order
failed Allocation failed — wallet refunded automatically
sandbox_completed Sandbox order — test payload, no charge

Idempotency

Send a unique idempotency_key with every POST /orders. If a request is retried (network blip, timeout) with the same key, we return the original order — flagged "idempotent": true — and never charge twice. Use one key per intended purchase (e.g. your own order id).

Sandbox & live

Every account starts in sandbox. All endpoints behave identically, but orders return a clearly-fake payload and nothing is charged:

json{
  "status": "sandbox_completed",
  "payload": { "esims": [
    { "iccid": "SANDBOX1234567890123",
      "activation_code": "LPA:1$sandbox.turnaa.cloud$TEST-XXXXXXXXXXXX",
      "qr_note": "SANDBOX — no real eSIM issued, no wallet charge" }
  ]}
}

Build and test the full flow against sandbox. When you top up your wallet we switch your key to live — the same code then provisions real eSIMs. Check mode from /balance at runtime if you branch on it.

Webhooks

Set an HTTPS webhook URL from your dashboard (Developer tab). We POST a signed JSON event so you don’t have to poll:

event Fired when
order.completed eSIMs allocated — data includes the full esims array
order.failed Allocation failed — amount already refunded
balance.low A sale dropped your balance below the threshold
jsonPOST your-webhook-url
X-Turnaa-Event: order.completed
X-Turnaa-Signature: sha256=9f86d081...

{ "event": "order.completed",
  "created": "2026-07-15T09:12:04+00:00",
  "data": { "order_id": 4821, "status": "completed", "mode": "live",
            "total": 3.54, "currency": "USD", "esims": [ ... ] } }

Verify every delivery — compute HMAC-SHA256(raw_body, signing_secret) and compare it to the X-Turnaa-Signature header (constant-time). The signing secret is shown in your dashboard when you save the URL.

php$raw = file_get_contents('php://input');
$sig = 'sha256=' . hash_hmac('sha256', $raw, $signing_secret);
if (!hash_equals($sig, $_SERVER['HTTP_X_TURNAA_SIGNATURE'] ?? '')) {
    http_response_code(400); exit;
}
$event = json_decode($raw, true);
nodeconst crypto = require('crypto');
const sig = 'sha256=' + crypto.createHmac('sha256', SIGNING_SECRET)
                              .update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.get('X-Turnaa-Signature')||'')))
  return res.sendStatus(400);
Webhooks are a convenience — GET /orders/{id} is always the source of truth. Return 2xx quickly; do slow work asynchronously.

Prefer no code? WordPress plugin

Running WordPress? Skip the raw API — install our reseller plugin, paste your key, and drop a shortcode anywhere:

shortcode

It renders live plans with your prices and blends into your theme. In WooCommerce mode, the Buy buttons run through your own checkout — when the customer pays, the eSIM is ordered from your Turnaa wallet and emailed automatically (QR + activation code) and the order auto-completes. Fully testable in sandbox.

Download the plugin (.zip)

Get access

A B2B account ties your API key, wallet and order history to one Turnaa login. Approval typically takes one business day, and you build entirely in sandbox until you are ready.

Step 1 — create your account

Partner applications are tied to a Turnaa account, so you can track status and manage your API key and wallet from one dashboard.

Sign in / Create account

Then come back here — this box turns into the application form.