API documentationGet API keys

Sell eSIMs from your own system

Issue eSIMs, top them up and follow their status over HTTPS. Every purchase uses the prepaid balance of your account, at your wholesale price.

Getting started

Create a key in your partner panel under API. We show it once, so store it on your server and never put it in a browser or an app. Send it with every request.

Your first request
curl https://panel.esimdesk.app/api/v1/balance \
  -H "Authorization: Bearer esk_live_..."

The base URL is https://panel.esimdesk.app/api/v1. Requests and responses use JSON. All prices are in USD. The limit is 120 requests per minute for each account.

Balance

GET /balance

Returns your current prepaid balance.

Response
{ "balance_usd": 100 }

Packages

GET /packages?location=IS

Lists what you can sell. Leave out location to get every package, or pass a country or region code.

Response
{
  "packages": [{
    "code": "IS_1_7",
    "name": "Iceland 1GB 7Days",
    "location": "IS",
    "countries": ["IS"],
    "data_bytes": 1073741824,
    "validity_days": 7,
    "wholesale_price_usd": 2.15,
    "retail_price_usd": 2.9
  }]
}

You pay wholesale_price_usd. retail_price_usd is the price from your margin settings and is only a suggestion, because you charge your customers yourself.

Orders

POST /orders

Buys an eSIM and deducts the wholesale price from your balance.

FieldRequiredDescription
package_codeYesCode from the package list.
external_idYesYour unique reference, up to 100 characters. Repeating it returns the same order instead of buying a second eSIM, so retries are safe.
customer_emailNoThe address of your customer.
languageNoLanguage of the customer page: en, pl, de, es, fr, it or pt. Your store default applies if you leave it out.
Request
curl -X POST https://panel.esimdesk.app/api/v1/orders \
  -H "Authorization: Bearer esk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"package_code":"IS_1_7","external_id":"order-1001","customer_email":"client@example.com"}'
Response
{
  "id": "32dfb067-0ea4-4d41-827c-cd633a75f533",
  "external_id": "order-1001",
  "status": "ready",
  "package_code": "IS_1_7",
  "package_name": "Iceland 1GB 7Days",
  "customer_email": "client@example.com",
  "wholesale_price_usd": 2.15,
  "created_at": "2026-11-03T08:30:27.294Z",
  "esim": {
    "state": "new",
    "iccid": "8985...",
    "activation_code": "LPA:1$...",
    "page_url": "https://your-store.example/order/...",
    "data_used_bytes": 0,
    "data_total_bytes": 1073741824,
    "expires_at": "2027-05-02T08:30:27.000Z"
  }
}

Order status is processing, ready, failed or cancelled. esim stays null until the order is ready. A failed order returns the wholesale price to your balance.

Give your customer page_url, or use activation_code in your own installation screen.

GET /orders/{id}

Returns one order, with fresh status and data usage.

GET /orders

Returns your 100 latest orders as { "orders": [...] }.

eSIM states

esim.stateMeaning
provisioningThe eSIM is being prepared.
newIssued and not installed. It can still be cancelled.
onboardInstalled on a device.
in_useUsing data.
used_upAll data is used.
suspendedTemporarily suspended.
expiredValidity has ended.
cancelledCancelled.
deletedRemoved from the device.
unknownThe state is not available right now. Check again later.

Cancel an eSIM

POST /orders/{id}/cancel

Works only before installation and only if the eSIM has not been topped up. A successful cancellation returns the wholesale price of the order to your balance. You refund your customer yourself. If the eSIM can no longer be cancelled, you get 409 cannot_cancel and your balance stays the same.

Top up an eSIM

Adds data to an eSIM you already issued. Not every package can be topped up, and the available top-ups depend on the eSIM. An eSIM that has been topped up can no longer be cancelled.

GET /orders/{id}/topups

Lists the top-ups for this eSIM and the ones already made.

Response
{
  "packages": [{
    "code": "TOPUP_1",
    "name": "Top-up 1GB 7Days",
    "data_bytes": 1073741824,
    "validity_days": 7,
    "wholesale_price_usd": 1.38,
    "retail_price_usd": 1.86
  }],
  "topups": []
}

POST /orders/{id}/topups

FieldRequiredDescription
package_codeYesCode from the top-up list of this eSIM.
external_idYesYour unique reference. Repeating it returns the same top-up with "duplicate": true and charges nothing.
Request
curl -X POST https://panel.esimdesk.app/api/v1/orders/ORDER_ID/topups \
  -H "Authorization: Bearer esk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"package_code":"TOPUP_1","external_id":"topup-1001"}'
Response
{
  "id": "e4962d06-e3d2-4d21-bd49-84c3170f4720",
  "external_id": "topup-1001",
  "order_id": "32dfb067-0ea4-4d41-827c-cd633a75f533",
  "status": "completed",
  "package_code": "TOPUP_1",
  "package_name": "Top-up 1GB 7Days",
  "wholesale_price_usd": 1.38,
  "created_at": "2026-11-03T08:30:27.294Z"
}

Top-up status is processing, completed or failed. A failed top-up returns 422 and the money goes back to your balance. If it stays processing, wait for the webhook or check the list again. We keep retrying it in the background.

Webhooks

Add a public HTTPS address in your partner panel under API. We send a POST request for each event and show the secret for signatures once.

EventSent when
order.readyThe eSIM has been issued.
order.failedThe order failed and the money went back to your balance.
order.cancelledThe eSIM was cancelled.
esim.updatedThe eSIM state changed, for example from new to onboard.
esim.topped_upA top-up was completed.
esim.topup_failedA top-up failed and the money went back to your balance.
testYou sent a test event from the panel.
Request body
{
  "id": "1f0c7a52-6f5d-4a7e-9a55-1f5b8d2a9c10",
  "type": "order.ready",
  "created_at": "2026-11-03T08:30:27.294Z",
  "data": {
    "order_id": "32dfb067-0ea4-4d41-827c-cd633a75f533",
    "external_id": "order-1001",
    "state": "new"
  }
}

Reply with any 2xx status within 8 seconds. We retry failed deliveries with growing delays for about two days. The same event can arrive twice and events can arrive out of order, so ignore an id you have already handled and read GET /orders/{id} for the current status. Status changes of installed eSIMs can take up to about an hour to reach you.

Verify the signature

Sign timestamp.raw_body with HMAC-SHA256 and your secret, then compare the result with the header. Use the raw body, before any JSON parsing, and reject old timestamps.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'

function isValid(rawBody, headers, secret) {
  const ts = headers['x-esimdesk-timestamp']
  const got = Buffer.from(headers['x-esimdesk-signature'] || '')
  const want = Buffer.from('sha256=' + createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex'))
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300
  return fresh && got.length === want.length && timingSafeEqual(got, want)
}

Errors

Errors use the HTTP status and this body:

Response
{ "error": { "code": "insufficient_balance", "message": "Your balance does not cover this package" } }
CodeStatusWhat to do
invalid_request400Fix the fields in the request.
unauthorized401Check the API key.
insufficient_balance402Add funds to your balance.
account_suspended403Contact us about your account.
not_found404Check the order ID.
package_not_found404Get the current package list.
not_available404This top-up is not offered for this eSIM. Get the current top-up list.
cannot_cancel409The eSIM can no longer be cancelled.
not_eligible409This eSIM cannot be topped up.
rate_limited429Slow down and retry after a minute.
daily_limit429Your daily order limit is reached. Try tomorrow or contact us.
provider_unavailable502Retry with the same external_id.