ChoEsim Partner API: quick start
This guide takes you from nothing to a first test order. It uses curl. You need about 20 minutes.
What the API does
The ChoEsim Partner API lets an approved travel company buy travel eSIMs in bulk and deliver them to its own customers.
- You list the plans you can buy and see your wholesale price.
- You buy eSIMs from a prepaid USD wallet.
- You get an activation string (an
LPA:1$...value) for each eSIM. You build the QR code and deliver it to your customer yourself. - You can cancel an order whose eSIMs are all unused, within the cancellation window. The wallet is credited.
- ChoEsim can tell you about changes with signed webhooks.
The API does not send e-mail to your customers. It does not take your customers' names, e-mail addresses or phone numbers.
All amounts are whole numbers in US cents (amount_minor). 1260 means USD 12.60.
1. Get a test key
You create your own keys, not through the API. Apply with a verified email address (it takes a few minutes and nobody has to approve it), then sign in to the partner portal and choose "Create test key". The portal also lets you create live keys, rotate keys and revoke them. If the portal is not available, write to support@choesim.com with the subject "Partner API".
A test key starts with cesim_test_. It works only on a simulated catalogue and a fake wallet. It never reaches a supplier and never spends money.
The key is shown once. Store it in a secret manager. ChoEsim keeps only a hash, so a lost key cannot be shown again.
A test key needs these scopes for this guide: catalog:read, orders:read, orders:write and wallet:read. Registering a webhook endpoint also needs webhooks:manage.
In the examples below, set two shell variables. The production host is https://choesim.com; the sandbox and the live API use the same host and are separated by the kind of key.
export CESIM_BASE="https://choesim.com/partner/v1"
export CESIM_KEY="cesim_test_REPLACE_ME"
Every request sends Authorization: Bearer <your key>. Never put the key in a URL or in client-side code.
2. Check your key
curl -s "$CESIM_BASE/me" \
-H "Authorization: Bearer $CESIM_KEY"
The response shows livemode (false for a test key), your scopes and your safety limits.
{
"object": "me",
"livemode": false,
"partner_id": "ptn_6b1f0c8e",
"key": {
"prefix": "cesim_test_ab12cd34",
"last4": "9xQz",
"scopes": ["catalog:read", "orders:read", "orders:write", "wallet:read"],
"ip_allowlist": []
},
"limits": {
"max_order_minor": 5000,
"daily_spend_cap_minor": 20000,
"max_open_orders": 3,
"daily_cancel_cap": 5,
"cancel_window_days": 14,
"max_items_per_order": 30,
"max_quantity_per_line": 10
}
}
The limit numbers above are an example. Your own limits are returned by GET /me. Daily limits count per UTC day.
3. Add fake money to the test wallet
A new test wallet starts at zero. Add fake money. This endpoint works only with test keys. It needs the orders:write scope and an Idempotency-Key.
curl -s -X POST "$CESIM_BASE/sandbox/wallet/topup" \
-H "Authorization: Bearer $CESIM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount_minor": 10000}'
The maximum balance is 1,000,000 minor units.
4. List plans
curl -s "$CESIM_BASE/plans?country=JP&limit=25" \
-H "Authorization: Bearer $CESIM_KEY"
Test keys see only test plans. Their ids start with test_plan_.
{
"object": "list",
"has_more": false,
"next_cursor": null,
"data": [
{
"id": "test_plan_ok_1gb_7d",
"object": "plan",
"name": "Test plan 1GB 7Days (sandbox)",
"coverage": ["JP"],
"volume_bytes": 1073741824,
"duration": 7,
"duration_unit": "DAY",
"data_type": "fixed",
"price": { "amount_minor": 1260, "currency": "USD" }
}
]
}
price is your wholesale price. It is a fake sandbox number. Real prices come from your own GET /plans with a live key. Version 1 sells fixed-data plans only.
The response is sent with Cache-Control: private, max-age=60, so you can reuse it for 1 minute. Prices can be up to 5 minutes old on our side. Send expected_total_minor when you order (step 6) to be protected against a change.
5. Create a quote
A quote prices a basket. It does not reserve anything or take money.
curl -s -X POST "$CESIM_BASE/quotes" \
-H "Authorization: Bearer $CESIM_KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"plan_id":"test_plan_ok_1gb_7d","quantity":2}]}'
{
"object": "quote",
"items": [
{
"plan_id": "test_plan_ok_1gb_7d",
"quantity": 2,
"unit_price": { "amount_minor": 1260, "currency": "USD" },
"line_total": { "amount_minor": 2520, "currency": "USD" }
}
],
"total": { "amount_minor": 2520, "currency": "USD" },
"wallet_balance": { "amount_minor": 10000, "currency": "USD" },
"sufficient_balance": true
}
6. Create a sandbox order
Orders need two things from you:
- An
Idempotency-Keyheader. It makes a retry safe. - A
reference. It is your own unique id for the order.
Send expected_total_minor from the quote. If the price has changed, the order is refused with 409 price_changed and nothing is debited.
curl -i -X POST "$CESIM_BASE/orders" \
-H "Authorization: Bearer $CESIM_KEY" \
-H "Idempotency-Key: 5b1d3c7e-0a3e-4f54-9d0e-REPLACE-ME" \
-H "Content-Type: application/json" \
-d '{
"reference": "partner-order-10042",
"customer_reference": "cust-7f3a",
"expected_total_minor": 2520,
"items": [{ "plan_id": "test_plan_ok_1gb_7d", "quantity": 2 }]
}'
Rules to remember:
customer_referenceis your own opaque id for your customer. It is rejected if it contains an@sign or nine or more digits in a row. Do not send personal data.- An order has at most 10 lines and 30 eSIMs in total, and at most 10 eSIMs per line.
- Unknown fields are rejected with
422 invalid_request.
You get one of two answers.
201withstatus: fulfilled. The eSIMs are ready and the response lists them.202withstatus: processing. The order is still in progress. Do not create a second order. Go to step 7.
POST /orders waits up to about 15 seconds before it answers 202.
If you repeat the same request later with the same Idempotency-Key, the status code follows the current state of the order. A first 202 becomes 201 once the eSIMs are ready.
If the order is refused before any money is taken (for example 409 price_changed or a validation error), nothing is stored for that key. You may send a corrected request with the same key, or use a new key.
A 201 response looks like this:
{
"id": "po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10",
"object": "order",
"livemode": false,
"reference": "partner-order-10042",
"customer_reference": "cust-7f3a",
"status": "fulfilled",
"status_detail": null,
"items": [
{
"plan_id": "test_plan_ok_1gb_7d",
"name": "Test plan 1GB 7Days (sandbox)",
"quantity": 2,
"unit_price": { "amount_minor": 1260, "currency": "USD" }
}
],
"total": { "amount_minor": 2520, "currency": "USD" },
"esims": [
{
"id": "esm_test_1",
"object": "esim",
"order_id": "po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10",
"iccid": "8900000000000000001",
"status": "GOT_RESOURCE",
"lpa": "LPA:1$sandbox.choesim.invalid$TEST-3F9A2C01"
}
],
"refund": null,
"created_at": "2026-11-01T09:30:00Z",
"updated_at": "2026-11-01T09:30:02Z"
}
The example shows one of the two eSIMs to keep it short. The real response lists both.
The lpa value is a secret. Anyone who has it can install the eSIM. Build your own QR code from it. ChoEsim does not return a QR image.
7. Read the order
curl -s "$CESIM_BASE/orders/po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10" \
-H "Authorization: Bearer $CESIM_KEY"
GET /orders/{id} is the source of truth. If you got 202, poll this endpoint until status is fulfilled or failed. Honour the Retry-After header.
The response is sent with Cache-Control: no-store. Do not cache it or write the lpa value to logs.
To try the slow path, order test_plan_slow_1gb_7d. See the sandbox guide.
8. Receive and verify a webhook
Webhooks tell you when an order changes. They are an optimisation. If one is lost, GET /orders/{id} still gives the truth.
Register an endpoint. This needs the webhooks:manage scope on your key. The URL must be https, on port 443, and must resolve to a public address. Redirects are not followed. You can have at most 3 endpoints per mode.
curl -s -X POST "$CESIM_BASE/webhook-endpoints" \
-H "Authorization: Bearer $CESIM_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://partner.example.com/hooks/choesim",
"enabled_events": ["order.fulfilled", "order.failed"]
}'
The response contains a secret that starts with whsec_. It is shown once. Store it in a secret manager.
ChoEsim sends a ping event when an endpoint is created.
Each delivery carries three headers: webhook-id, webhook-timestamp and webhook-signature. Verify the signature on the raw request body before you trust it. A full Node.js and Python example, with a test vector you can run, is in the API guide.
A webhook body is small. It has ids and a status, never activation data. After you get one, call GET /orders/{id}.
{
"id": "evt_1f9c2a7e",
"object": "event",
"type": "order.fulfilled",
"api_version": "v1",
"livemode": false,
"created": "2026-11-01T09:30:02Z",
"data": {
"order_id": "po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10",
"reference": "partner-order-10042",
"status": "fulfilled"
}
}
Reply with a 2xx status within 10 seconds, and do the real work after you reply. The same event can arrive more than once. Use webhook-id to skip duplicates.
9. Go-live checklist
Tick every item before you create a live key in the portal.
- You accepted the partner terms (the box in the online application).
- Your wallet is funded. The wallet is prepaid: pay by card through OnePay on the portal, choosing the amount yourself. It is credited when the payment is confirmed.
- You use a live key (
cesim_live_...) on your server only, in a secret manager. - You sent your server IP addresses to ChoEsim if you want an IP allow-list. It is empty by default.
- Every
POST /ordersandPOST /orders/{id}/cancelsends anIdempotency-Key. - You never create a second order after a timeout or a
202. You poll or repeat the same request with the same key. - You use a unique
referencefor each order. It can never be reused. - You pass
expected_total_minorand handle409 price_changed. - Your webhook handler verifies the signature on the raw body, checks the timestamp, skips duplicate
webhook-idvalues and answers fast. - You also reconcile with
GET /eventsandGET /orders, so a missed webhook does not matter. - You handle
429by waitingRetry-Afterseconds,503by retrying later, and500by checking the order byreferencefirst. - You know your cancellation window and daily cancellation cap (
GET /me). - Activation strings (
lpa) are not written to logs, analytics or the browser. - You do not send personal data of end customers to ChoEsim.
- You tested the failure paths in the sandbox:
fail,slow,unknown,price_change,nocancel,sbx-429-andsbx-insufficient-. - You checked device compatibility and the destination with your customer before selling. ChoEsim does not promise compatibility, coverage or speed.
A first real order on a live key is a separate step agreed with ChoEsim. Live ordering is switched on by ChoEsim, not by you.