Journey API v1 Dashboard

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.

Order an eSIM
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.

Authorization header
curl https://journeyesims.com/api/v1/balance \
  -H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d"
Keep keys server-side. A key can order eSIMs against your prepaid balance. If one leaks, revoke it in the dashboard — revocation is immediate.

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.

Base URL
https://journeyesims.com/api/v1

Rate 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.

MethodEndpointLimit
GET/balance120 / min
GET/catalog60 / min
POST/esims60 / min
GET/esims/:id120 / min
GET/esims120 / min
GET/orders120 / 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 envelope
{
  "error": {
    "code": "insufficient_credit",
    "message": "Insufficient wholesale credit. Top up your balance and retry."
  }
}
StatusCodeMeaning
400invalid_requestA required field is missing or malformed.
400invalid_referencereference contains characters outside A–Z a–z 0–9 . _ : -.
401missing_api_keyNo Authorization: Bearer header was sent.
401invalid_api_keyThe key is unknown, revoked, or inactive.
402insufficient_creditYour prepaid balance can't cover the order. Top up and retry.
404plan_not_foundNo plan matches the supplied planId.
404esim_not_foundNo eSIM with that id/ICCID exists under your account.
422plan_unavailableThe plan exists but isn't available for wholesale.
429rate_limitedYou exceeded the per-endpoint rate limit.
502provisioning_failedUpstream provisioning failed — you were not charged.

Get balance

GET/api/v1/balance120 / min

Returns your current prepaid wholesale credit.

Response
{
  "balance": 250.00,
  "balanceCents": 25000,
  "currency": "USD"
}

List catalog

GET/api/v1/catalog60 / min

Lists every purchasable plan with your wholesale price (your cost, in USD). Use planId when ordering.

Response
{
  "plans": [
    {
      "planId": "jp",
      "country": "Japan",
      "region": null,
      "data": "10 GB",
      "days": 15,
      "unlimited": false,
      "price": 16.50,
      "currency": "USD"
    }
  ]
}

Order eSIMs

POST/api/v1/esims60 / min

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

FieldTypeDescription
planIdstringrequiredA planId from the catalog.
quantityintegeroptionalHow many eSIMs to provision. Defaults to 1; max 50.
referencestringoptionalYour idempotency key. May contain A–Z a–z 0–9 . _ : -. Reusing it returns the original order without charging again.
Request
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"])
201 Created
{
  "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
    }
  ]
}
Partial fulfilment. If you order a 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

GET/api/v1/esims/:id120 / min

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.

Request
curl https://journeyesims.com/api/v1/esims/8944000000000000000 \
  -H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d"
Response
{
  "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

GET/api/v1/esims120 / min

Returns every eSIM provisioned under your account, newest first, each in the same shape as the eSIM object.

Request
curl https://journeyesims.com/api/v1/esims \
  -H "Authorization: Bearer jrn_live_7f3c2a_9d8e1b0a4c6f2e5d"

List orders

GET/api/v1/orders120 / min

Returns your most recent orders (up to 100), newest first.

Response
{
  "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.

FieldTypeDescription
idstringrequiredJourney identifier for the eSIM (use this or the ICCID to fetch it).
statusstringrequiredOne of ready, active, provisioning.
iccidstringoptionalThe SIM's ICCID once provisioned.
activationobjectrequiredInstallation material: lpa, activationCode, qrCodeImage (data URI), smdpAddress, matchingId.
usageobjectrequiredLive usage: totalMB, usedMB, remainingMB, remainingDays, unlimited, updatedAt.
activatedAtintegeroptionalEpoch 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.