Roamledge API reference
v2026-09-16REST · JSON · amounts in US cents
One REST API for travel eSIMs in 190+ countries. Read the catalog at your wholesale price, order a plan, and hand your customer a QR code — all charged to your prepaid Roamledge balance.
Base URL https://roamledge.com/api/v1. You need an approved reseller account and an API key: apply here. Pricing and tiers are on the developers page.
Authentication
Every request carries an API key as a bearer token. Create keys in your developer dashboard; the secret is shown once, at creation, and never again.
curl https://roamledge.com/api/v1/ping \
-H "Authorization: Bearer rl_live_a1b2c3_xxxxxxxxxxxxxxxxxxxxxxxx"X-Api-Key: <key> is accepted as an alternative if a bearer header is awkward in your stack. Keys are server-side credentials — never ship one in a browser, a mobile app or a public repository. A revoked key stops working on the next request.
Live and test keys behave identically and read the same catalog. A test key still spends real balance, so use a cheap plan while you integrate.
Requests & responses
Base URL: https://roamledge.com/api/v1. Request bodies are JSON. Responses are JSON with Content-Type: application/json and always include a request_id — quote it if you need our help with a specific call.
| Field | Type | Description |
|---|---|---|
| *_cents | integer | Every amount is an integer in US cents. 1299 is $12.99. |
| *_at | string | ISO 8601 UTC timestamps, e.g. 2026-09-16T14:05:00.000Z. |
| *_bytes | integer | Data volumes in bytes. 3221225472 is 3 GB. |
| request_id | string | Unique per call. Included on success and on failure. |
The response header X-Api-Version carries the API version this deployment speaks. We add fields without warning — ignore what you do not recognise — and will never remove or repurpose one without a new version and an email to every active reseller.
Errors
Failures use standard status codes and a single error shape. The code is stable and safe to branch on; the message is written for a human and may change.
{
"error": {
"type": "balance_error",
"code": "insufficient_balance",
"message": "Your balance does not cover this order. Top up at /account/balance.",
"required_cents": 1039,
"balance_cents": 420
},
"request_id": "req_7f2c91a4e1b84c0f9d3a6b25"
}| Field | Type | Description |
|---|---|---|
| unauthorized | 401 | Missing, unknown or revoked API key. |
| forbidden | 403 | Key is valid but the account is not approved, or is suspended. |
| invalid_request | 400 | A parameter is missing or malformed. Check `param`. |
| insufficient_balance | 402 | Top up, then retry. Carries `required_cents` and `balance_cents`. |
| not_found | 404 | No such order, plan or destination on this account. |
| plan_unavailable | 409 | The plan slug is no longer buyable — re-read /v1/plans. |
| idempotency_conflict | 409 | That Idempotency-Key was used with a different body. |
| rate_limited | 429 | Slow down. Carries `retry_after` and a Retry-After header. |
| server_error | 500 | Our fault. Retry with the same Idempotency-Key. |
Rate limits
Limits are per API key, per minute:
| Field | Type | Description |
|---|---|---|
| Reads | 120 / min | /v1/ping, /v1/account, /v1/destinations, /v1/plans, order reads. |
| Order creation | 30 / min | POST /v1/orders. |
Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. A 429 carries Retry-After in seconds — wait that long rather than retrying immediately. The catalog changes slowly, so cache /v1/destinations and /v1/plans for a few minutes rather than calling them per page view. Need more headroom for a launch? Ask us.
Idempotency
Send Idempotency-Key on every POST /v1/orders. If a response never reaches you, retry with the same key: we return the original order instead of buying a second eSIM. Use something from your own system — an order id or a booking reference — not a random value per attempt.
curl -X POST https://roamledge.com/api/v1/orders \
-H "Authorization: Bearer $ROAMLEDGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-1042" \
-d '{"plan":"JP_3_30","external_ref":"booking-1042"}'A replayed response carries Idempotent-Replay: true. Re-using a key with a different body is rejected with idempotency_conflict — that is a bug on your side, not a duplicate. Keys are remembered for 30 days. Failed orders are not stored against the key, so topping up and retrying the same key works.
Check a key
GET/v1/ping
The cheapest possible call. Use it in your deploy checks to confirm a key still works.
{
"object": "ping",
"ok": true,
"api_version": "2026-09-16",
"livemode": true,
"key_prefix": "rl_live_a1b2c3",
"time": "2026-09-16T14:05:00.000Z",
"request_id": "req_…"
}Your account
GET/v1/account
Your balance, your tier and how far you are from the next one. Poll it to warn yourself before you run dry.
{
"object": "account",
"company_name": "Wanderbox Travel",
"status": "approved",
"balance_cents": 48250,
"currency": "USD",
"tier": { "id": "gold", "name": "Gold", "discount_percent": 20 },
"volume_30d_cents": 312400,
"next_tier": {
"id": "platinum",
"name": "Platinum",
"discount_percent": 25,
"remaining_cents": 687600
},
"request_id": "req_…"
}Destinations
GET/v1/destinations
Every country, region and global plan area we sell.
| Field | Type | Description |
|---|---|---|
| type | query | country, region or global. Optional. |
| search | query | Matches the name, or exactly matches the ISO code. Optional. |
curl "https://roamledge.com/api/v1/destinations?type=country&search=japan" \
-H "Authorization: Bearer $ROAMLEDGE_API_KEY"{
"object": "list",
"data": [
{
"object": "destination",
"slug": "japan",
"name": "Japan",
"code": "JP",
"type": "country",
"countries": 1,
"plans": 12,
"has_5g": true,
"has_day_pass": true,
"retail_from_cents": 549,
"your_from_cents": 439
}
],
"has_more": false,
"request_id": "req_…"
}your_from_cents is indicative. The exact price you pay for a specific plan always comes from /v1/plans, because our margin floor can lift an individual plan above the flat tier discount.
Plans
GET/v1/plans?destination=<slug>
Buyable plans for one destination, priced for you. destination is required — the whole catalog is thousands of rows and nobody wants it in a single response.
curl "https://roamledge.com/api/v1/plans?destination=japan" \
-H "Authorization: Bearer $ROAMLEDGE_API_KEY"{
"object": "list",
"destination": { "slug": "japan", "name": "Japan", "code": "JP", "type": "country" },
"discount_percent": 20,
"data": [
{
"object": "plan",
"slug": "JP_3_30",
"name": "Japan 3GB 30Days",
"data_bytes": 3221225472,
"data_gb": 3,
"days": 30,
"is_day_pass": false,
"price_cents": 1039,
"retail_price_cents": 1299,
"network": "5G",
"top_up_supported": true
}
],
"has_more": false,
"request_id": "req_…"
}price_cents is what we charge you. For a day pass (is_day_pass: true) it is the price per day, and the order total depends on how many days you buy — our day-pass curve makes longer trips cheaper per day. Plan slugs are stable, but a plan can be withdrawn by the network, so handle plan_unavailable by re-reading this endpoint.
Create an order
POST/v1/orders
Buys a plan and charges your balance immediately. Returns 201 with the order, which starts at paid and moves to completed once the eSIM is provisioned.
| Field | Type | Description |
|---|---|---|
| plan | string, required | A slug from /v1/plans, e.g. JP_3_30. |
| days | integer | Required for day-pass plans. 1–30. |
| external_ref | string | Your own reference. Echoed on the order and in every webhook. Max 120 chars. |
curl -X POST https://roamledge.com/api/v1/orders \
-H "Authorization: Bearer $ROAMLEDGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-1042" \
-d '{
"plan": "JP_3_30",
"external_ref": "booking-1042"
}'{
"object": "order_response",
"order": {
"object": "order",
"number": "RL-K7M2QP",
"status": "paid",
"currency": "USD",
"total_cents": 1039,
"external_ref": "booking-1042",
"created_at": "2026-09-16T14:05:00.000Z",
"paid_at": "2026-09-16T14:05:01.000Z",
"completed_at": null,
"failure_reason": null,
"items": [
{
"plan": "JP_3_30",
"destination": { "slug": "japan", "name": "Japan", "code": "JP", "type": "country" },
"data_bytes": 3221225472,
"days": 30,
"is_day_pass": false,
"price_cents": 1039,
"esims": []
}
]
},
"request_id": "req_…"
}esims is empty until provisioning finishes — usually seconds. Either poll the order or, better, configure a webhook and wait for order.completed.
Read orders
GET/v1/orders/{number}
One order, including the eSIM install details once they exist.
{
"object": "order_response",
"order": {
"number": "RL-K7M2QP",
"status": "completed",
"items": [
{
"plan": "JP_3_30",
"esims": [
{
"reference": "1234567890ABCDEF",
"iccid": "8910300000012345678",
"status": "released",
"activation_code": "LPA:1$rsp.truphone.com$QRF-BQTGN-…",
"smdp_address": "rsp.truphone.com",
"qr_code_url": "https://p.qrsim.net/…png",
"data_used_bytes": 0,
"data_total_bytes": 3221225472,
"activated_at": null,
"expires_at": null
}
]
}
]
},
"request_id": "req_…"
}Give your customer either the qr_code_url to scan or the activation_code to paste — both install the same eSIM. An eSIM cannot move to another phone once installed, so deliver it to the right person.
GET/v1/orders
Your API orders, newest first. limit is 1–100 (default 20). Pass the next_cursor from a response as cursor to get the next page; has_more tells you when to stop.
curl "https://roamledge.com/api/v1/orders?limit=50&cursor=RL-K7M2QP" \
-H "Authorization: Bearer $ROAMLEDGE_API_KEY"Statuses: pending, paid, provisioning, completed, failed, refunded, cancelled.
Balance
GET/v1/balance/transactions
Your ledger: top-ups, order payments, refunds and any support adjustment. Amounts are signed — negative is money leaving. Same limit / cursor paging as orders.
{
"object": "list",
"data": [
{
"object": "balance_transaction",
"id": "clx…",
"amount_cents": -1039,
"balance_after_cents": 47211,
"reason": "order_payment",
"description": "Order RL-K7M2QP",
"order_number": "RL-K7M2QP",
"created_at": "2026-09-16T14:05:01.000Z"
}
],
"has_more": true,
"next_cursor": "clx…",
"request_id": "req_…"
}Topping up is done from your account, not through the API — we are not going to let a key move money in. Refunds for API orders return to your balance.
Webhooks
Set an HTTPS endpoint in your dashboard and we post events to it as they happen. Three attempts, then the delivery is marked failed and logged for you to inspect.
| Field | Type | Description |
|---|---|---|
| order.paid | event | The balance has been charged and the order is confirmed. |
| order.completed | event | The eSIM is provisioned — install details are on the order. |
| order.failed | event | Provisioning failed; the order carries a failure_reason. |
| order.refunded | event | The order was refunded and the money returned to your balance. |
| balance.low | event | Your balance fell below the level you can no longer order at. |
POST https://your-endpoint
X-Roamledge-Event: order.paid
X-Roamledge-Signature: t=1789514400,v1=6a3f…
{
"id": "evt_m4k2p9x1",
"type": "order.paid",
"created": 1789514400,
"data": {
"order": {
"number": "RL-K7M2QP",
"status": "paid",
"total_cents": 1039,
"currency": "USD",
"external_ref": "booking-1042",
"paid_at": "2026-09-16T14:05:01.000Z"
}
}
}Verify every event before you act on it. The signature is an HMAC-SHA256 of {timestamp}.{raw body} using your signing secret. Compare in constant time, and reject a timestamp more than five minutes old so a captured delivery cannot be replayed.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
if (!parts.v1 || expected.length !== parts.v1.length) return false;
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) return false;
return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
}Respond 2xx quickly — queue the work rather than doing it inline. Anything else counts as a failure and we retry. Events can arrive more than once, so make your handler idempotent on data.order.number.
Going live
- Store your key in a server-side secret manager. Rotate it if it ever reaches a log or a repository.
- Cache the catalog for a few minutes rather than calling it per page view.
- Always send
Idempotency-Keyon orders, keyed to your own booking or order id. - Handle
insufficient_balanceby alerting a human — orders stop until the balance is topped up. - Verify webhook signatures, and reply 2xx before doing any slow work.
- Keep an eye on your usage page for 4xx spikes — they are almost always a plan slug that has moved or a missing parameter.
Stuck on something? Send us the request_id and we can see exactly what happened.
