ChoEsim

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.

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:

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:

You get one of two answers.

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.

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.

Next