Skip to content

Buy eSIMs

POST
/esims/purchase
curl --request POST \
--url 'https://api.roamex.com/v1/esims/purchase?lang=nl' \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-2026-10-22-0001' \
--data '{ "planId": "00000000-0000-4000-8000-0000000000b1", "quantity": 2, "costCentre": "NL-SALES-2026" }'

Buys one or more eSIMs of a plan and pays from your balance. Send an Idempotency-Key header so that a retry returns the first order instead of buying again. If fewer eSIMs are delivered than requested, partial states how many and what was refunded.

lang
string
default: en

Language of plan and destination names in the response: en, nl, es, de, it, fr, pl, pt, tr, el, cs or ro. Any other value, or none, gives English. It never changes which items are returned or any figure.

Example

nl
Idempotency-Key
string

Makes retries safe. The key is combined with your account, the plan, quantity, days and costCentre: repeating all of them answers with the original order and idempotent_replay: true, and charges nothing. Changing the plan, the quantity, the cost centre or, for a day pass, the days makes it a new order, charged again. days is ignored for other plans, and quantity and days are compared after they are rounded and capped. Without this header, identical requests in the same clock minute count as one order, and an identical order that is still being provisioned is answered with 409 ORDER_IN_PROGRESS.

Example

order-2026-10-22-0001
Media typeapplication/json
object
planId
required

A plan id from /plans.

string format: uuid
quantity

Number of eSIMs. Rounded to a whole number and kept between 1 and 50. A missing or non-numeric value means 1.

integer
default: 1 >= 1 <= 50
days

Day passes only: the number of days. Values above 60 are lowered to 60. A missing value, or one below 1, uses the plan’s own duration. Ignored for other plans.

integer
>= 1 <= 60
costCentre

Your own accounting label for this order, returned on its eSIMs by /esims/list and /esims/details. Trimmed first; blank means none. At most 64 characters and no control characters, or 400 INVALID_COST_CENTRE.

string | null
<= 64 characters

Example

{
"planId": "00000000-0000-4000-8000-0000000000b1",
"quantity": 2,
"costCentre": "NL-SALES-2026"
}

The order. On a replay of an earlier request, idempotent_replay is true, newBalance is absent, and cost is what that order was charged.

Media typeapplication/json
object
success
required
boolean
esims
required

The eSIMs of this order.

Array<object>
object
id
required

Use as esimId on the other eSIM endpoints.

string format: uuid
status
required

ready once the install data is available, otherwise pending.

string
Allowed values: ready pending
order
required
object
id
required
string format: uuid
quantity
required

ESIMs delivered.

integer
requestedQuantity
required

ESIMs requested, after the 1 to 50 limits are applied.

integer
plan
required
object
id
required
string format: uuid
name
required

The plan’s name. For a day pass it includes the days bought.

string
cost

What your balance was charged for the order, before any refund. Absent on a replay whose order has no recorded charge.

object
amount
required

Euro cents.

integer
currency
required
string
Allowed values: EUR
formatted
required
string
partial

Present when fewer eSIMs were delivered than requested, or part of the order was refunded.

object
requested
required
integer
delivered
required
integer
unrecorded

ESIMs that were provisioned and charged but are missing from esims. Roamex recovers them by hand. Present only when greater than 0, and never on a replay.

integer
refunded
required

The amount returned to your balance.

object
amount
required

Euro cents.

integer
currency
required
string
Allowed values: EUR
formatted
required
string
refundPending
required

true when a refund is owed and has not been made yet.

boolean
net

cost minus refunded.

object
amount
required

Euro cents.

integer
currency
required
string
Allowed values: EUR
formatted
required
string
newBalance

Your balance after the order. Absent on a replay.

object
amount
required

Euro cents.

integer
currency
required
string
Allowed values: EUR
formatted
required
string
idempotent_replay

Present only when this answers a repeat of an earlier request.

boolean

Example

