Wholesale API
eSIM Provisioning API
Provision, activate and monitor eSIMs programmatically, and resell them under your own brand. A single POST returns a ready-to-install QR and LPA activation code.
Introduction
The Journey Wholesale API is a JSON REST API over HTTPS. Orders draw down a prepaid credit balance you top up from your wholesale dashboard — there are no per-request invoices. Every response is JSON; every error uses a consistent envelope with a machine-readable code.
Prepaid & atomic
Each order debits your balance atomically. If provisioning fails, you're automatically refunded — you're never charged for an eSIM you didn't receive.
Instant activation
Orders return the LPA string, SM-DP+ address, matching ID and a base64 QR image, ready to hand to your customer immediately.
Quickstart
Create an API key in your dashboard, then order your first eSIM. Keys carry your live credit, so keep them secret and server-side.
curl -X POST https://journeyesims.com/api/v1/esims \
-H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d" \
-H "Content-Type: application/json" \
-d '{ "planId": "jp", "quantity": 1, "reference": "order-1234" }'const res = await fetch("https://journeyesims.com/api/v1/esims", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.JOURNEY_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ planId: "jp", quantity: 1, reference: "order-1234" })
});
if (!res.ok) throw new Error("Order failed: " + res.status);
const order = await res.json();
console.log(order.esims[0].activation.lpa);import os, requests
res = requests.post(
"https://journeyesims.com/api/v1/esims",
headers={"Authorization": "Bearer " + os.environ["JOURNEY_API_KEY"]},
json={"planId": "jp", "quantity": 1, "reference": "order-1234"},
timeout=30,
)
res.raise_for_status()
order = res.json()
print(order["esims"][0]["activation"]["lpa"])Authentication
Authenticate every request with an API key sent as a Bearer token. Keys look like jrn_live_<prefix>_<secret> and the secret is shown only once, at creation. Requests without a valid key return 401.
curl https://journeyesims.com/api/v1/balance \
-H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d"Base URL & versioning
All endpoints are relative to the versioned base URL. The version is pinned in the path, so v1 stays backward-compatible; any breaking change ships under a new version.
https://journeyesims.com/api/v1Rate limits
Limits are applied per API key, per endpoint, on a rolling one-minute window. Exceeding a limit returns 429 rate_limited; retry after a short pause.
| Method | Endpoint | Limit |
|---|---|---|
| GET | /balance | 120 / min |
| GET | /catalog | 60 / min |
| POST | /esims | 60 / min |
| GET | /esims/:id | 120 / min |
| GET | /esims | 120 / min |
| GET | /orders | 120 / min |
Errors
Errors use standard HTTP status codes and a JSON envelope. Branch on error.code (stable) rather than the human-readable error.message.
{
"error": {
"code": "insufficient_credit",
"message": "Insufficient wholesale credit. Top up your balance and retry."
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed. |
| 400 | invalid_reference | reference contains characters outside A–Z a–z 0–9 . _ : -. |
| 401 | missing_api_key | No Authorization: Bearer header was sent. |
| 401 | invalid_api_key | The key is unknown, revoked, or inactive. |
| 402 | insufficient_credit | Your prepaid balance can't cover the order. Top up and retry. |
| 404 | plan_not_found | No plan matches the supplied planId. |
| 404 | esim_not_found | No eSIM with that id/ICCID exists under your account. |
| 422 | plan_unavailable | The plan exists but isn't available for wholesale. |
| 429 | rate_limited | You exceeded the per-endpoint rate limit. |
| 502 | provisioning_failed | Upstream provisioning failed — you were not charged. |
Get balance
Returns your current prepaid wholesale credit.
{
"balance": 250.00,
"balanceCents": 25000,
"currency": "USD"
}List catalog
Lists every purchasable plan with your wholesale price (your cost, in USD). Use planId when ordering.
{
"plans": [
{
"planId": "jp",
"country": "Japan",
"region": null,
"data": "10 GB",
"days": 15,
"unlimited": false,
"price": 16.50,
"currency": "USD"
}
]
}Order eSIMs
Charges your balance and provisions one or more eSIMs. Returns 201 for a new order, or 200 when an idempotent reference replays an existing one.
Body parameters
| Field | Type | Description | |
|---|---|---|---|
| planId | string | required | A planId from the catalog. |
| quantity | integer | optional | How many eSIMs to provision. Defaults to 1; max 50. |
| reference | string | optional | Your idempotency key. May contain A–Z a–z 0–9 . _ : -. Reusing it returns the original order without charging again. |
curl -X POST https://journeyesims.com/api/v1/esims \
-H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d" \
-H "Content-Type: application/json" \
-d '{ "planId": "jp", "quantity": 1, "reference": "order-1234" }'const res = await fetch("https://journeyesims.com/api/v1/esims", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.JOURNEY_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ planId: "jp", quantity: 1, reference: "order-1234" })
});
if (!res.ok) throw new Error("Order failed: " + res.status);
const order = await res.json();
console.log(order.esims[0].activation.lpa);import os, requests
res = requests.post(
"https://journeyesims.com/api/v1/esims",
headers={"Authorization": "Bearer " + os.environ["JOURNEY_API_KEY"]},
json={"planId": "jp", "quantity": 1, "reference": "order-1234"},
timeout=30,
)
res.raise_for_status()
order = res.json()
print(order["esims"][0]["activation"]["lpa"]){
"orderId": "ord_9f2a7c1d",
"status": "provisioned",
"planId": "jp",
"quantity": 1,
"reference": "order-1234",
"unitPrice": 16.50,
"totalCharged": 16.50,
"currency": "USD",
"createdAt": 1717000000000,
"esims": [
{
"id": "jp-w-9f2a7c1d",
"orderId": "ord_9f2a7c1d",
"reference": "order-1234",
"planId": "jp",
"country": "Japan",
"data": "10 GB",
"days": 15,
"status": "ready",
"iccid": "8944000000000000000",
"activation": {
"lpa": "LPA:1$smdp.journey.example$ABC-123-XYZ",
"activationCode": "LPA:1$smdp.journey.example$ABC-123-XYZ",
"qrCodeImage": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"smdpAddress": "smdp.journey.example",
"matchingId": "ABC-123-XYZ"
},
"usage": {
"totalMB": 10240,
"usedMB": 0,
"remainingMB": 10240,
"remainingDays": 15,
"unlimited": false,
"updatedAt": null
},
"activatedAt": null,
"createdAt": 1717000000000
}
]
}quantity and only some lines provision, you're charged only for the successful ones and refunded the rest — the response's quantity and totalCharged reflect what actually succeeded.Retrieve an eSIM
Fetches one eSIM by its Journey id or its ICCID. Live usage and activation state are refreshed from the network on each call, so this is the endpoint to poll for data consumption.
curl https://journeyesims.com/api/v1/esims/8944000000000000000 \
-H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d"{
"esim": {
"id": "jp-w-9f2a7c1d",
"status": "active",
"iccid": "8944000000000000000",
"activation": {
"lpa": "LPA:1$smdp.journey.example$ABC-123-XYZ",
"qrCodeImage": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"smdpAddress": "smdp.journey.example",
"matchingId": "ABC-123-XYZ"
},
"usage": {
"totalMB": 10240,
"usedMB": 512,
"remainingMB": 9728,
"remainingDays": 12,
"unlimited": false,
"updatedAt": 1717003600000
},
"activatedAt": 1717001000000
}
}List eSIMs
Returns every eSIM provisioned under your account, newest first, each in the same shape as the eSIM object.
curl https://journeyesims.com/api/v1/esims \
-H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d"List orders
Returns your most recent orders (up to 100), newest first.
{
"orders": [
{
"orderId": "ord_9f2a7c1d",
"status": "provisioned",
"planId": "jp",
"quantity": 1,
"reference": "order-1234",
"totalCharged": 16.50,
"currency": "USD",
"createdAt": 1717000000000
}
]
}The eSIM object
Every eSIM endpoint returns objects in this shape. Fields under activation are everything a device needs to install the profile.
| Field | Type | Description | |
|---|---|---|---|
| id | string | required | Journey identifier for the eSIM (use this or the ICCID to fetch it). |
| status | string | required | One of ready, active, provisioning. |
| iccid | string | optional | The SIM's ICCID once provisioned. |
| activation | object | required | Installation material: lpa, activationCode, qrCodeImage (data URI), smdpAddress, matchingId. |
| usage | object | required | Live usage: totalMB, usedMB, remainingMB, remainingDays, unlimited, updatedAt. |
| activatedAt | integer | optional | Epoch ms of first activation, or null. |
Idempotency
Pass a unique reference on POST /esims to make ordering safe to retry. If a request times out or you're unsure it landed, repeat it with the same reference: the API returns the original order (HTTP 200) instead of creating — and charging for — a duplicate.
Webhooks
Set an HTTPS webhook URL in your dashboard to receive eSIM status and usage updates as they change, so you don't have to poll retrieve. Endpoints should respond 2xx quickly and process asynchronously.