openapi: 3.1.0 info: title: ChoEsim Partner API version: "1.0.0" summary: Wholesale eSIM ordering for approved ChoEsim partners. description: | Every price in the examples is an arithmetic illustration, not a real ChoEsim price. Plan ids starting with `test_plan_` exist only in test mode. Human documentation: https://choesim.com/partners/docs/quick-start/ ## Modes A key starting with `cesim_live_` works on live data and a prepaid USD wallet. A key starting with `cesim_test_` works on a separate simulated catalogue and a separate fake wallet; it never reaches any supplier and never spends money. ## Authentication Send `Authorization: Bearer ` on every request. Keys are shown once when created; ChoEsim stores only a hash. Never put a key in a URL, a log or client-side code. Keys carry scopes (`catalog:read`, `orders:read`, `orders:write`, `wallet:read`, `webhooks:manage`) and may be restricted to a list of server IP addresses. ## Idempotency `POST /orders` and `POST /orders/{order_id}/cancel` require an `Idempotency-Key` header (1-64 characters from `A-Za-z0-9_:.-`, a UUID is recommended). Repeating a request with the same key and the same body returns the **current** state of the same order, with `Idempotent-Replayed: true`. The status code reflects that current state, not the first answer: `201` once an order creation has reached `fulfilled`, `202` while it is still `accepted` or `processing`, `200` when it ended as `failed` or `cancelled`. (A first answer of `202` therefore becomes `201` on a later replay once the eSIMs are ready.) The same rule applies to cancellation: `200` when cancelled, `202` while `cancelling`. The same key with a different body returns `422 idempotency_key_reuse`. A request still running returns `409 idempotency_in_progress`. Keys are kept for 7 days. Independently, every order carries a partner-chosen `reference` that is unique forever per mode; a second order with the same `reference` returns `409 duplicate_reference` with the existing order id. ## Timeouts are not failures `POST /orders` waits up to about 15 seconds. If the order is not complete by then you receive `202` and `status: processing`. Never create a second order because a call timed out or returned 202: poll `GET /orders/{id}`, or repeat the same request with the same `Idempotency-Key`. An order in `processing` with `status_detail: under_review` is being checked by ChoEsim staff; the wallet stays debited until the outcome is proven. ## Webhooks Webhooks are an optimisation. `GET /orders/{id}` is the source of truth; reconcile with `GET /events` and `GET /orders`. Delivery order is not guaranteed and an event can be delivered more than once: de-duplicate on the `webhook-id` header. Payloads are thin (ids and status, never activation data). ## Rate limits Limits apply per key and per request class. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (seconds until the window resets). A `429` always carries `Retry-After` (seconds). Other safety limits (maximum order value, daily spend, simultaneous open orders) are returned by `GET /me` and are enforced in the database. ## Errors Errors use RFC 9457 `application/problem+json` with a stable machine-readable `code` and a `request_id`. Clients must ignore unknown members. Status and code pairs: 400 `invalid_request` (malformed JSON, bad query parameter) and `idempotency_key_required`; 401 `invalid_api_key`; 402 `insufficient_balance`; 403 `forbidden_scope`, `ip_not_allowed`, `partner_suspended`, `wallet_frozen`; 404 `not_found`; 409 `idempotency_in_progress`, `duplicate_reference`, `price_changed`, `esim_not_cancellable`, `cancel_window_expired`; 413 `payload_too_large`; 415 `unsupported_media_type`; 422 `invalid_request` (body fails validation: unknown field, out-of-range value), `idempotency_key_reuse`, `plan_unavailable`, `order_limit_exceeded` (per-order value, daily spend or simultaneous open orders; the `limit` member names which), `cancel_limit_exceeded` (daily cancellation cap); 429 `rate_limited`; 500 `internal_error` (any operation; nothing is lost silently, quote `request_id`); 503 `orders_disabled`, `supplier_unavailable`, `supplier_capacity`. Orders that have not been debited are never left half-created after a 4xx or 503. ## Versioning The major version is in the path. Adding fields, endpoints or output enum values is not a breaking change; clients must ignore what they do not know. Deprecation is announced with `Deprecation` and `Sunset` headers and at least six months notice, except for urgent security, legal or abuse reasons. ## Money All amounts are integers in minor units (`amount_minor`, USD cents) with `currency: USD`. No floating point. contact: name: ChoEsim partner support email: support@choesim.com url: https://choesim.com/partners/ license: name: Proprietary, subject to the ChoEsim partner terms identifier: LicenseRef-ChoEsim-Partner-Terms servers: - url: "https://choesim.com/partner/v1" description: Production. Test keys (cesim_test_) and live keys (cesim_live_) use the same host. security: - BearerApiKey: [] tags: - name: Account - name: Catalogue - name: Orders - name: eSIMs - name: Wallet - name: Events and webhooks - name: Sandbox paths: /me: get: tags: [Account] operationId: getMe summary: Show the key, mode, scopes and limits description: Requires any valid key. Use it to confirm which mode a key is in and which safety limits apply. x-required-scope: none responses: "200": description: Key information. headers: X-RateLimit-Limit: {$ref: "#/components/headers/RateLimitLimit"} X-RateLimit-Remaining: {$ref: "#/components/headers/RateLimitRemaining"} X-RateLimit-Reset: {$ref: "#/components/headers/RateLimitReset"} Request-Id: {$ref: "#/components/headers/RequestId"} content: application/json: schema: {$ref: "#/components/schemas/Me"} example: 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} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} /plans: get: tags: [Catalogue] operationId: listPlans summary: List plans with your wholesale price description: | Returns only plans you can buy. Prices are your wholesale price in USD cents; the cost to ChoEsim and any retail price are never returned. Fixed-data plans only in v1. The response is sent with `Cache-Control: private, max-age=60`, so a client may reuse it for 1 minute. The prices themselves can be up to 5 minutes old on our side (supplier catalogue cache); `POST /orders` with `expected_total_minor` protects you against a change. x-required-scope: catalog:read parameters: - name: country in: query description: ISO 3166-1 alpha-2 code; plans whose coverage includes it. schema: {type: string, pattern: "^[A-Z]{2}$", examples: [JP]} - {$ref: "#/components/parameters/Cursor"} - {$ref: "#/components/parameters/Limit"} responses: "200": description: A page of plans. headers: X-RateLimit-Limit: {$ref: "#/components/headers/RateLimitLimit"} X-RateLimit-Remaining: {$ref: "#/components/headers/RateLimitRemaining"} X-RateLimit-Reset: {$ref: "#/components/headers/RateLimitReset"} Request-Id: {$ref: "#/components/headers/RequestId"} content: application/json: schema: {$ref: "#/components/schemas/PlanList"} example: 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} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} "503": {$ref: "#/components/responses/Unavailable"} /plans/{plan_id}: get: tags: [Catalogue] operationId: getPlan summary: Get one plan x-required-scope: catalog:read parameters: - name: plan_id in: path required: true schema: {type: string, maxLength: 100} responses: "200": description: The plan. content: application/json: schema: {$ref: "#/components/schemas/Plan"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/RateLimited"} /quotes: post: tags: [Catalogue] operationId: createQuote summary: Price a basket (informational, does not reserve anything) description: The quote is not a reservation. Pass `expected_total_minor` to `POST /orders` to be protected against a price change. x-required-scope: catalog:read requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/QuoteRequest"} example: items: - {plan_id: test_plan_ok_1gb_7d, quantity: 2} responses: "200": description: Priced basket. content: application/json: schema: {$ref: "#/components/schemas/Quote"} example: 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 "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "413": {$ref: "#/components/responses/PayloadTooLarge"} "415": {$ref: "#/components/responses/UnsupportedMediaType"} "422": {$ref: "#/components/responses/Unprocessable"} "429": {$ref: "#/components/responses/RateLimited"} /orders: post: tags: [Orders] operationId: createOrder summary: Buy eSIMs from your wallet description: | Debits your wallet and starts delivery in one atomic step. Requires `Idempotency-Key`. Returns `201` when the eSIMs are ready within about 15 seconds, otherwise `202` with `Location` and `status: processing`. The request carries no end-customer personal data: use `customer_reference` for your own opaque id (an `@` sign or nine or more consecutive digits is rejected). ChoEsim never e-mails your customers. Up to 10 lines and 30 eSIMs in total, at most 10 per line. Daily (per-day) plans are not available in v1. In test mode, magic plan ids and references select the outcome (see the `x-sandbox-magic-values` extension below). x-required-scope: orders:write x-sandbox-magic-values: test_plan_ok_1gb_7d: fulfilled immediately test_plan_slow_1gb_7d: accepted, processing for 30 seconds, then fulfilled test_plan_unknown_1gb_7d: processing with status_detail under_review for 120 seconds, then fulfilled test_plan_fail_1gb_7d: failed, wallet credited, order.failed event test_plan_oos_1gb_7d: 422 plan_unavailable test_plan_price_change_1gb_7d: 409 price_changed on the first attempt per reference test_plan_nocancel_1gb_7d: fulfilled, but cancel returns 409 esim_not_cancellable reference prefix sbx-429-: 429 rate_limited with Retry-After 2 reference prefix sbx-insufficient-: 402 insufficient_balance parameters: - {$ref: "#/components/parameters/IdempotencyKey"} requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/OrderCreate"} examples: basic: summary: Two eSIMs, with price protection value: reference: partner-order-10042 customer_reference: cust-7f3a expected_total_minor: 2520 items: - {plan_id: test_plan_ok_1gb_7d, quantity: 2} responses: "201": description: Order created and fulfilled. headers: Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"} Request-Id: {$ref: "#/components/headers/RequestId"} X-RateLimit-Limit: {$ref: "#/components/headers/RateLimitLimit"} X-RateLimit-Remaining: {$ref: "#/components/headers/RateLimitRemaining"} X-RateLimit-Reset: {$ref: "#/components/headers/RateLimitReset"} content: application/json: schema: {$ref: "#/components/schemas/Order"} example: 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" - id: esm_test_2 object: esim order_id: po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10 iccid: "8900000000000000002" status: GOT_RESOURCE lpa: "LPA:1$sandbox.choesim.invalid$TEST-3F9A2C02" refund: null created_at: "2026-11-01T09:30:00Z" updated_at: "2026-11-01T09:30:02Z" "202": description: Order accepted and still processing. Poll the order or wait for a webhook. Never re-create the order. headers: Location: {description: Absolute path of the order., schema: {type: string}} Retry-After: {$ref: "#/components/headers/RetryAfter"} Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"} Request-Id: {$ref: "#/components/headers/RequestId"} content: application/json: schema: {$ref: "#/components/schemas/Order"} example: id: po_test_9d2b7a44-0c1d-4e5f-8a12-7b6c5d4e3f21 object: order livemode: false reference: partner-order-10043 customer_reference: null status: processing status_detail: under_review items: - {plan_id: test_plan_unknown_1gb_7d, name: "Test plan 1GB 7Days (sandbox, unknown outcome)", quantity: 1, unit_price: {amount_minor: 1260, currency: USD}} total: {amount_minor: 1260, currency: USD} esims: [] refund: null created_at: "2026-11-01T09:31:00Z" updated_at: "2026-11-01T09:31:15Z" "200": description: The order ended as `failed` (the wallet was credited back; see `refund`) or `cancelled`. A repeat of a request with the same Idempotency-Key returns this status once the order has ended that way. headers: Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"} Request-Id: {$ref: "#/components/headers/RequestId"} content: application/json: schema: {$ref: "#/components/schemas/Order"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthorized"} "402": {$ref: "#/components/responses/InsufficientBalance"} "403": {$ref: "#/components/responses/Forbidden"} "409": {$ref: "#/components/responses/Conflict"} "413": {$ref: "#/components/responses/PayloadTooLarge"} "415": {$ref: "#/components/responses/UnsupportedMediaType"} "422": {$ref: "#/components/responses/Unprocessable"} "429": {$ref: "#/components/responses/RateLimited"} "500": {$ref: "#/components/responses/InternalError"} "503": {$ref: "#/components/responses/Unavailable"} get: tags: [Orders] operationId: listOrders summary: List your orders, newest first x-required-scope: orders:read parameters: - {$ref: "#/components/parameters/Cursor"} - {$ref: "#/components/parameters/Limit"} - name: status in: query schema: {$ref: "#/components/schemas/OrderStatus"} - name: created_from in: query description: Only orders created at or after this instant. schema: {type: string, format: date-time} - name: created_to in: query schema: {type: string, format: date-time} - name: reference in: query description: Exact match on your reference. schema: {type: string, maxLength: 64} responses: "200": description: A page of orders. `esims` is omitted in lists; fetch an order for activation data. content: application/json: schema: {$ref: "#/components/schemas/OrderList"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} /orders/{order_id}: get: tags: [Orders] operationId: getOrder summary: Get an order, including activation data when fulfilled description: | The response contains activation strings (`lpa`) and is sent with `Cache-Control: no-store`. Treat them as secrets. Each read of activation data is recorded in the audit log. An order that is not yours answers `404`, the same as one that does not exist. x-required-scope: orders:read parameters: - {$ref: "#/components/parameters/OrderId"} responses: "200": description: The order. content: application/json: schema: {$ref: "#/components/schemas/Order"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/RateLimited"} "500": {$ref: "#/components/responses/InternalError"} /orders/{order_id}/cancel: post: tags: [Orders] operationId: cancelOrder summary: Cancel a whole order of unused eSIMs and credit your wallet description: | Allowed only if every eSIM of the order is unused and the supplier accepts the cancellation. The wallet is credited in full, once, only after every eSIM is confirmed cancelled. Partial cancellation is not supported. If some eSIMs cannot be cancelled the order moves to `cancelling` and staff resolve it; no automatic credit is made. A daily cancellation cap applies (`422 cancel_limit_exceeded`, counted per UTC day) and so does a cancellation window: an order older than `cancel_window_days` (default 14, see `GET /me`) is refused with `409 cancel_window_expired` and nothing is changed. Requires `Idempotency-Key`. A cancel that is still running is not taken over by a second request with the same key: you receive `409 idempotency_in_progress` until it ends. If you retry after a partial failure, eSIMs already cancelled are not cancelled twice. x-required-scope: orders:write parameters: - {$ref: "#/components/parameters/OrderId"} - {$ref: "#/components/parameters/IdempotencyKey"} responses: "200": description: Cancelled and credited (or replay of an earlier successful call). content: application/json: schema: {$ref: "#/components/schemas/Order"} "202": description: Cancellation started and not finished within the wait budget; poll the order. content: application/json: schema: {$ref: "#/components/schemas/Order"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "422": {$ref: "#/components/responses/Unprocessable"} "429": {$ref: "#/components/responses/RateLimited"} "500": {$ref: "#/components/responses/InternalError"} /esims/{esim_id}: get: tags: [eSIMs] operationId: getEsim summary: Get one eSIM of one of your orders x-required-scope: orders:read parameters: - {$ref: "#/components/parameters/EsimId"} responses: "200": description: The eSIM. content: application/json: schema: {$ref: "#/components/schemas/Esim"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/RateLimited"} /esims/{esim_id}/usage: get: tags: [eSIMs] operationId: getEsimUsage summary: Last known data usage and validity description: | Built from supplier notifications that ChoEsim has received; it can lag or be empty. Fields are `null` when unknown (UNKNOWN is not zero). This call never queries the supplier, so polling it is cheap. x-required-scope: orders:read parameters: - {$ref: "#/components/parameters/EsimId"} responses: "200": description: Usage snapshot. content: application/json: schema: {$ref: "#/components/schemas/EsimUsage"} example: object: esim_usage esim_id: esm_test_1 esim_status: IN_USE total_bytes: 1073741824 used_bytes: 104857600 remaining_bytes: 968884224 remaining_days: 5 expires_at: "2026-11-08T09:30:00Z" updated_at: "2026-11-02T08:00:00Z" "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/RateLimited"} /wallet: get: tags: [Wallet] operationId: getWallet summary: Wallet balance x-required-scope: wallet:read responses: "200": description: Balance in USD cents. content: application/json: schema: {$ref: "#/components/schemas/Wallet"} example: object: wallet livemode: false currency: USD balance_minor: 7480 status: active as_of: "2026-11-01T09:32:00Z" "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} /wallet/entries: get: tags: [Wallet] operationId: listWalletEntries summary: Append-only ledger of your wallet, newest first description: Every entry carries the running balance after it, so a statement can be rebuilt and checked. x-required-scope: wallet:read parameters: - {$ref: "#/components/parameters/Cursor"} - {$ref: "#/components/parameters/Limit"} - name: created_from in: query schema: {type: string, format: date-time} - name: created_to in: query schema: {type: string, format: date-time} responses: "200": description: A page of ledger entries. content: application/json: schema: {$ref: "#/components/schemas/WalletEntryList"} example: object: list has_more: false next_cursor: null data: - {id: wle_0f1e2d3c, object: wallet_entry, seq: 3, kind: debit_order, amount_minor: -2520, currency: USD, balance_after_minor: 7480, order_id: po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10, ref: null, created_at: "2026-11-01T09:30:00Z"} - {id: wle_0f1e2d3b, object: wallet_entry, seq: 2, kind: topup, amount_minor: 10000, currency: USD, balance_after_minor: 10000, order_id: null, ref: sandbox-topup-1, created_at: "2026-11-01T09:00:00Z"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} /events: get: tags: [Events and webhooks] operationId: listEvents summary: Event log (reconcile after webhook downtime) description: Events are kept for 90 days. Same objects that are sent to your webhook endpoints. x-required-scope: orders:read parameters: - {$ref: "#/components/parameters/Cursor"} - {$ref: "#/components/parameters/Limit"} - name: type in: query schema: {$ref: "#/components/schemas/EventType"} - name: created_from in: query schema: {type: string, format: date-time} responses: "200": description: A page of events, oldest first so you can resume from `next_cursor`. content: application/json: schema: {$ref: "#/components/schemas/EventList"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} /events/{event_id}/resend: post: tags: [Events and webhooks] operationId: resendEvent summary: Send an event again to your endpoints description: The same `webhook-id` is used, so a receiver that de-duplicates will ignore it if it already processed the event. x-required-scope: webhooks:manage parameters: - {$ref: "#/components/parameters/EventId"} responses: "202": description: Delivery scheduled. content: application/json: schema: type: object required: [object, event_id] properties: object: {type: string, const: event_resend} event_id: {type: string} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/RateLimited"} /webhook-endpoints: get: tags: [Events and webhooks] operationId: listWebhookEndpoints summary: List webhook endpoints (secrets are never returned) x-required-scope: webhooks:manage responses: "200": description: Endpoints. content: application/json: schema: type: object required: [object, data] properties: object: {type: string, const: list} data: type: array items: {$ref: "#/components/schemas/WebhookEndpoint"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} post: tags: [Events and webhooks] operationId: createWebhookEndpoint summary: Register an endpoint (HTTPS only; the signing secret is shown once) description: | The URL must be `https`, on port 443, and resolve to a public address. Redirects are not followed. Rejected addresses return `422 invalid_request` with `errors[].pointer` set to `/url`. At most 3 endpoints per mode. Sandbox note: a test-mode key sends real webhooks to a test-mode endpoint. x-required-scope: webhooks:manage requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/WebhookEndpointCreate"} example: url: "https://partner.example.com/hooks/choesim" enabled_events: ["order.fulfilled", "order.failed"] responses: "201": description: Created. `secret` is returned only here. content: application/json: schema: {$ref: "#/components/schemas/WebhookEndpointWithSecret"} example: id: whe_8f2a1c9d object: webhook_endpoint livemode: false url: "https://partner.example.com/hooks/choesim" enabled_events: ["order.fulfilled", "order.failed"] status: active secret: "whsec_Y2hvZXNpbS1kb2NzLWV4YW1wbGUta2V5LTMyYnl0ZXMh" created_at: "2026-11-01T09:00:00Z" "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} "422": {$ref: "#/components/responses/Unprocessable"} /webhook-endpoints/{endpoint_id}: delete: tags: [Events and webhooks] operationId: deleteWebhookEndpoint summary: Delete an endpoint x-required-scope: webhooks:manage parameters: - name: endpoint_id in: path required: true schema: {type: string} responses: "204": {description: Deleted.} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} "404": {$ref: "#/components/responses/NotFound"} /webhook-endpoints/{endpoint_id}/rotate-secret: post: tags: [Events and webhooks] operationId: rotateWebhookSecret summary: Issue a new signing secret; the old one also signs for 24 hours description: During the overlap every delivery carries two signatures in `webhook-signature`, separated by a space. x-required-scope: webhooks:manage parameters: - name: endpoint_id in: path required: true schema: {type: string} responses: "200": description: New secret, shown once. content: application/json: schema: {$ref: "#/components/schemas/WebhookEndpointWithSecret"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} "404": {$ref: "#/components/responses/NotFound"} /sandbox/wallet/topup: post: tags: [Sandbox] operationId: sandboxTopup summary: Add fake money to the test wallet (test keys only) description: A live key receives `403 forbidden_scope`. Requires `Idempotency-Key`. Maximum balance 1,000,000 minor units. Requires the `orders:write` scope because it changes a (fake) balance; it is a test-key-only endpoint. x-required-scope: orders:write parameters: - {$ref: "#/components/parameters/IdempotencyKey"} requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [amount_minor] properties: amount_minor: {type: integer, minimum: 1, maximum: 1000000} example: {amount_minor: 10000} responses: "201": description: Credited. content: application/json: schema: {$ref: "#/components/schemas/Wallet"} "401": {$ref: "#/components/responses/Unauthorized"} "403": {$ref: "#/components/responses/Forbidden"} "429": {$ref: "#/components/responses/RateLimited"} "422": {$ref: "#/components/responses/Unprocessable"} webhooks: order.fulfilled: post: summary: Every eSIM of the order is ready (fetch the order for activation data). operationId: webhookOrderFulfilled parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventOrder"} example: 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} responses: "2XX": {description: Acknowledged. Reply quickly (within 10 seconds) and process asynchronously.} "410": {description: Gone. ChoEsim stops sending to this endpoint and disables it.} order.failed: post: summary: The order definitely failed and the wallet was credited. Never sent for an unknown outcome. operationId: webhookOrderFailed parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventOrder"} example: id: evt_2a8d3b6f object: event type: order.failed api_version: v1 livemode: false created: "2026-11-01T09:31:02Z" data: {order_id: po_test_5c1d8e72-1b2a-4c3d-9e8f-6a7b5c4d3e2f, reference: partner-order-10044, status: failed, failure_code: price_changed} responses: "2XX": {description: Acknowledged.} order.cancelled: post: summary: Cancellation finished and the wallet was credited. operationId: webhookOrderCancelled parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventOrder"} example: id: evt_4c0f5d9b object: event type: order.cancelled api_version: v1 livemode: false created: "2026-11-02T10:00:00Z" data: {order_id: po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10, reference: partner-order-10042, status: cancelled} responses: "2XX": {description: Acknowledged.} order.delayed: post: summary: The order has been processing longer than expected. Informational; no action needed from you. operationId: webhookOrderDelayed parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventOrder"} example: id: evt_5d1a6eac object: event type: order.delayed api_version: v1 livemode: false created: "2026-11-01T09:41:00Z" data: {order_id: po_test_9d2b7a44-0c1d-4e5f-8a12-7b6c5d4e3f21, reference: partner-order-10043, status: processing} responses: "2XX": {description: Acknowledged.} esim.status_changed: post: summary: Supplier-reported status of an eSIM changed (installed, in use, used up, expired, cancelled). operationId: webhookEsimStatusChanged parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventEsim"} example: id: evt_3b9e4c8a object: event type: esim.status_changed api_version: v1 livemode: true created: "2026-11-02T08:00:00Z" data: {esim_id: esm_example, order_id: po_example, esim_status: IN_USE} responses: "2XX": {description: Acknowledged.} wallet.credited: post: summary: A top-up or refund was credited to the wallet. operationId: webhookWalletCredited parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventWallet"} example: id: evt_6e2b7fbd object: event type: wallet.credited api_version: v1 livemode: false created: "2026-11-01T09:00:00Z" data: {balance_minor: 10000, currency: USD, entry_id: wle_0f1e2d3b} responses: "2XX": {description: Acknowledged.} wallet.low_balance: post: summary: The wallet fell below the threshold agreed with ChoEsim. operationId: webhookWalletLowBalance parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventWallet"} example: id: evt_7f3c80ce object: event type: wallet.low_balance api_version: v1 livemode: false created: "2026-11-01T09:35:00Z" data: {balance_minor: 900, currency: USD} responses: "2XX": {description: Acknowledged.} ping: post: summary: Sent when an endpoint is created or re-enabled. operationId: webhookPing parameters: - {$ref: "#/components/parameters/WebhookId"} - {$ref: "#/components/parameters/WebhookTimestamp"} - {$ref: "#/components/parameters/WebhookSignature"} requestBody: content: application/json: schema: {$ref: "#/components/schemas/EventPing"} example: id: evt_80d491df object: event type: ping api_version: v1 livemode: false created: "2026-11-01T09:00:00Z" data: {endpoint_id: whe_8f2a1c9d} responses: "2XX": {description: Acknowledged.} components: securitySchemes: BearerApiKey: type: http scheme: bearer bearerFormat: "cesim_{live|test}_{key_id}_{secret}" description: | Static API key. Scope required by each operation is given in `x-required-scope`. Webhook authenticity is a separate mechanism: Standard Webhooks signing with the endpoint secret (`webhook-id`, `webhook-timestamp`, `webhook-signature`; HMAC-SHA256 over `{id}.{timestamp}.{body}`). Verify with the raw body, reject timestamps more than 5 minutes old, compare in constant time. **Signing key (Standard Webhooks method).** The endpoint secret is `whsec_` followed by standard base64 of the key bytes. Strip the `whsec_` prefix and base64-decode the rest: the decoded bytes are the HMAC key (do not use the base64 text as the key). Signature = `base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}"))`, sent as `v1,`. Test vector (documentation key, never a real secret): secret `whsec_Y2hvZXNpbS1kb2NzLWV4YW1wbGUta2V5LTMyYnl0ZXMh`, `webhook-id` `evt_1f9c2a7e`, `webhook-timestamp` `1793525402`, body exactly `{"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"}}` gives `webhook-signature: v1,yOGM2hgcarOxufvwHvPAvpRmv8QfypZQYDforcG5N50=`. The vector was computed with OpenSSL HMAC-SHA256; the method follows the Standard Webhooks specification as summarised in the project source register (S-R2-006) and must be re-checked against the upstream text before publication. parameters: IdempotencyKey: name: Idempotency-Key in: header required: true description: 1-64 characters from `A-Za-z0-9_:.-`; a UUID is recommended. Kept for 7 days. schema: {type: string, minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9_:.-]+$"} OrderId: name: order_id in: path required: true schema: {type: string, maxLength: 80, examples: [po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10]} EsimId: name: esim_id in: path required: true schema: {type: string, maxLength: 100} EventId: name: event_id in: path required: true schema: {type: string, maxLength: 80} Cursor: name: cursor in: query description: Opaque value from `next_cursor` of the previous page. schema: {type: string, maxLength: 200} Limit: name: limit in: query schema: {type: integer, minimum: 1, maximum: 100, default: 25} WebhookId: name: webhook-id in: header required: true description: Event id; identical on every retry. De-duplicate on it. schema: {type: string} WebhookTimestamp: name: webhook-timestamp in: header required: true description: Unix time in seconds of this attempt (changes on each retry). schema: {type: string} WebhookSignature: name: webhook-signature in: header required: true description: One or two space-separated `v1,` signatures (two during secret rotation). schema: {type: string} headers: RateLimitLimit: description: Requests allowed in the current window for this key and request class. schema: {type: integer} RateLimitRemaining: description: Requests left in the current window. schema: {type: integer} RateLimitReset: description: Seconds until the window resets. schema: {type: integer} RetryAfter: description: Seconds to wait before trying again. schema: {type: integer, minimum: 0} RequestId: description: Unique id of this request; quote it to support. schema: {type: string} Deprecation: description: "RFC 9745 Structured Field Date (`@unix`) present on deprecated operations." schema: {type: string} Sunset: description: HTTP-date after which the operation may stop working (RFC 8594). schema: {type: string} IdempotentReplayed: description: "`true` when this response replays an earlier request with the same Idempotency-Key." schema: {type: string, enum: ["true"]} responses: BadRequest: description: Malformed request, missing `Idempotency-Key`, or invalid query. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:idempotency_key_required" title: Idempotency-Key header required status: 400 detail: POST /orders requires an Idempotency-Key header. instance: /partner/v1/orders code: idempotency_key_required request_id: req_7c1e2f90 Unauthorized: description: Unknown, revoked or expired key. The reason is deliberately not revealed. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:invalid_api_key" title: Invalid API key status: 401 detail: The API key is missing, unknown, revoked or expired. instance: /partner/v1/orders code: invalid_api_key request_id: req_7c1e2f91 Forbidden: description: "Key valid but not allowed: `forbidden_scope` (scope missing, or a test-only endpoint called with a live key), `ip_not_allowed` (caller IP not on the key's allow-list), `partner_suspended`, or `wallet_frozen`." content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:forbidden_scope" title: Scope not granted status: 403 detail: This key does not have the orders:write scope. instance: /partner/v1/orders code: forbidden_scope request_id: req_7c1e2f92 NotFound: description: No such resource, or it belongs to another partner (the two are indistinguishable by design). content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:not_found" title: Not found status: 404 instance: /partner/v1/orders/po_unknown code: not_found request_id: req_7c1e2f93 Conflict: description: | `idempotency_in_progress`, `duplicate_reference`, `price_changed`, `esim_not_cancellable`, `cancel_window_expired`, or an order state that does not allow the action. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} examples: price_changed: summary: Price moved between quote and order; nothing was debited value: type: "urn:choesim:problem:price_changed" title: Price changed status: 409 detail: The total is now different from expected_total_minor. Nothing was debited. instance: /partner/v1/orders code: price_changed request_id: req_7c1e2f94 expected_total_minor: 2520 current_total_minor: 2600 duplicate_reference: summary: Reference already used value: type: "urn:choesim:problem:duplicate_reference" title: Duplicate reference status: 409 detail: An order with this reference already exists. instance: /partner/v1/orders code: duplicate_reference request_id: req_7c1e2f95 order_id: po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10 idempotency_in_progress: summary: Same key still running value: type: "urn:choesim:problem:idempotency_in_progress" title: Request in progress status: 409 detail: A request with this Idempotency-Key is still running. Retry shortly. instance: /partner/v1/orders code: idempotency_in_progress request_id: req_7c1e2f96 InsufficientBalance: description: The wallet balance is lower than the order total. Nothing was debited. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:insufficient_balance" title: Insufficient wallet balance status: 402 detail: The wallet balance is lower than the order total. instance: /partner/v1/orders code: insufficient_balance request_id: req_7c1e2f97 balance_minor: 1000 required_minor: 2520 PayloadTooLarge: description: Body larger than 10 KB. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} UnsupportedMediaType: description: Content-Type must be application/json. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} Unprocessable: description: "Valid JSON that breaks a rule: `invalid_request` (unknown field, out of range, personal data in customer_reference), `plan_unavailable`, `order_limit_exceeded`, `cancel_limit_exceeded`, `idempotency_key_reuse`." content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} examples: validation: summary: Unknown field and a quantity out of range value: type: "urn:choesim:problem:invalid_request" title: Invalid request status: 422 detail: The request body has 2 problems. instance: /partner/v1/orders code: invalid_request request_id: req_7c1e2f98 errors: - {pointer: /price, detail: Unknown field. Prices are computed by ChoEsim.} - {pointer: /items/0/quantity, detail: Must be between 1 and 10.} plan_unavailable: summary: Plan not sellable to this partner value: type: "urn:choesim:problem:plan_unavailable" title: Plan unavailable status: 422 detail: The plan is not available. Nothing was debited. instance: /partner/v1/orders code: plan_unavailable request_id: req_7c1e2f99 order_limit_exceeded: summary: Order above a safety limit (nothing debited) value: type: "urn:choesim:problem:order_limit_exceeded" title: Order limit exceeded status: 422 detail: The order total is above the maximum value allowed per order for this partner. instance: /partner/v1/orders code: order_limit_exceeded request_id: req_7c1e2fa1 limit: max_order cancel_limit_exceeded: summary: Daily cancellation cap reached value: type: "urn:choesim:problem:cancel_limit_exceeded" title: Cancellation limit exceeded status: 422 detail: The daily cancellation cap has been reached. Try again after 00:00 UTC. instance: /partner/v1/orders/po_example/cancel code: cancel_limit_exceeded request_id: req_7c1e2fa2 idempotency_key_reuse: summary: Same key, different body value: type: "urn:choesim:problem:idempotency_key_reuse" title: Idempotency key reused status: 422 detail: This Idempotency-Key was used with a different request. instance: /partner/v1/orders code: idempotency_key_reuse request_id: req_7c1e2f9a personal_data: summary: customer_reference looks like personal data value: type: "urn:choesim:problem:invalid_request" title: Invalid request status: 422 detail: customer_reference must be an opaque id and must not contain an email address or a phone number. instance: /partner/v1/orders code: invalid_request request_id: req_7c1e2f9b errors: - {pointer: /customer_reference, detail: Looks like personal data.} RateLimited: description: Too many requests. Wait `Retry-After` seconds. headers: Retry-After: {$ref: "#/components/headers/RetryAfter"} X-RateLimit-Limit: {$ref: "#/components/headers/RateLimitLimit"} X-RateLimit-Remaining: {$ref: "#/components/headers/RateLimitRemaining"} X-RateLimit-Reset: {$ref: "#/components/headers/RateLimitReset"} content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:rate_limited" title: Too many requests status: 429 detail: Request rate exceeded for this key. instance: /partner/v1/orders code: rate_limited request_id: req_7c1e2f9c retry_after: 12 InternalError: description: Unexpected server error. Nothing is exposed beyond the `request_id`. For `POST /orders` check `GET /orders?reference=...` before retrying; repeating the same request with the same `Idempotency-Key` is always safe. content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:internal_error" title: Internal error status: 500 instance: /partner/v1/orders code: internal_error request_id: req_7c1e2fa0 Unavailable: description: | `orders_disabled` (maintenance or kill switch), `supplier_unavailable` (supplier circuit open) or `supplier_capacity` (not enough supplier balance). In every case nothing was debited; retry after `Retry-After`. headers: Retry-After: {$ref: "#/components/headers/RetryAfter"} content: application/problem+json: schema: {$ref: "#/components/schemas/Problem"} example: type: "urn:choesim:problem:supplier_unavailable" title: Supplier temporarily unavailable status: 503 detail: Orders are paused for a short time. Nothing was debited. instance: /partner/v1/orders code: supplier_unavailable request_id: req_7c1e2f9d retry_after: 60 schemas: Money: type: object additionalProperties: false required: [amount_minor, currency] properties: amount_minor: {type: integer, description: Minor units (USD cents). Never a float.} currency: {type: string, enum: [USD]} ErrorCode: type: string enum: - invalid_request - invalid_api_key - forbidden_scope - ip_not_allowed - partner_suspended - not_found - idempotency_key_required - idempotency_key_reuse - idempotency_in_progress - duplicate_reference - price_changed - esim_not_cancellable - cancel_window_expired - cancel_limit_exceeded - wallet_frozen - insufficient_balance - plan_unavailable - order_limit_exceeded - rate_limited - payload_too_large - unsupported_media_type - orders_disabled - supplier_unavailable - supplier_capacity - internal_error Problem: type: object description: RFC 9457 problem details. Unknown members must be ignored. required: [type, title, status, code, request_id] properties: type: {type: string, description: "URN of the form urn:choesim:problem:."} title: {type: string} status: {type: integer} detail: {type: string} instance: {type: string} code: {$ref: "#/components/schemas/ErrorCode"} request_id: {type: string} retry_after: {type: integer} balance_minor: {type: integer} required_minor: {type: integer} expected_total_minor: {type: integer} current_total_minor: {type: integer} order_id: {type: string} limit: {type: string, enum: [max_order, daily_spend, open_orders], description: Which safety limit was hit (order_limit_exceeded).} errors: type: array items: type: object required: [pointer, detail] properties: pointer: {type: string, description: JSON Pointer into the request body.} detail: {type: string} additionalProperties: true Me: type: object required: [object, livemode, partner_id, key, limits] properties: object: {type: string, const: me} livemode: {type: boolean} partner_id: {type: string} key: type: object required: [prefix, last4, scopes, ip_allowlist] properties: prefix: {type: string} last4: {type: string} scopes: type: array items: {type: string, enum: ["catalog:read", "orders:read", "orders:write", "wallet:read", "webhooks:manage"]} ip_allowlist: type: array items: {type: string} expires_at: {type: [string, "null"], format: date-time} limits: type: object required: [max_order_minor, daily_spend_cap_minor, max_open_orders, daily_cancel_cap, cancel_window_days, max_items_per_order, max_quantity_per_line] properties: daily_cancel_cap: {type: integer, description: Cancellations allowed per UTC day.} cancel_window_days: {type: integer, description: "An order can be cancelled only within this many days of its creation (default 14, adjustable per partner)."} max_order_minor: {type: integer} daily_spend_cap_minor: {type: integer} max_open_orders: {type: integer} max_items_per_order: {type: integer, const: 30} max_quantity_per_line: {type: integer, const: 10} Plan: type: object required: [id, object, name, coverage, volume_bytes, duration, duration_unit, data_type, price] properties: id: {type: string} object: {type: string, const: plan} name: {type: string} coverage: type: array description: ISO 3166-1 alpha-2 country codes, as reported by the supplier. Coverage and speed claims are the supplier's, not a ChoEsim guarantee. items: {type: string, pattern: "^[A-Z]{2}$"} volume_bytes: {type: integer} duration: {type: integer} duration_unit: {type: string, enum: [DAY]} data_type: {type: string, enum: [fixed]} ip_label: {type: [string, "null"]} price: {$ref: "#/components/schemas/Money"} PlanList: type: object required: [object, data, has_more, next_cursor] properties: object: {type: string, const: list} data: type: array items: {$ref: "#/components/schemas/Plan"} has_more: {type: boolean} next_cursor: {type: [string, "null"]} QuoteRequest: type: object additionalProperties: false required: [items] properties: items: type: array minItems: 1 maxItems: 10 items: {$ref: "#/components/schemas/OrderLineInput"} OrderLineInput: type: object additionalProperties: false required: [plan_id, quantity] properties: plan_id: {type: string, maxLength: 100} quantity: {type: integer, minimum: 1, maximum: 10} Quote: type: object required: [object, items, total, sufficient_balance] properties: object: {type: string, const: quote} items: type: array items: type: object required: [plan_id, quantity, unit_price, line_total] properties: plan_id: {type: string} quantity: {type: integer} unit_price: {$ref: "#/components/schemas/Money"} line_total: {$ref: "#/components/schemas/Money"} total: {$ref: "#/components/schemas/Money"} wallet_balance: {$ref: "#/components/schemas/Money"} sufficient_balance: {type: boolean} OrderCreate: type: object additionalProperties: false required: [reference, items] properties: reference: type: string minLength: 1 maxLength: 64 pattern: "^[A-Za-z0-9_:.-]+$" description: Your own unique id for this order. Unique forever per mode; a second order with the same reference is refused. customer_reference: type: string maxLength: 64 description: Opaque id of your end customer. Must not contain personal data. expected_total_minor: type: integer minimum: 0 description: If set and different from the current total, the order is refused with 409 price_changed and nothing is debited. items: type: array minItems: 1 maxItems: 10 description: At most 30 eSIMs in total across lines. items: {$ref: "#/components/schemas/OrderLineInput"} OrderStatus: type: string description: | `accepted` wallet debited, delivery not started; `processing` in progress (see `status_detail`); `fulfilled` eSIMs ready; `failed` proven failure, wallet credited; `cancelling` cancellation under way; `cancelled` cancelled and wallet credited. enum: [accepted, processing, fulfilled, failed, cancelling, cancelled] OrderItem: type: object required: [plan_id, name, quantity, unit_price] properties: plan_id: {type: string} name: {type: string} quantity: {type: integer} unit_price: {$ref: "#/components/schemas/Money"} Order: type: object required: [id, object, livemode, reference, status, items, total, created_at, updated_at] properties: id: {type: string} object: {type: string, const: order} livemode: {type: boolean} reference: {type: string} customer_reference: {type: [string, "null"]} status: {$ref: "#/components/schemas/OrderStatus"} status_detail: type: [string, "null"] description: "`under_review` (outcome being checked by staff), `awaiting_credentials` (bought, activation data not yet stored) or null." enum: [under_review, awaiting_credentials, null] items: type: array items: {$ref: "#/components/schemas/OrderItem"} total: {$ref: "#/components/schemas/Money"} esims: type: array description: Present on single-order reads and in create responses; contains activation secrets. items: {$ref: "#/components/schemas/Esim"} failure_code: type: [string, "null"] description: Set when status is failed. refund: type: - object - "null" required: [amount_minor, currency, credited_at] properties: amount_minor: {type: integer} currency: {type: string, enum: [USD]} credited_at: {type: string, format: date-time} created_at: {type: string, format: date-time} updated_at: {type: string, format: date-time} OrderList: type: object required: [object, data, has_more, next_cursor] properties: object: {type: string, const: list} data: type: array items: {$ref: "#/components/schemas/Order"} has_more: {type: boolean} next_cursor: {type: [string, "null"]} Esim: type: object required: [id, object, order_id, iccid, status, lpa] properties: id: {type: string} object: {type: string, const: esim} order_id: {type: string} iccid: {type: string} status: {type: string, description: "Last supplier-reported status, for example GOT_RESOURCE, IN_USE, USED_UP, CANCEL."} lpa: type: string description: Activation string (starts with `LPA:1$`). A secret; build your own QR code from it. ChoEsim does not return the supplier's QR image URL. EsimUsage: type: object required: [object, esim_id, updated_at] properties: object: {type: string, const: esim_usage} esim_id: {type: string} esim_status: {type: [string, "null"]} total_bytes: {type: [integer, "null"]} used_bytes: {type: [integer, "null"]} remaining_bytes: {type: [integer, "null"]} remaining_days: {type: [integer, "null"]} expires_at: {type: [string, "null"], format: date-time} updated_at: {type: [string, "null"], format: date-time} Wallet: type: object required: [object, livemode, currency, balance_minor, status, as_of] properties: object: {type: string, const: wallet} livemode: {type: boolean} currency: {type: string, enum: [USD]} balance_minor: {type: integer, minimum: 0} status: {type: string, enum: [active, frozen]} as_of: {type: string, format: date-time} WalletEntry: type: object required: [id, object, seq, kind, amount_minor, currency, balance_after_minor, created_at] properties: id: {type: string} object: {type: string, const: wallet_entry} seq: {type: integer} kind: {type: string, enum: [topup, debit_order, credit_refund, adjustment, reversal]} amount_minor: {type: integer, description: Signed. Debits are negative.} currency: {type: string, enum: [USD]} balance_after_minor: {type: integer, minimum: 0} order_id: {type: [string, "null"]} ref: {type: [string, "null"]} created_at: {type: string, format: date-time} WalletEntryList: type: object required: [object, data, has_more, next_cursor] properties: object: {type: string, const: list} data: type: array items: {$ref: "#/components/schemas/WalletEntry"} has_more: {type: boolean} next_cursor: {type: [string, "null"]} EventType: type: string enum: [order.fulfilled, order.failed, order.cancelled, order.delayed, esim.status_changed, wallet.credited, wallet.low_balance, ping] EventBase: type: object required: [id, object, type, api_version, livemode, created, data] properties: id: {type: string, description: Same value as the `webhook-id` header.} object: {type: string, const: event} type: {$ref: "#/components/schemas/EventType"} api_version: {type: string, const: v1} livemode: {type: boolean} created: {type: string, format: date-time} EventOrder: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: {type: string, enum: [order.fulfilled, order.failed, order.cancelled, order.delayed]} data: type: object required: [order_id, reference, status] properties: order_id: {type: string} reference: {type: string} status: {$ref: "#/components/schemas/OrderStatus"} failure_code: {type: string} EventEsim: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: {type: string, const: esim.status_changed} data: type: object required: [esim_id, order_id, esim_status] properties: esim_id: {type: string} order_id: {type: string} esim_status: {type: string} EventWallet: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: {type: string, enum: [wallet.credited, wallet.low_balance]} data: type: object required: [balance_minor, currency] properties: balance_minor: {type: integer} currency: {type: string, enum: [USD]} entry_id: {type: string} EventPing: allOf: - $ref: "#/components/schemas/EventBase" - type: object properties: type: {type: string, const: ping} data: type: object additionalProperties: false required: [endpoint_id] properties: endpoint_id: {type: string} Event: description: One event. Select the schema with `type` (discriminator); each `type` value belongs to exactly one schema. oneOf: - $ref: "#/components/schemas/EventOrder" - $ref: "#/components/schemas/EventEsim" - $ref: "#/components/schemas/EventWallet" - $ref: "#/components/schemas/EventPing" discriminator: propertyName: type mapping: order.fulfilled: "#/components/schemas/EventOrder" order.failed: "#/components/schemas/EventOrder" order.cancelled: "#/components/schemas/EventOrder" order.delayed: "#/components/schemas/EventOrder" esim.status_changed: "#/components/schemas/EventEsim" wallet.credited: "#/components/schemas/EventWallet" wallet.low_balance: "#/components/schemas/EventWallet" ping: "#/components/schemas/EventPing" EventList: type: object required: [object, data, has_more, next_cursor] properties: object: {type: string, const: list} data: type: array items: {$ref: "#/components/schemas/Event"} has_more: {type: boolean} next_cursor: {type: [string, "null"]} WebhookEndpointCreate: type: object additionalProperties: false required: [url] properties: url: {type: string, format: uri, maxLength: 500, pattern: "^https://"} enabled_events: type: array default: ["*"] items: anyOf: - $ref: "#/components/schemas/EventType" - type: string const: "*" WebhookEndpoint: type: object required: [id, object, livemode, url, enabled_events, status, created_at] properties: id: {type: string} object: {type: string, const: webhook_endpoint} livemode: {type: boolean} url: {type: string} enabled_events: type: array items: {type: string} status: {type: string, enum: [active, disabled]} disabled_reason: {type: [string, "null"]} last_success_at: {type: [string, "null"], format: date-time} created_at: {type: string, format: date-time} WebhookEndpointWithSecret: allOf: - $ref: "#/components/schemas/WebhookEndpoint" - type: object required: [secret] properties: secret: {type: string, description: Shown once. Store it in your secret manager.}