{
"success": true,
"esims": [
{
"id": "00000000-0000-4000-8000-0000000000c1",
"status": "ready"
},
{
"id": "00000000-0000-4000-8000-0000000000c2",
"status": "ready"
}
],
"order": {
"id": "00000000-0000-4000-8000-0000000000d1",
"quantity": 2,
"requestedQuantity": 2,
"plan": {
"id": "00000000-0000-4000-8000-0000000000b1",
"name": "Japan · 1 GB · 7 days"
}
},
"cost": {
"amount": 900,
"currency": "EUR",
"formatted": "€9.00"
},
"newBalance": {
"amount": 24100,
"currency": "EUR",
"formatted": "€241.00"
}
}
X-RateLimit-Limit
integer

Calls allowed per minute for this endpoint group.

X-RateLimit-Remaining
integer

Calls left in the current minute.

X-RateLimit-Reset
integer

When the current window ends, in Unix seconds.

  • INVALID_JSON: the body is missing or not a JSON object.
  • MISSING_PLAN_ID: planId is missing.
  • INVALID_COST_CENTRE: costCentre is not a string, is longer than 64 characters, or contains a control character.
  • PLAN_NOT_PRICED: the plan has no price.
  • PLAN_NOT_AVAILABLE: the plan is not sold.
  • INSUFFICIENT_BALANCE: your balance does not cover the order. Nothing was charged.
  • ORDER_FAILED: no eSIM could be ordered, and the order is refunded. If that refund fails, Roamex is alerted to make it by hand.
Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}

AUTH_ERROR: the key is missing, malformed, unknown or revoked. The message says which, for example Missing Authorization header, Invalid Authorization header format. Use: Bearer <api_key>, Invalid API key format or Invalid API key.

Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false,
"error": {
"code": "AUTH_ERROR",
"message": "Invalid API key"
}
}
  • DESTINATION_NOT_ALLOWED: the plan’s destination is not on your account’s list of allowed destinations.
  • SPEND_CAP_EXCEEDED: the order would take you over your account’s monthly spend limit.
  • TRIAL_RESTRICTED: a trial account asked for more than its trial allows.
  • AUTH_ERROR: the account is suspended or not approved.
Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}
  • PLAN_NOT_FOUND: no plan has this id, or the plan is no longer active.
  • NOT_FOUND: the API is switched off on the platform.
Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}

Do not retry these with a new Idempotency-Key unless you mean to buy again.

  • ORDER_IN_PROGRESS: an identical order is still being provisioned. When it completes, its order.created event arrives by webhook and in /events/recent, and its eSIMs appear in /esims/list.
  • ORDER_TERMINAL: the earlier identical request ended without an eSIM. The message says whether it was refunded.
Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}

PAYLOAD_TOO_LARGE: the Content-Length header is over 100 KB.

Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}

RATE_LIMITED: the account’s per-minute budget for this endpoint group is used up, or too many refused keys came from this IP address. Wait the Retry-After seconds.

Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Please wait 42 seconds before retrying."
}
}
X-RateLimit-Limit
integer

Calls allowed per minute for this endpoint group.

X-RateLimit-Remaining
integer

Calls left in the current minute.

X-RateLimit-Reset
integer

When the current window ends, in Unix seconds.

Retry-After
integer

Seconds to wait before retrying.

  • PURCHASE_FAILED: the order could not be created. Nothing was charged.
  • INTERNAL_ERROR, or AUTH_ERROR with status 500: see the shared errors.
Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}

Transient. This request charged nothing.

  • PURCHASES_PAUSED: purchases are paused. Retry after the Retry-After seconds.
  • PRICING_UNAVAILABLE: your account pricing could not be confirmed.
  • TRIAL_CHECK_UNAVAILABLE: a trial account’s allowance could not be checked.
  • REPLAY_UNAVAILABLE: an earlier request created this order but its eSIMs could not be read. Retry the identical request with the same Idempotency-Key.
Media typeapplication/json
object
success
required
boolean
error
required
object
code
required

Machine-readable. Branch on this.

string
message
required

A sentence for people. It can change.

string

Example

{
"success": false
}
Retry-After
integer

Seconds to wait. Sent with PURCHASES_PAUSED (300).