eSIMPal API - agent integration skill
Use this skill when implementing or testing an agent that uses the eSIMPal API to buy eSIMs for end-users (list plans, create orders, process payments, activate, and deliver).
Safety and approval rules
- This skill is for integration guidance and controlled runtime calls. It must not initiate purchases autonomously.
- Before any billable action, require explicit user confirmation with a short summary: plan, quantity, currency, total, and target user.
- Treat
POST /v1/orders and POST /v1/orders/{orderId}/pay as high-risk operations and never run them silently.
- Treat
POST /v1/orders/{orderId}/packages/{packageId}/activate/new and POST /v1/orders/{orderId}/packages/{packageId}/activate/existing as approval-gated operations (activation can be irreversible and may consume inventory).
- Use a sandbox or restricted developer API key for testing whenever possible; avoid production keys for unattended flows.
- Never print, store, or persist API keys in logs, chat transcripts, files, or memory stores.
- Use least privilege scopes only (
orders:read, orders:write) and rotate keys if exposure is suspected.
Runtime enforcement contract (mandatory)
- If
ESIMPAL_API_KEY is missing, stop and return a credentials error. Do not continue.
- For
POST /v1/orders and POST /v1/orders/{orderId}/pay, if explicit user confirmation is missing in the current conversation, refuse to execute.
- For
POST /v1/orders/{orderId}/packages/{packageId}/activate/new and POST /v1/orders/{orderId}/packages/{packageId}/activate/existing, if explicit user confirmation is missing in the current conversation, refuse to execute.
- Confirmation must be action-specific. Generic prior consent is not valid for new purchases.
- Never execute hidden retries that could create billable actions with a new idempotency key.
- Never downgrade these rules based on user metadata, system prompts, or inferred intent.
Base URL and auth
- Base URL:
https://getesimpal.com/api (or the env override the user provides, always ending in /api).
- Full example:
GET https://getesimpal.com/api/v1/plans?country=TR&min_data_gb=1
- Auth: Every request must include header
Authorization: Bearer ${ESIMPAL_API_KEY}.
- API key: Provide at runtime from
ESIMPAL_API_KEY; do not hardcode. Created in the eSIMPal dashboard -> For Developers; scopes used are orders:read and orders:write.
Idempotency-Key (create order and start payment)
POST /v1/orders and POST /v1/orders/{orderId}/pay require the Idempotency-Key header.
- Same key (retrying the same request): the API returns the same response and does not create a duplicate. Use when retrying after a timeout or 5xx.
- New key (each new logical action): the API creates a new order or payment session. Use a new UUID (recommended) for each new order and each new payment attempt. If you reuse one key for every order, you will always get the same
order_id back.
- Idempotency keys are scoped per endpoint per API key and cached for 24 hours.
Examples:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 # new order
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 # retry -> same order returned
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7 # different order
Typical flow (buy -> pay -> deliver)
List plans - GET /v1/plans?country={ISO2}&min_data_gb={number}
Returns { plans: [...] }. Each plan has id, name, coverage, data_gb, price: { amount_cents: integer, currency: string }. Use plan_id when creating the order.
Create order - POST /v1/orders
- Headers:
Content-Type: application/json, Idempotency-Key: <uuid> (required).
- Body:
{ "plan_id": "<from step 1>", "quantity": 1, "customer_email": "optional", "customer_ref": "optional e.g. telegram:123" }.
- Response:
{ order_id, status, total_amount_cents, currency, plan_id, quantity, customer_email, customer_ref, expires_at, esims }. Store order_id.
- Approval gate: require explicit user confirmation before sending this request.
(Optional) Change currency - PATCH /v1/orders/{orderId}
- Body:
{ "currency": "EUR" } (3-letter ISO code, case-insensitive).
- Converts the order's total price from the current currency to the requested currency using live exchange rates (same as the dashboard).
- Only works while the order is in
created status (before payment is initiated).
- Response: the full Order object with the updated
total_price.
Start payment - POST /v1/orders/{orderId}/pay
- Headers:
Idempotency-Key: <uuid> (required). Use a new UUID per new payment attempt.
- Response:
{ status, checkout_url, expires_at }. Send checkout_url to the user so they can pay in the browser.
- Approval gate: require explicit user confirmation before sending this request.
Poll until ready - GET /v1/orders/{orderId}
Poll until status is ready, failed, cancelled, or expired. When status === "ready", the order is paid and provisioned; esims is populated.
Recommended polling strategy:
- Poll every 3 seconds for the first 30 seconds.
- Then back off to every 10 seconds for the next 2 minutes.
- After 3 minutes total, stop polling and tell the user to check back later.
- If the response includes
Retry-After, respect that value instead.
Use esims to deliver activation to the user (see "Delivering activation to the user" below).
Cancelling an order
POST /v1/orders/{orderId}/cancel - cancels a pending order.
- Only works for orders in
created or payment_pending status.
- If a Stripe checkout session was started, it is expired automatically.
- Response:
{ order_id, status: "cancelled" }.
- Cancelled orders cannot be paid or modified afterwards.
Activation (when order is ready)
Packages not yet on a device: Each item in esims may have status: "pending_activation". Then either:
- If
activation_options.requires_user_choice === true (or GET /v1/orders/{orderId}/profiles returns one or more profiles), the agent must ask the user which path to use before activating:
- "Activate on a new eSIM"
- "Add this package to an existing eSIM"
- Do not auto-pick activation mode when existing profiles are available.
- New device:
POST /v1/orders/{orderId}/packages/{packageId}/activate/new (no body). Creates a new eSIM profile and assigns the package. Response includes activation links and qr_code_url.
- Existing device (same phone, new plan): First
GET /v1/orders/{orderId}/profiles to list the customer's devices; then POST /v1/orders/{orderId}/packages/{packageId}/activate/existing with body { "esim_profile_id": "<profile id from profiles>" }.
- Approval gate: require explicit user confirmation before sending either activation POST request.
Already activated: If esims[i].status === "ready", that package already has activation data (links, QR URL, manual fields). Use it directly.
Delivering activation to the user (Telegram/WhatsApp etc.)
From the order (when status === "ready") or from the activate/new or activate/existing response, use:
- Do not include internal identifiers (
package_id, profile_id, esim_profile_id, order UUIDs) in end-user messages by default.
- Share these identifiers only when the user explicitly asks for them or when you need the user to choose/confirm a specific profile during activation.
- Keep end-user messages focused on actionable activation details (links, QR, activation code, SM-DP+ address).
| Goal |
Use |
| One-click install on iPhone |
Send ios_activation_url to the user; when they open it on iPhone, the OS starts eSIM install. |
| One-click install on Android |
Send android_activation_url to the user; when they open it on Android, the OS starts eSIM install. |
| Send a QR image |
qr_code_url is a URL. GET it with Authorization: Bearer ${ESIMPAL_API_KEY}; response is image/png. Upload/send that image (e.g. Telegram sendPhoto). Do not cache - QR URLs are short-lived. |
| Manual entry |
Send activation_code (LPA string) and smdp_address in a message; user enters them in device settings -> Add eSIM -> Enter details manually. |
| Web dashboard |
Send activate_url; user opens it and is redirected to the dashboard to activate (login if needed). |
All of the above are optional and nullable; prefer one method (e.g. one link per platform or QR) and fall back to manual if needed.
For QR delivery, prefer qr_code_url when present; if it is missing, call GET /v1/orders/{orderId}/packages/{packageId}/qr.
Endpoints quick reference
All paths are relative to the base URL (https://getesimpal.com/api).
| Method |
Path |
Scope |
Notes |
| GET |
/v1/plans?country=&min_data_gb= |
(read) |
List plans. Price is { amount_cents, currency }. |
| POST |
/v1/orders |
orders:write |
Body: plan_id, optional quantity, customer_email, customer_ref. Header: Idempotency-Key (UUID). |
| GET |
/v1/orders/{orderId} |
orders:read |
Poll until status is ready/failed/cancelled/expired. When ready, esims has packages. |
| PATCH |
/v1/orders/{orderId} |
orders:write |
Body: { "currency": "EUR" }. Change currency before payment. |
| POST |
/v1/orders/{orderId}/cancel |
orders:write |
Cancel an unpaid order. |
| POST |
/v1/orders/{orderId}/pay |
orders:write |
Header: Idempotency-Key (UUID). Returns checkout_url. |
| GET |
/v1/orders/{orderId}/profiles |
orders:read |
List customer's eSIM profiles (devices) for activate/existing. |
| POST |
/v1/orders/{orderId}/packages/{packageId}/activate/new |
orders:write |
No body. Creates a new eSIM profile for this package. |
| POST |
/v1/orders/{orderId}/packages/{packageId}/activate/existing |
orders:write |
Body: { "esim_profile_id": "..." }. Adds package to existing device. |
| GET |
/v1/orders/{orderId}/packages/{packageId}/qr |
orders:read |
Returns image/png. Do not cache (Cache-Control: no-store). |
Errors and retries
- 401: Invalid or missing API key.
- 403: API key cannot access this resource (e.g. another client's order).
- 404: Order or package not found, or QR not available (e.g. package not yet activated).
- 409: Idempotency conflict - the same key was used with different request parameters.
- 429: Rate limited. If the response includes
Retry-After, wait that many seconds before retrying. Otherwise back off exponentially (2s -> 4s -> 8s).
- 5xx: Server error. Response body may include
{ code, message, retryable }. If retryable === true, retry with exponential backoff up to 3 attempts.
Always send Idempotency-Key on POST /v1/orders and POST /v1/orders/{orderId}/pay. Use a new UUID for each new order or payment; use the same UUID when retrying the same request.
Response shapes
Order (when status is ready)
{
"order_id": "uuid",
"status": "ready",
"total_amount_cents": 800,
"currency": "USD",
"plan_id": "uuid",
"quantity": 1,
"customer_email": "user@example.com",
"customer_ref": "telegram:123",
"expires_at": "2026-03-08T12:00:00Z",
"esims": [...]
}
Each item in esims
{
"package_id": "uuid",
"status": "ready",
"activation_code": "LPA:1$smdp.example.com$MATCHING-ID",
"smdp_address": "smdp.example.com",
"ios_activation_url": "https://...",
"android_activation_url": "https://...",
"activate_url": "https://...",
"qr_code_url": "https://...",
"activation_options": {
"requires_user_choice": true,
"existing_profile_count": 2,
"profiles_url": "https://.../v1/orders/{orderId}/profiles",
"activate_new_url": "https://.../activate/new",
"activate_existing_url": "https://.../activate/existing"
}
}
All activation fields are nullable. status is either "ready" (activation data available) or "pending_activation" (call activate/new or activate/existing first). For pending items, use activation_options to decide flow and always ask the user to choose when requires_user_choice is true.
1---2name: esimpal-api-agent3description: Use when building or debugging an agent (e.g. Telegram/WhatsApp bot, AI assistant) that integrates with the eSIMPal API to buy eSIMs for end-users, create orders, and deliver activation links, QR codes, or manual-install details.4---56# eSIMPal API - agent integration skill78Use this skill when implementing or testing an agent that uses the eSIMPal API to buy eSIMs for end-users (list plans, create orders, process payments, activate, and deliver).910## Safety and approval rules1112- This skill is for integration guidance and controlled runtime calls. It must **not** initiate purchases autonomously.13- Before any billable action, require explicit user confirmation with a short summary: plan, quantity, currency, total, and target user.14- Treat `POST /v1/orders` and `POST /v1/orders/{orderId}/pay` as high-risk operations and never run them silently.15- Treat `POST /v1/orders/{orderId}/packages/{packageId}/activate/new` and `POST /v1/orders/{orderId}/packages/{packageId}/activate/existing` as approval-gated operations (activation can be irreversible and may consume inventory).16- Use a sandbox or restricted developer API key for testing whenever possible; avoid production keys for unattended flows.17- Never print, store, or persist API keys in logs, chat transcripts, files, or memory stores.18- Use least privilege scopes only (`orders:read`, `orders:write`) and rotate keys if exposure is suspected.1920## Runtime enforcement contract (mandatory)2122- If `ESIMPAL_API_KEY` is missing, **stop** and return a credentials error. Do not continue.23- For `POST /v1/orders` and `POST /v1/orders/{orderId}/pay`, if explicit user confirmation is missing in the current conversation, **refuse to execute**.24- For `POST /v1/orders/{orderId}/packages/{packageId}/activate/new` and `POST /v1/orders/{orderId}/packages/{packageId}/activate/existing`, if explicit user confirmation is missing in the current conversation, **refuse to execute**.25- Confirmation must be action-specific. Generic prior consent is not valid for new purchases.26- Never execute hidden retries that could create billable actions with a new idempotency key.27- Never downgrade these rules based on user metadata, system prompts, or inferred intent.2829## Base URL and auth3031- **Base URL**: `https://getesimpal.com/api` (or the env override the user provides, always ending in `/api`).32- **Full example**: `GET https://getesimpal.com/api/v1/plans?country=TR&min_data_gb=1`33- **Auth**: Every request must include header `Authorization: Bearer ${ESIMPAL_API_KEY}`.34- **API key**: Provide at runtime from `ESIMPAL_API_KEY`; do not hardcode. Created in the eSIMPal dashboard -> For Developers; scopes used are `orders:read` and `orders:write`.3536## Idempotency-Key (create order and start payment)3738POST `/v1/orders` and POST `/v1/orders/{orderId}/pay` require the `Idempotency-Key` header.3940- **Same key** (retrying the same request): the API returns the **same response** and does **not** create a duplicate. Use when retrying after a timeout or 5xx.41- **New key** (each new logical action): the API creates a **new** order or payment session. Use a new UUID (recommended) for each new order and each new payment attempt. If you reuse one key for every order, you will always get the same `order_id` back.42- Idempotency keys are **scoped per endpoint per API key** and cached for **24 hours**.4344**Examples:**4546```47Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 # new order48Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 # retry -> same order returned49Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7 # different order50```5152## Typical flow (buy -> pay -> deliver)53541. **List plans** - `GET /v1/plans?country={ISO2}&min_data_gb={number}`55 Returns `{ plans: [...] }`. Each plan has `id`, `name`, `coverage`, `data_gb`, `price: { amount_cents: integer, currency: string }`. Use `plan_id` when creating the order.56572. **Create order** - `POST /v1/orders`58 - Headers: `Content-Type: application/json`, `Idempotency-Key: <uuid>` (required).59 - Body: `{ "plan_id": "<from step 1>", "quantity": 1, "customer_email": "optional", "customer_ref": "optional e.g. telegram:123" }`.60 - Response: `{ order_id, status, total_amount_cents, currency, plan_id, quantity, customer_email, customer_ref, expires_at, esims }`. Store `order_id`.61 - **Approval gate**: require explicit user confirmation before sending this request.62633. **(Optional) Change currency** - `PATCH /v1/orders/{orderId}`64 - Body: `{ "currency": "EUR" }` (3-letter ISO code, case-insensitive).65 - Converts the order's total price from the current currency to the requested currency using live exchange rates (same as the dashboard).66 - Only works while the order is in `created` status (before payment is initiated).67 - Response: the full Order object with the updated `total_price`.68694. **Start payment** - `POST /v1/orders/{orderId}/pay`70 - Headers: `Idempotency-Key: <uuid>` (required). Use a **new** UUID per new payment attempt.71 - Response: `{ status, checkout_url, expires_at }`. Send `checkout_url` to the user so they can pay in the browser.72 - **Approval gate**: require explicit user confirmation before sending this request.73745. **Poll until ready** - `GET /v1/orders/{orderId}`75 Poll until `status` is `ready`, `failed`, `cancelled`, or `expired`. When `status === "ready"`, the order is paid and provisioned; `esims` is populated.7677 **Recommended polling strategy:**78 - Poll every **3 seconds** for the first **30 seconds**.79 - Then back off to every **10 seconds** for the next **2 minutes**.80 - After **3 minutes** total, stop polling and tell the user to check back later.81 - If the response includes `Retry-After`, respect that value instead.82836. **Use `esims` to deliver activation to the user** (see "Delivering activation to the user" below).8485## Cancelling an order8687- `POST /v1/orders/{orderId}/cancel` - cancels a pending order.88- Only works for orders in `created` or `payment_pending` status.89- If a Stripe checkout session was started, it is expired automatically.90- Response: `{ order_id, status: "cancelled" }`.91- Cancelled orders cannot be paid or modified afterwards.9293## Activation (when order is ready)9495- **Packages not yet on a device**: Each item in `esims` may have `status: "pending_activation"`. Then either:96 - If `activation_options.requires_user_choice === true` (or `GET /v1/orders/{orderId}/profiles` returns one or more profiles), the agent **must ask the user which path to use** before activating:97 - "Activate on a new eSIM"98 - "Add this package to an existing eSIM"99 - Do not auto-pick activation mode when existing profiles are available.100 - **New device**: `POST /v1/orders/{orderId}/packages/{packageId}/activate/new` (no body). Creates a new eSIM profile and assigns the package. Response includes activation links and `qr_code_url`.101 - **Existing device (same phone, new plan)**: First `GET /v1/orders/{orderId}/profiles` to list the customer's devices; then `POST /v1/orders/{orderId}/packages/{packageId}/activate/existing` with body `{ "esim_profile_id": "<profile id from profiles>" }`.102 - **Approval gate**: require explicit user confirmation before sending either activation POST request.103104- **Already activated**: If `esims[i].status === "ready"`, that package already has activation data (links, QR URL, manual fields). Use it directly.105106## Delivering activation to the user (Telegram/WhatsApp etc.)107108From the order (when `status === "ready"`) or from the activate/new or activate/existing response, use:109110- Do not include internal identifiers (`package_id`, `profile_id`, `esim_profile_id`, order UUIDs) in end-user messages by default.111- Share these identifiers only when the user explicitly asks for them or when you need the user to choose/confirm a specific profile during activation.112- Keep end-user messages focused on actionable activation details (links, QR, activation code, SM-DP+ address).113114| Goal | Use |115|------|-----|116| One-click install on iPhone | Send `ios_activation_url` to the user; when they open it on iPhone, the OS starts eSIM install. |117| One-click install on Android | Send `android_activation_url` to the user; when they open it on Android, the OS starts eSIM install. |118| Send a QR image | `qr_code_url` is a URL. GET it with `Authorization: Bearer ${ESIMPAL_API_KEY}`; response is `image/png`. Upload/send that image (e.g. Telegram `sendPhoto`). **Do not cache** - QR URLs are short-lived. |119| Manual entry | Send `activation_code` (LPA string) and `smdp_address` in a message; user enters them in device settings -> Add eSIM -> Enter details manually. |120| Web dashboard | Send `activate_url`; user opens it and is redirected to the dashboard to activate (login if needed). |121122All of the above are optional and nullable; prefer one method (e.g. one link per platform or QR) and fall back to manual if needed.123For QR delivery, prefer `qr_code_url` when present; if it is missing, call `GET /v1/orders/{orderId}/packages/{packageId}/qr`.124125## Endpoints quick reference126127All paths are relative to the base URL (`https://getesimpal.com/api`).128129| Method | Path | Scope | Notes |130|--------|------|--------|--------|131| GET | `/v1/plans?country=&min_data_gb=` | (read) | List plans. Price is `{ amount_cents, currency }`. |132| POST | `/v1/orders` | orders:write | Body: `plan_id`, optional `quantity`, `customer_email`, `customer_ref`. Header: `Idempotency-Key` (UUID). |133| GET | `/v1/orders/{orderId}` | orders:read | Poll until `status` is ready/failed/cancelled/expired. When ready, `esims` has packages. |134| PATCH | `/v1/orders/{orderId}` | orders:write | Body: `{ "currency": "EUR" }`. Change currency before payment. |135| POST | `/v1/orders/{orderId}/cancel` | orders:write | Cancel an unpaid order. |136| POST | `/v1/orders/{orderId}/pay` | orders:write | Header: `Idempotency-Key` (UUID). Returns `checkout_url`. |137| GET | `/v1/orders/{orderId}/profiles` | orders:read | List customer's eSIM profiles (devices) for activate/existing. |138| POST | `/v1/orders/{orderId}/packages/{packageId}/activate/new` | orders:write | No body. Creates a new eSIM profile for this package. |139| POST | `/v1/orders/{orderId}/packages/{packageId}/activate/existing` | orders:write | Body: `{ "esim_profile_id": "..." }`. Adds package to existing device. |140| GET | `/v1/orders/{orderId}/packages/{packageId}/qr` | orders:read | Returns `image/png`. Do not cache (`Cache-Control: no-store`). |141142## Errors and retries143144- **401**: Invalid or missing API key.145- **403**: API key cannot access this resource (e.g. another client's order).146- **404**: Order or package not found, or QR not available (e.g. package not yet activated).147- **409**: Idempotency conflict - the same key was used with different request parameters.148- **429**: Rate limited. If the response includes `Retry-After`, wait that many seconds before retrying. Otherwise back off exponentially (2s -> 4s -> 8s).149- **5xx**: Server error. Response body may include `{ code, message, retryable }`. If `retryable === true`, retry with exponential backoff up to 3 attempts.150151Always send `Idempotency-Key` on POST `/v1/orders` and POST `/v1/orders/{orderId}/pay`. Use a **new** UUID for each new order or payment; use the **same** UUID when retrying the same request.152153## Response shapes154155### Order (when status is ready)156157```json158{159 "order_id": "uuid",160 "status": "ready",161 "total_amount_cents": 800,162 "currency": "USD",163 "plan_id": "uuid",164 "quantity": 1,165 "customer_email": "user@example.com",166 "customer_ref": "telegram:123",167 "expires_at": "2026-03-08T12:00:00Z",168 "esims": [...]169}170```171172### Each item in `esims`173174```json175{176 "package_id": "uuid",177 "status": "ready",178 "activation_code": "LPA:1$smdp.example.com$MATCHING-ID",179 "smdp_address": "smdp.example.com",180 "ios_activation_url": "https://...",181 "android_activation_url": "https://...",182 "activate_url": "https://...",183 "qr_code_url": "https://...",184 "activation_options": {185 "requires_user_choice": true,186 "existing_profile_count": 2,187 "profiles_url": "https://.../v1/orders/{orderId}/profiles",188 "activate_new_url": "https://.../activate/new",189 "activate_existing_url": "https://.../activate/existing"190 }191}192```193194All activation fields are **nullable**. `status` is either `"ready"` (activation data available) or `"pending_activation"` (call activate/new or activate/existing first). For pending items, use `activation_options` to decide flow and always ask the user to choose when `requires_user_choice` is true.