Developers
Four endpoints, two webhooks, no surprises.
Everything the store does, your product can do. The whole surface fits on one page.
Why this API exists
Provisioning connectivity should happen at the moment a traveller books, inside your own product. It should not be something they discover at an airport.
A booking platform can attach data to an itinerary. A device maker can activate a plan during onboarding. A team tool can hand a new starter their data alongside their laptop.
All three of those are four calls and one webhook. There is no partner portal to log into and no CSV to upload.
Authentication
Send your key as a bearer token on every request. Keys are prefixed sk_test for sandbox and sk_live for production.
A key pasted into the wrong environment fails with a 401 rather than quietly spending money. Keys are scoped, so a read only key cannot place an order and a key issued for one team account cannot see another.
Rotate from the console at any time. The old key keeps working for 24 hours, so you are never forced into a flag day.
curl https://api.orbislo.com/v1/catalog?country=jp \
-H "Authorization: Bearer sk_live_9f2c..." \
-H "Orbislo-Version: 2026-08-01"About the version header
The version header is optional and pins the response shape to a dated release. Without it you get the version your key was created against, which never changes underneath you.
We add fields without warning. We never remove a field or change its type inside a version.
4
endpoints in the whole API, plus 2 webhook events
90s
median time from a successful order to an activated profile
24h
window in which a repeated idempotency key returns the original response
The endpoints
| Method | Path | What it does | Rate limit |
|---|---|---|---|
| GET | /v1/catalog | Every plan we sell, with price, data, validity and the countries it covers. | 600 a minute |
| POST | /v1/orders | Buys a plan and returns an order with a provisioning job attached. | 60 a minute |
| GET | /v1/provisioning/:id | The state of one provisioning job, from queued through to activated or failed. | 600 a minute |
| GET | /v1/usage/:esim_id | Bytes used, bytes remaining, and the carrier one eSIM is attached to. | 300 a minute |
Limits are per key in a sliding window. Every response carries the remaining count and the reset time, and a 429 carries a retry delay in seconds. If your use case genuinely needs a bigger bucket, ask, because raising one is a configuration change and not a negotiation.
Catalog
The catalog is the source of truth for what exists and what it costs. It covers the 145 destinations live today.
Filter by country, by region or by plan family. Prices are returned in minor units, so floating point never reaches your billing code.
GET /v1/catalog?country=jp
{
"object": "list",
"data": [
{
"id": "plan_jp_5gb",
"name": "Japan 5 GB",
"countries": ["jp"],
"data_mb": 5120,
"price": { "amount": 1150, "currency": "usd" },
"expires": false,
"throttle_mbps": null,
"tethering": true,
"carriers": ["NTT Docomo", "KDDI", "SoftBank"]
}
],
"has_more": false
}Two fields worth reading twice
The expires field is false on every metered plan we sell, because our data does not expire. If you are building a price comparison, that field changes the arithmetic more than the price does.
The throttle_mbps field is null on metered plans and 1 on unlimited day passes, where full speed runs to 2 GB a day and then drops to 1 Mbps.
We put the throttle number in the API for the same reason we print it on the buy button. A number a traveller finds out later is a support ticket.
Orders
One POST buys a plan and starts provisioning. The Idempotency-Key header is required rather than advisory.
The worst outcome in this API is a timeout that leaves you unsure whether a traveller was charged. A required key removes that state entirely.
POST /v1/orders
Idempotency-Key: 4f1d0f6e-1c3a-4a2b-9d77-1b6a0e7c9f21
Content-Type: application/json
{
"plan_id": "plan_jp_5gb",
"traveller_ref": "user_88213",
"imei": "356938035643809",
"activate": "on_first_use"
}
201 Created
{
"id": "ord_7Kd2mQ",
"status": "provisioning",
"esim_id": "esim_2xB9Ln",
"provisioning_id": "prv_5Ttq81",
"amount": { "amount": 1150, "currency": "usd" },
"activation": {
"type": "universal_link",
"url": "https://orbislo.com/i/2xB9Ln",
"lpa": "LPA:1$rsp.orbislo.com$K4-9TT-2XB9LN"
}
}Device checks and delayed activation
The imei field is optional but strongly recommended. Send it and we check eSIM eligibility and carrier lock before taking money.
A phone that cannot hold a profile gets a 422 with device_not_eligible instead of a sold plan and an angry traveller.
Setting activate to on_first_use means any validity window starts when the traveller lands, not when your server called us.
Provisioning status
Provisioning is asynchronous, because a carrier platform is on the other end of it. Poll this endpoint, or take the webhook and skip the polling.
Median time from queued to activated is under 90 seconds. Anything still queued after 10 minutes is a failure, and it refunds itself.
GET /v1/provisioning/prv_5Ttq81
{
"id": "prv_5Ttq81",
"status": "activated",
"states": [
{ "state": "queued", "at": "2026-08-24T09:14:02Z" },
{ "state": "provisioning", "at": "2026-08-24T09:14:04Z" },
{ "state": "installed", "at": "2026-08-24T09:14:41Z" },
{ "state": "activated", "at": "2026-08-24T09:15:07Z" }
],
"carrier": "KDDI",
"failure_reason": null
}Webhooks
Two events, both of which change what a traveller should be told. Point them at any HTTPS endpoint, set separately per environment.
Webhook events
| Event | Fires when | What to do with it |
|---|---|---|
| esim.activated | The profile attaches to a network for the first time. | Tell the traveller they are online. This is the moment the purchase became real to them. |
| esim.depleted | The plan crosses a usage threshold, at 80 percent and again at 100 percent. | Offer a top up before they are stranded rather than after. |
POST https://your-app.example/hooks/orbislo
Orbislo-Signature: t=1756032907,v1=6c1b...
Content-Type: application/json
{
"id": "evt_9pQ4rz",
"type": "esim.depleted",
"created": "2026-08-24T11:41:33Z",
"data": {
"esim_id": "esim_2xB9Ln",
"threshold": 80,
"used_mb": 4096,
"remaining_mb": 1024,
"country": "jp"
}
}Verifying a delivery
Compute HMAC SHA-256 over the timestamp, a full stop, and the raw request body, using your endpoint secret. Compare in constant time.
Reject anything with a timestamp older than 5 minutes, which stops a replay. Deliveries are at least once and retried 8 times over 24 hours, so key your handler on the event id.
Error codes
| HTTP | Code | What it means | What to do |
|---|---|---|---|
| 400 | invalid_request | A field is missing or has the wrong type. The body names the field. | Fix the request. Retrying will not help. |
| 401 | invalid_token | The bearer token is wrong, revoked, or from the other environment. | Check you are not sending a sandbox key to the live host. |
| 402 | insufficient_balance | Your account balance will not cover the order. | Top up, then retry with the same idempotency key. |
| 404 | not_found | There is no object with that id in this environment. | Sandbox ids and live ids are not interchangeable. |
| 409 | idempotency_conflict | The same idempotency key was reused with a different body. | Use a new key, or resend the original body byte for byte. |
| 422 | device_not_eligible | The device is not eSIM capable, or it is carrier locked. | Run the device check before you take the money. |
| 429 | rate_limited | You went past the bucket for that endpoint. | Back off using the retry delay in the header. Do not spin. |
| 500 | internal_error | Ours. It is already in our alerting. | Retry with the same idempotency key after 2 seconds. |
| 503 | provider_unavailable | An upstream carrier platform is down. | Retry for up to 10 minutes. We fail over automatically where a second carrier exists. |
Every error body carries a code, a human readable message, and a request id. Quote the request id to support and you skip the first four questions.
Sandbox against live
Same host, same paths, same response shapes. A key prefixed sk_test never touches money and never creates a carrier profile.
Provisioning jobs move through the full state machine in about 4 seconds, so your tests do not sleep for 90 seconds each.
Order the plan id plan_test_fail to get a failed job with a reason. Order plan_test_slow to get one that sits in provisioning for 11 minutes, so you can exercise your timeout path.
Webhook deliveries fire in sandbox too, against a URL you set per environment.
If you would rather not build anything
The affiliate track pays on referred orders with no integration at all, which is the right answer for most content sites.
The MCP endpoint exposes our coverage dataset and price index to an AI assistant with one line of configuration and no code.
The plans you would be selling, the measured speed data behind the catalog and the full country list are all published openly on this site.
Questions developers ask
Can I get an API key today?
How long does approval take?
What is the difference between sandbox and live?
Do I need idempotency keys?
What are the rate limits?
How reliable are webhooks?
Is there an SDK?
What happens when provisioning fails?
Start with the sandbox, or skip the code entirely
Sandbox credentials arrive the same working day. If you would rather not write an integration, the affiliate track and the MCP endpoint both need nothing built.