# eSIMify Partners API v1 - OpenAPI 3.1. All example values are fake.
openapi: 3.1.0
info:
  title: eSIMify Partners API
  version: 1.0.0
  summary: Sell travel eSIMs from your own systems.
  description: |-
    Server-to-server API for travel agents and corporates: browse packages, order eSIMs, top them up, track usage and receive webhooks.

    **Preview.** This describes v1 ahead of launch.

    ## Conventions
    - **Base URL**: shown in your Business portal → Developers. It ends in `/partner/api`. All paths below start with `/v1`.
    - **Authentication**: `Authorization: Bearer <key>`. `sk_test_…` keys use the sandbox, `sk_live_…` keys place real orders. Keep keys on your server.
    - **Format**: JSON, UTF-8, `snake_case` fields. Unknown request fields are ignored. A request body can be at most 64 KB. Text must not contain control characters; lengths are counted in characters.
    - **Money**: `{ "amount": "249.00", "currency": "INR" }`; `amount` is a decimal string with 2 decimal places.
    - **Timestamps**: ISO 8601 UTC, e.g. `2026-10-02T09:14:07Z`.
    - **Lists**: `{ "data": [...], "has_more": true, "next_cursor": "…" }`. Pass `limit` (1–100, default 25) and `cursor`. A cursor only works on the list and the kind of key it came from.
    - **Errors**: HTTP status plus `{ "error": { "code", "message", "request_id", "param"? } }`. Branch on `code`.
    - **A `2xx` is not always a success**: an order or a top-up can be created with the status `failed` (the amount is returned). Check `status` in the body.
    - **Request ids**: every response carries `X-Request-Id`.
    - **Idempotency**: every `POST` needs an `Idempotency-Key` header (1–255 characters). Only `2xx` responses are remembered, so the same key replays a `failed` order too: use a new key to try again. After a `4xx` or `5xx` the same key runs the request again.
    - **Rate limits**: 120 requests per minute per API key, 30 per minute for `POST`. Every response to a recognised key carries the `X-RateLimit-*` headers; a `429` adds `Retry-After`. A `401` carries none.
    - **White-label**: eSIMify sends no email or message to your customer, or to you, for API orders and top-ups.
    - **Sandbox**: a test key gets a virtual balance of 1000000.00 INR that refills when it drops below 10000.00.

    All values in the examples are fake.
servers:
- url: '{baseUrl}'
  description: Your eSIMify Partners API base URL.
  variables:
    baseUrl:
      default: https://api.example.com/partner/api
      description: the base URL shown in your Business portal → Developers
security:
- bearerAuth: []
tags:
- name: Catalogue
  description: Countries, regions and the packages you can sell.
- name: Balance
  description: Your prepaid balance.
- name: Orders
  description: Buy eSIMs.
- name: eSIMs
  description: Installation details, status and data usage.
- name: Top-ups
  description: Add data to an eSIM the customer already has, and read the top-ups of an eSIM.
- name: Events
  description: Everything that was recorded on your account in the last 30 days. Live `esim.*` events are recorded
    only while you have an enabled live webhook endpoint.
- name: Sandbox
  description: Test-key-only helpers.
- name: QR codes
  description: Public QR code images.
- name: Webhooks
  description: 'Events eSIMify sends to your HTTPS endpoints. Each request is a `POST` with `Content-Type: application/json`
    whose body is an Event. An endpoint gets one request at a time, in `sequence` order; the first attempt is made
    at once.'
paths:
  /v1/countries:
    get:
      operationId: listCountries
      tags:
      - Catalogue
      summary: List countries and regions
      description: Every country and region that currently has at least one package, with the number of packages
        for each. Countries first, then regions. Not paginated.
      responses:
        '200':
          description: The destinations.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountryList'
              example:
                data:
                - code: TH
                  name: Thailand
                  type: country
                  packages_count: 12
                - code: AE
                  name: United Arab Emirates
                  type: country
                  packages_count: 9
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/packages:
    get:
      operationId: listPackages
      tags:
      - Catalogue
      summary: List packages
      description: |-
        The packages you can order, with your price. The catalogue and prices are the same for test and live keys. Results are paginated: pass `limit` (1–100, default 25) and the previous page's `next_cursor` as `cursor`.

        The list shows **one** package per destination, data size and validity: the one eSIMify sells in its own store, then the one with the lowest price for you. The `id` behind such a slot can change; an `id` you stored earlier can still be fetched and ordered while that package is on sale, and answers `404 package_not_found` once it is retired. Refresh your copy at least daily.

        `price` is the retail price less your partner discount, never above `retail_price`. An order is charged at the price at the moment it is placed.
      parameters:
      - name: country
        in: query
        required: false
        description: ISO 3166-1 alpha-2 country code, upper or lower case. Returns the packages that work in that
          country, regional and global ones included. An unknown code returns an empty list.
        schema:
          type: string
          pattern: ^[A-Za-z]{2}$
        example: TH
      - name: region
        in: query
        required: false
        description: A region `code` from `GET /v1/countries` (`region-<number>`). The older name-based codes are
          still accepted.
        schema:
          type: string
      - name: unlimited
        in: query
        required: false
        description: '`true` returns unlimited packages only, `false` metered packages only.'
        schema:
          type: boolean
      - name: q
        in: query
        required: false
        description: Case-insensitive search in the package and destination name.
        schema:
          type: string
          maxLength: 100
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of packages.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PackageList'
              example:
                data:
                - id: pkg_10482
                  name: Thailand 5 GB 15 Days
                  type: country
                  country: TH
                  countries:
                  - TH
                  data_mb: 5120
                  unlimited: false
                  validity_days: 15
                  price:
                    amount: '249.00'
                    currency: INR
                  retail_price:
                    amount: '349.00'
                    currency: INR
                  topup: true
                - id: pkg_10519
                  name: Thailand Unlimited 7 Days
                  type: country
                  country: TH
                  countries:
                  - TH
                  data_mb: null
                  unlimited: true
                  validity_days: 7
                  price:
                    amount: '599.00'
                    currency: INR
                  retail_price:
                    amount: '799.00'
                    currency: INR
                  topup: false
                has_more: true
                next_cursor: MTA1MTk
        '400':
          description: A query parameter is invalid.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: limit must be between 1 and 100.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: limit
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/packages/{id}:
    get:
      operationId: getPackage
      tags:
      - Catalogue
      summary: Retrieve a package
      description: One package by id. A `200` means the package can be ordered now.
      parameters:
      - name: id
        in: path
        required: true
        description: Package id, `pkg_<number>`.
        schema:
          type: string
        example: pkg_10482
      responses:
        '200':
          description: The package.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Package'
              example:
                id: pkg_10482
                name: Thailand 5 GB 15 Days
                type: country
                country: TH
                countries:
                - TH
                data_mb: 5120
                unlimited: false
                validity_days: 15
                price:
                  amount: '249.00'
                  currency: INR
                retail_price:
                  amount: '349.00'
                  currency: INR
                topup: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No package with this id.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                package_not_found:
                  summary: Unknown package id
                  value:
                    error:
                      code: package_not_found
                      message: No such package.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/balance:
    get:
      operationId: getBalance
      tags:
      - Balance
      summary: Retrieve your balance
      description: 'Your prepaid balance for the key''s mode. With a test key this is the virtual sandbox balance:
        it starts at 1000000.00 INR, is debited by sandbox orders and top-ups, and refills to 1000000.00 automatically
        when it drops below 10000.00.'
      responses:
        '200':
          description: The balance.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Balance'
              example:
                balance:
                  amount: '999751.00'
                  currency: INR
                low_balance_threshold: null
                mode: test
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/orders:
    post:
      operationId: createOrder
      tags:
      - Orders
      summary: Create an order
      description: |-
        Buys `quantity` eSIMs of one package and debits your balance. Returns `201` for a new order and `200` (with `Idempotent-Replayed: true`) when the same `Idempotency-Key` and body are sent again.

        **Check `status`.** The call waits up to about 20 seconds for the eSIMs (use a client timeout of at least 30 seconds). An order that is not finished by then comes back as `processing`; wait for the `order.completed` / `order.failed` webhook or poll `GET /v1/orders/{id}`. In live, a supplier failure is answered with `201` and an order whose status is `failed` or `partially_completed` (the missing units are refunded), not with an error. To try again after a `failed` order use a NEW `Idempotency-Key`: the old key replays the failed order.

        eSIMify sends no email or message to your customer, or to you, for API orders.

        With a test key the order completes immediately with synthetic eSIMs. `customer_ref` prefixes trigger other outcomes: `test_insufficient_balance` → `402`, `test_order_failed` → a `failed` order, `test_processing` → `processing` then completed 10 to 25 seconds later, `test_supplier_down` → `503`.
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreateRequest'
            example:
              package_id: pkg_10482
              quantity: 1
              customer_ref: booking-78231
              customer:
                name: Test Traveller
                email: traveller@example.com
      responses:
        '200':
          description: 'Replay of an earlier request with the same `Idempotency-Key` and body: the first response,
            exactly as it was sent (a live order first answered as `processing` is returned as it is now).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Idempotent-Replayed:
              $ref: '#/components/headers/Idempotent-Replayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              example:
                id: ord_test_3f9a1c07b2e4
                status: completed
                mode: test
                package_id: pkg_10482
                quantity: 1
                items:
                - package_id: pkg_10482
                  quantity: 1
                customer_ref: booking-78231
                total:
                  amount: '249.00'
                  currency: INR
                refunded:
                  amount: '0.00'
                  currency: INR
                balance_after:
                  amount: '999751.00'
                  currency: INR
                created: '2026-10-02T09:14:07Z'
                esims:
                - iccid: '8999999000000012345'
                  status: ready
        '201':
          description: The order was created. Its `status` can be `completed`, `processing`, `partially_completed`
            or `failed`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              examples:
                completed:
                  summary: Completed immediately
                  value:
                    id: ord_test_3f9a1c07b2e4
                    status: completed
                    mode: test
                    package_id: pkg_10482
                    quantity: 1
                    items:
                    - package_id: pkg_10482
                      quantity: 1
                    customer_ref: booking-78231
                    total:
                      amount: '249.00'
                      currency: INR
                    refunded:
                      amount: '0.00'
                      currency: INR
                    balance_after:
                      amount: '999751.00'
                      currency: INR
                    created: '2026-10-02T09:14:07Z'
                    esims:
                    - iccid: '8999999000000012345'
                      status: ready
                processing:
                  summary: Still being fulfilled
                  value:
                    id: ord_test_a41b7c9d0e2f
                    status: processing
                    mode: test
                    package_id: pkg_10482
                    quantity: 1
                    items:
                    - package_id: pkg_10482
                      quantity: 1
                    customer_ref: test_processing-001
                    total:
                      amount: '249.00'
                      currency: INR
                    refunded:
                      amount: '0.00'
                      currency: INR
                    balance_after:
                      amount: '999751.00'
                      currency: INR
                    created: '2026-10-02T09:14:07Z'
                    esims: []
                failed:
                  summary: Failed (amount refunded)
                  value:
                    id: ord_test_9d8c7b6a5f4e
                    status: failed
                    mode: test
                    package_id: pkg_10482
                    quantity: 1
                    items:
                    - package_id: pkg_10482
                      quantity: 1
                    customer_ref: test_order_failed-001
                    total:
                      amount: '249.00'
                      currency: INR
                    refunded:
                      amount: '249.00'
                      currency: INR
                    balance_after:
                      amount: '1000000.00'
                      currency: INR
                    created: '2026-10-02T09:14:07Z'
                    esims: []
        '400':
          description: 'The request is not valid: a missing or invalid field (`param` names it), a missing or over-long
            `Idempotency-Key`, text with control characters, a body that is not a JSON object, or a body over 64
            KB (refused before the key is checked, so without `X-RateLimit-*` headers).'
          headers:
            X-Request-Id: &id001
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit: &id002
              $ref: '#/components/headers/X-RateLimit-Limit-IfKeyKnown'
            X-RateLimit-Remaining: &id003
              $ref: '#/components/headers/X-RateLimit-Remaining-IfKeyKnown'
            X-RateLimit-Reset: &id004
              $ref: '#/components/headers/X-RateLimit-Reset-IfKeyKnown'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: quantity must be between 1 and 50.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: quantity
                idempotency_key_required:
                  summary: Idempotency-Key header missing
                  value:
                    error:
                      code: idempotency_key_required
                      message: The Idempotency-Key header is required on POST requests.
                      request_id: req_8f3a1c9d2b7e4a60
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Your balance does not cover the order. Nothing was debited. Add funds and send the same request
            again with the same `Idempotency-Key`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                insufficient_balance:
                  summary: Balance too low
                  value:
                    error:
                      code: insufficient_balance
                      message: Your balance is too low for this purchase.
                      request_id: req_8f3a1c9d2b7e4a60
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No package you can sell has this id.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                package_not_found:
                  summary: Unknown package id
                  value:
                    error:
                      code: package_not_found
                      message: No such package.
                      request_id: req_8f3a1c9d2b7e4a60
        '409':
          description: Idempotency conflict.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                idempotency_key_reused:
                  summary: Same key, different body
                  value:
                    error:
                      code: idempotency_key_reused
                      message: This Idempotency-Key was already used with a different request body.
                      request_id: req_8f3a1c9d2b7e4a60
                request_in_progress:
                  summary: Original request still running
                  value:
                    error:
                      code: request_in_progress
                      message: A request with this Idempotency-Key is still being processed. Retry shortly.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: 'Sandbox only: returned for the `test_supplier_down` trigger. The live API does not return
            it today. Retry with the same `Idempotency-Key`.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                supplier_unavailable:
                  summary: Upstream temporarily unavailable
                  value:
                    error:
                      code: supplier_unavailable
                      message: The eSIM could not be issued right now. Please retry shortly.
                      request_id: req_8f3a1c9d2b7e4a60
    get:
      operationId: listOrders
      tags:
      - Orders
      summary: List orders
      description: 'Your orders, newest first; top-ups are not included. Orders placed by your staff in the Business
        portal are listed too. Results are paginated: pass `limit` (1–100, default 25) and the previous page''s
        `next_cursor` as `cursor`.'
      parameters:
      - name: status
        in: query
        required: false
        description: Only orders with this status.
        schema:
          $ref: '#/components/schemas/OrderStatus'
      - name: customer_ref
        in: query
        required: false
        description: Orders with exactly this `customer_ref` (case-sensitive).
        schema:
          type: string
          maxLength: 120
        example: booking-78231
      - name: created_after
        in: query
        required: false
        description: ISO 8601 date or timestamp. Orders created at or after it (inclusive). No zone = UTC.
        schema:
          type: string
          format: date-time
        example: '2026-10-01T00:00:00Z'
      - name: created_before
        in: query
        required: false
        description: ISO 8601 date or timestamp. Orders created before it (exclusive). No zone = UTC.
        schema:
          type: string
          format: date-time
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of orders.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'
              example:
                data:
                - id: ord_test_3f9a1c07b2e4
                  status: completed
                  mode: test
                  package_id: pkg_10482
                  quantity: 1
                  items:
                  - package_id: pkg_10482
                    quantity: 1
                  customer_ref: booking-78231
                  total:
                    amount: '249.00'
                    currency: INR
                  refunded:
                    amount: '0.00'
                    currency: INR
                  balance_after:
                    amount: '999751.00'
                    currency: INR
                  created: '2026-10-02T09:14:07Z'
                  esims:
                  - iccid: '8999999000000012345'
                    status: ready
                has_more: false
                next_cursor: null
        '400':
          description: A query parameter is invalid.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: limit must be between 1 and 100.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: limit
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/orders/{id}:
    get:
      operationId: getOrder
      tags:
      - Orders
      summary: Retrieve an order
      description: One order by id, with the eSIMs issued so far.
      parameters:
      - name: id
        in: path
        required: true
        description: Order id, `ord_…`.
        schema:
          type: string
        example: ord_test_3f9a1c07b2e4
      responses:
        '200':
          description: The order.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              example:
                id: ord_test_3f9a1c07b2e4
                status: completed
                mode: test
                package_id: pkg_10482
                quantity: 1
                items:
                - package_id: pkg_10482
                  quantity: 1
                customer_ref: booking-78231
                total:
                  amount: '249.00'
                  currency: INR
                refunded:
                  amount: '0.00'
                  currency: INR
                balance_after:
                  amount: '999751.00'
                  currency: INR
                created: '2026-10-02T09:14:07Z'
                esims:
                - iccid: '8999999000000012345'
                  status: ready
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No order with this id in this mode.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                order_not_found:
                  summary: Unknown order id
                  value:
                    error:
                      code: order_not_found
                      message: No such order.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/esims:
    get:
      operationId: listEsims
      tags:
      - eSIMs
      summary: List eSIMs
      description: 'The eSIMs issued to you, newest first, in summary form (no `activation` block; retrieve a single
        eSIM for that). Results are paginated: pass `limit` (1–100, default 25) and the previous page''s `next_cursor`
        as `cursor`.'
      parameters:
      - name: status
        in: query
        required: false
        description: Only eSIMs with this status.
        schema:
          $ref: '#/components/schemas/EsimStatus'
      - name: customer_ref
        in: query
        required: false
        description: Only eSIMs from orders created with this `customer_ref`.
        schema:
          type: string
          maxLength: 120
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of eSIMs.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EsimList'
              example:
                data:
                - iccid: '8999999000000012345'
                  status: ready
                  mode: test
                  order_id: ord_test_3f9a1c07b2e4
                  customer_ref: booking-78231
                  package:
                    id: pkg_10482
                    name: Thailand 5 GB 15 Days
                  qr_code_url: https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png
                  install_links:
                    ios: https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                    android: https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                  data:
                    unlimited: false
                    total_mb: 5120
                    used_mb: 0
                    remaining_mb: 5120
                  validity_days: 15
                  activated_at: null
                  expires_at: null
                  created: '2026-10-02T09:14:07Z'
                has_more: false
                next_cursor: null
        '400':
          description: A query parameter is invalid.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: limit must be between 1 and 100.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: limit
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/esims/{iccid}:
    get:
      operationId: getEsim
      tags:
      - eSIMs
      summary: Retrieve an eSIM
      description: 'The full eSIM: status, data usage, validity and everything needed to install it (QR code URL,
        LPA string, manual-entry details, one-tap install links). For a live eSIM this asks the mobile network for
        fresh usage, at most once a minute; usage still arrives with a delay. A completed top-up shows at once in
        `data`, `validity_days` and `expires_at`.'
      parameters:
      - $ref: '#/components/parameters/Iccid'
      responses:
        '200':
          description: The eSIM.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Esim'
              examples:
                ready:
                  summary: Issued, not yet activated
                  value:
                    iccid: '8999999000000012345'
                    status: ready
                    mode: test
                    order_id: ord_test_3f9a1c07b2e4
                    customer_ref: booking-78231
                    package:
                      id: pkg_10482
                      name: Thailand 5 GB 15 Days
                    qr_code_url: https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png
                    activation:
                      lpa: LPA:1$sandbox.esimify.in$TEST-7K2M9QX4B1TZ
                      smdp_address: sandbox.esimify.in
                      activation_code: TEST-7K2M9QX4B1TZ
                    install_links:
                      ios: https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                      android: https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                    data:
                      unlimited: false
                      total_mb: 5120
                      used_mb: 0
                      remaining_mb: 5120
                    validity_days: 15
                    activated_at: null
                    expires_at: null
                    created: '2026-10-02T09:14:07Z'
                active:
                  summary: In use
                  value:
                    iccid: '8999999000000012345'
                    status: active
                    mode: test
                    order_id: ord_test_3f9a1c07b2e4
                    customer_ref: booking-78231
                    package:
                      id: pkg_10482
                      name: Thailand 5 GB 15 Days
                    qr_code_url: https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png
                    activation:
                      lpa: LPA:1$sandbox.esimify.in$TEST-7K2M9QX4B1TZ
                      smdp_address: sandbox.esimify.in
                      activation_code: TEST-7K2M9QX4B1TZ
                    install_links:
                      ios: https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                      android: https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                    data:
                      unlimited: false
                      total_mb: 5120
                      used_mb: 4198
                      remaining_mb: 922
                    validity_days: 15
                    activated_at: '2026-10-02T09:20:41Z'
                    expires_at: '2026-10-17T09:20:41Z'
                    created: '2026-10-02T09:14:07Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No eSIM with this ICCID in this mode.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_found:
                  summary: Unknown ICCID
                  value:
                    error:
                      code: esim_not_found
                      message: No such eSIM.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/esims/{iccid}/topup-packages:
    get:
      operationId: listTopupPackages
      tags:
      - Top-ups
      summary: List top-up packages for an eSIM
      description: The packages that can be added to this eSIM as a top-up, with your price. Not paginated (at most
        200). An empty list means there is nothing to offer.
      parameters:
      - $ref: '#/components/parameters/Iccid'
      responses:
        '200':
          description: The top-up packages.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TopupPackageList'
              example:
                data:
                - id: pkg_10483
                  name: Thailand 3 GB 7 Days
                  type: country
                  country: TH
                  countries:
                  - TH
                  data_mb: 3072
                  unlimited: false
                  validity_days: 7
                  price:
                    amount: '159.00'
                    currency: INR
                  retail_price:
                    amount: '229.00'
                    currency: INR
                  topup: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No eSIM with this ICCID in this mode.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_found:
                  summary: Unknown ICCID
                  value:
                    error:
                      code: esim_not_found
                      message: No such eSIM.
                      request_id: req_8f3a1c9d2b7e4a60
        '409':
          description: The eSIM is expired or cancelled and cannot be topped up.
          headers:
            X-Request-Id: &id005
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit: &id006
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining: &id007
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset: &id008
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_topupable:
                  summary: Expired or cancelled eSIM
                  value:
                    error:
                      code: esim_not_topupable
                      message: This eSIM is expired and cannot be topped up.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/esims/{iccid}/topups:
    post:
      operationId: createTopup
      tags:
      - Top-ups
      summary: Top up an eSIM
      description: |-
        Adds a package to the eSIM and debits your balance. Returns `201` for a new top-up and `200` (with `Idempotent-Replayed: true`) for a replay.

        **Check `status`.** A top-up the mobile network provider does not apply is answered with `201` and `status: "failed"`: nothing was added and the amount was returned. To try again use a NEW `Idempotency-Key`.

        A `topup.completed` event is sent for a completed top-up; a failed one sends no event. After a completed top-up `esim.usage_80` / `esim.usage_100` can be sent again. eSIMify sends nothing to your customer.

        With a test key the `customer_ref` prefixes work as for orders: `test_insufficient_balance` → `402`, `test_supplier_down` → `503`, `test_order_failed` → a `failed` top-up.
      parameters:
      - $ref: '#/components/parameters/Iccid'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TopupCreateRequest'
            example:
              package_id: pkg_10483
              customer_ref: booking-78231
      responses:
        '200':
          description: 'Replay of an earlier request with the same `Idempotency-Key` and body: the original response.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Idempotent-Replayed:
              $ref: '#/components/headers/Idempotent-Replayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Topup'
              example:
                id: top_test_5b1e9c2a7d40
                status: completed
                iccid: '8999999000000012345'
                package_id: pkg_10483
                customer_ref: booking-78231
                total:
                  amount: '159.00'
                  currency: INR
                balance_after:
                  amount: '999592.00'
                  currency: INR
                created: '2026-10-02T09:31:55Z'
        '201':
          description: The top-up was created. Its `status` can be `completed`, `processing` or `failed`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Topup'
              example:
                id: top_test_5b1e9c2a7d40
                status: completed
                iccid: '8999999000000012345'
                package_id: pkg_10483
                customer_ref: booking-78231
                total:
                  amount: '159.00'
                  currency: INR
                balance_after:
                  amount: '999592.00'
                  currency: INR
                created: '2026-10-02T09:31:55Z'
        '400':
          description: 'The request is not valid: `package_id` missing, `customer_ref` too long or with control
            characters, a missing or over-long `Idempotency-Key`, a body that is not a JSON object, or a body over
            64 KB.'
          headers:
            X-Request-Id: *id001
            X-RateLimit-Limit: *id002
            X-RateLimit-Remaining: *id003
            X-RateLimit-Reset: *id004
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: package_id is required.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: package_id
                idempotency_key_required:
                  summary: Idempotency-Key header missing
                  value:
                    error:
                      code: idempotency_key_required
                      message: The Idempotency-Key header is required on POST requests.
                      request_id: req_8f3a1c9d2b7e4a60
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Your balance does not cover the top-up. Nothing was debited.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                insufficient_balance:
                  summary: Balance too low
                  value:
                    error:
                      code: insufficient_balance
                      message: Your balance is too low for this purchase.
                      request_id: req_8f3a1c9d2b7e4a60
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: The eSIM does not exist in this mode, or `package_id` is not one of this eSIM's top-up packages
            (an id that does not exist, or a real package this eSIM cannot take).
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_found:
                  summary: Unknown ICCID
                  value:
                    error:
                      code: esim_not_found
                      message: No such eSIM.
                      request_id: req_8f3a1c9d2b7e4a60
                package_not_found:
                  summary: Not a top-up package of this eSIM
                  value:
                    error:
                      code: package_not_found
                      message: No such top-up package for this eSIM. See GET /v1/esims/{iccid}/topup-packages.
                      request_id: req_8f3a1c9d2b7e4a60
        '409':
          description: The eSIM cannot be topped up (expired, cancelled or being cancelled), or an idempotency conflict.
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_topupable:
                  summary: Expired or cancelled eSIM
                  value:
                    error:
                      code: esim_not_topupable
                      message: This eSIM is expired and cannot be topped up.
                      request_id: req_8f3a1c9d2b7e4a60
                idempotency_key_reused:
                  summary: Same key, different body
                  value:
                    error:
                      code: idempotency_key_reused
                      message: This Idempotency-Key was already used with a different request. Use a new key for
                        a new request.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: Idempotency-Key
                request_in_progress:
                  summary: Original request still running
                  value:
                    error:
                      code: request_in_progress
                      message: A request with this Idempotency-Key is still being processed. Retry in a few seconds.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: Idempotency-Key
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: 'Sandbox only: returned for the `test_supplier_down` trigger. The live API does not return
            it today.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                supplier_unavailable:
                  summary: Upstream temporarily unavailable
                  value:
                    error:
                      code: supplier_unavailable
                      message: The eSIM could not be issued right now. Please retry shortly.
                      request_id: req_8f3a1c9d2b7e4a60
    get:
      operationId: listTopups
      tags:
      - Top-ups
      summary: List the top-ups of an eSIM
      description: 'Every top-up of one eSIM, newest first, whatever its status. Use it to reconcile a top-up whose
        response you lost. Results are paginated: pass `limit` (1–100, default 25) and the previous page''s `next_cursor`
        as `cursor`.'
      parameters:
      - $ref: '#/components/parameters/Iccid'
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The top-ups.
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TopupList'
              example:
                data:
                - id: top_test_5b1e9c2a7d40
                  status: completed
                  iccid: '8999999000000012345'
                  package_id: pkg_10483
                  customer_ref: booking-78231
                  total:
                    amount: '159.00'
                    currency: INR
                  balance_after:
                    amount: '999592.00'
                    currency: INR
                  created: '2026-10-02T09:31:55Z'
                has_more: false
                next_cursor: null
        '400':
          description: '`limit` or `cursor` is not valid.'
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: Invalid query parameter
                  value:
                    error:
                      code: invalid_request
                      message: limit must be between 1 and 100.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: limit
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No eSIM with this ICCID in this mode.
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_found:
                  summary: Unknown ICCID
                  value:
                    error:
                      code: esim_not_found
                      message: No such eSIM.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/topups/{id}:
    get:
      operationId: getTopup
      tags:
      - Top-ups
      summary: Retrieve a top-up
      description: One top-up by id. A live key cannot see sandbox top-ups and a test key cannot see live ones.
      parameters:
      - name: id
        in: path
        required: true
        description: 'The top-up id, `top_…` (sandbox: `top_test_<12 hex>`).'
        schema:
          type: string
        example: top_test_5b1e9c2a7d40
      responses:
        '200':
          description: The top-up.
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Topup'
              example:
                id: top_test_5b1e9c2a7d40
                status: completed
                iccid: '8999999000000012345'
                package_id: pkg_10483
                customer_ref: booking-78231
                total:
                  amount: '159.00'
                  currency: INR
                balance_after:
                  amount: '999592.00'
                  currency: INR
                created: '2026-10-02T09:31:55Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No top-up with this id in this mode.
          headers:
            X-Request-Id: *id005
            X-RateLimit-Limit: *id006
            X-RateLimit-Remaining: *id007
            X-RateLimit-Reset: *id008
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                topup_not_found:
                  summary: Unknown top-up id
                  value:
                    error:
                      code: topup_not_found
                      message: No such top-up.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/events:
    get:
      operationId: listEvents
      tags:
      - Events
      summary: List events
      description: 'The events recorded for the key''s mode in the last 30 days, newest first. Order, top-up and
        balance events are always recorded. Live `esim.*` events are recorded only while the account has an enabled
        live webhook endpoint, and only for changes seen in the last 72 hours. `ping` is never stored. Results are
        paginated: pass `limit` (1–100, default 25) and the previous page''s `next_cursor` as `cursor`.'
      parameters:
      - name: type
        in: query
        required: false
        description: Only events of this type.
        schema:
          $ref: '#/components/schemas/StoredEventType'
        example: order.completed
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of events.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventList'
              example:
                data:
                - id: evt_0a1b2c3d4e5f60718293a4b5
                  type: esim.usage_80
                  mode: test
                  created: '2026-10-02T09:40:12Z'
                  sequence: 41
                  data:
                    iccid: '8999999000000012345'
                    status: active
                    order_id: ord_test_3f9a1c07b2e4
                    customer_ref: booking-78231
                    data:
                      unlimited: false
                      total_mb: 5120
                      used_mb: 4198
                      remaining_mb: 922
                    expires_at: '2026-10-17T09:20:41Z'
                - id: evt_5c1d9e7a3b2f4c6d8e0a1b2c
                  type: order.completed
                  mode: test
                  created: '2026-10-02T09:14:07Z'
                  sequence: 42
                  data:
                    id: ord_test_3f9a1c07b2e4
                    status: completed
                    mode: test
                    package_id: pkg_10482
                    quantity: 1
                    items:
                    - package_id: pkg_10482
                      quantity: 1
                    customer_ref: booking-78231
                    total:
                      amount: '249.00'
                      currency: INR
                    refunded:
                      amount: '0.00'
                      currency: INR
                    balance_after:
                      amount: '999751.00'
                      currency: INR
                    created: '2026-10-02T09:14:07Z'
                    esims:
                    - iccid: '8999999000000012345'
                      status: ready
                has_more: false
                next_cursor: null
        '400':
          description: A query parameter is invalid.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: limit must be between 1 and 100.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: limit
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/events/{id}:
    get:
      operationId: getEvent
      tags:
      - Events
      summary: Retrieve an event
      description: One event by id.
      parameters:
      - name: id
        in: path
        required: true
        description: Event id, `evt_<24 hex>`.
        schema:
          type: string
        example: evt_5c1d9e7a3b2f4c6d8e0a1b2c
      responses:
        '200':
          description: The event.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Event'
              example:
                id: evt_5c1d9e7a3b2f4c6d8e0a1b2c
                type: order.completed
                mode: test
                created: '2026-10-02T09:14:07Z'
                sequence: 43
                data:
                  id: ord_test_3f9a1c07b2e4
                  status: completed
                  mode: test
                  package_id: pkg_10482
                  quantity: 1
                  items:
                  - package_id: pkg_10482
                    quantity: 1
                  customer_ref: booking-78231
                  total:
                    amount: '249.00'
                    currency: INR
                  refunded:
                    amount: '0.00'
                    currency: INR
                  balance_after:
                    amount: '999751.00'
                    currency: INR
                  created: '2026-10-02T09:14:07Z'
                  esims:
                  - iccid: '8999999000000012345'
                    status: ready
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No event with this id in this mode.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  summary: Unknown path or resource
                  value:
                    error:
                      code: not_found
                      message: No such resource.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/sandbox/esims/{iccid}/simulate:
    post:
      operationId: simulateEsimEvent
      tags:
      - Sandbox
      summary: Simulate an eSIM lifecycle event
      description: |-
        **Test keys only.** Moves a sandbox eSIM along its life and sends the matching webhook (`esim.activated`, `esim.usage_80`, `esim.usage_100` or `esim.expired`) to your test-mode endpoints. Called with a live key this path answers `404`.

        The events must come in the order a real eSIM goes through: `activated` (on a `ready` eSIM), then `usage_80` / `usage_100` (on an `active` one), then `expired` (on an `active` or `depleted` one). An event the current status does not allow answers `409 invalid_state`. An event that changes nothing (a repeated `activated`, a usage level already reached, `expired` on an expired eSIM) returns the eSIM as it is and sends no webhook. Usage events on an unlimited eSIM answer `400`.
      parameters:
      - $ref: '#/components/parameters/Iccid'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulateRequest'
            example:
              event: usage_80
      responses:
        '200':
          description: The updated sandbox eSIM.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Idempotent-Replayed:
              $ref: '#/components/headers/Idempotent-Replayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Esim'
              example:
                iccid: '8999999000000012345'
                status: active
                mode: test
                order_id: ord_test_3f9a1c07b2e4
                customer_ref: booking-78231
                package:
                  id: pkg_10482
                  name: Thailand 5 GB 15 Days
                qr_code_url: https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png
                activation:
                  lpa: LPA:1$sandbox.esimify.in$TEST-7K2M9QX4B1TZ
                  smdp_address: sandbox.esimify.in
                  activation_code: TEST-7K2M9QX4B1TZ
                install_links:
                  ios: https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                  android: https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
                data:
                  unlimited: false
                  total_mb: 5120
                  used_mb: 4198
                  remaining_mb: 922
                validity_days: 15
                activated_at: '2026-10-02T09:20:41Z'
                expires_at: '2026-10-17T09:20:41Z'
                created: '2026-10-02T09:14:07Z'
        '400':
          description: '`event` is missing or not one of the four values, a usage event was sent for an unlimited
            eSIM, or the `Idempotency-Key` is missing.'
          headers:
            X-Request-Id: *id001
            X-RateLimit-Limit: *id002
            X-RateLimit-Remaining: *id003
            X-RateLimit-Reset: *id004
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_request:
                  summary: A parameter is missing or invalid
                  value:
                    error:
                      code: invalid_request
                      message: event must be one of activated, usage_80, usage_100, expired.
                      request_id: req_8f3a1c9d2b7e4a60
                      param: event
                idempotency_key_required:
                  summary: Idempotency-Key header missing
                  value:
                    error:
                      code: idempotency_key_required
                      message: The Idempotency-Key header is required on POST requests.
                      request_id: req_8f3a1c9d2b7e4a60
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No sandbox eSIM with this ICCID, or the request was made with a live key.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                esim_not_found:
                  summary: Unknown ICCID
                  value:
                    error:
                      code: esim_not_found
                      message: No such eSIM.
                      request_id: req_8f3a1c9d2b7e4a60
                not_found:
                  summary: Unknown path or resource
                  value:
                    error:
                      code: not_found
                      message: No such resource.
                      request_id: req_8f3a1c9d2b7e4a60
        '409':
          description: The event does not fit the eSIM's current status, or an idempotency conflict.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_state:
                  summary: Event not allowed in this status
                  value:
                    error:
                      code: invalid_state
                      message: Event "usage_80" cannot be applied to an eSIM that is ready. Send the "activated"
                        event first.
                      request_id: req_8f3a1c9d2b7e4a60
                idempotency_key_reused:
                  summary: Same key, different body
                  value:
                    error:
                      code: idempotency_key_reused
                      message: This Idempotency-Key was already used with a different request body.
                      request_id: req_8f3a1c9d2b7e4a60
                request_in_progress:
                  summary: Original request still running
                  value:
                    error:
                      code: request_in_progress
                      message: A request with this Idempotency-Key is still being processed. Retry shortly.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/qr/{token}.png:
    get:
      operationId: getQrCode
      tags:
      - QR codes
      summary: QR code image
      description: 'PNG QR code of an eSIM''s LPA string. **Public: no API key.** The URL is signed; use the `qr_code_url`
        of an eSIM as given rather than building it yourself. The URL does not expire and cannot be revoked; it
        answers `404` once the eSIM is cancelled. At most 240 requests per minute from one IP address.'
      security: []
      parameters:
      - name: token
        in: path
        required: true
        description: Signed token, taken from `qr_code_url`.
        schema:
          type: string
      responses:
        '200':
          description: The QR code.
          headers:
            Cache-Control:
              description: Always `private, max-age=3600`.
              schema:
                type: string
              example: private, max-age=3600
          content:
            image/png:
              schema:
                type: string
                format: binary
        '404':
          description: The token is invalid, or the eSIM was cancelled.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            Cache-Control:
              description: Always `no-store`.
              schema:
                type: string
              example: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  summary: Unknown path or resource
                  value:
                    error:
                      code: not_found
                      message: No such resource.
                      request_id: req_8f3a1c9d2b7e4a60
        '429':
          description: More than 240 requests in a minute from this IP address (or more than 30 with an invalid
            token).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rate_limited:
                  summary: Rate limit exceeded
                  value:
                    error:
                      code: rate_limited
                      message: Too many requests. Retry after the number of seconds in Retry-After.
                      request_id: req_8f3a1c9d2b7e4a60
webhooks:
  order.completed:
    post:
      operationId: onOrderCompleted
      tags:
      - Webhooks
      summary: Order completed
      description: 'An order reached `completed` or `partially_completed`. `data` is the Order: read `data.status`
        and count `data.esims`. Sent for live orders placed through the API and in the Business portal.'
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderEvent'
            example:
              id: evt_5c1d9e7a3b2f4c6d8e0a1b2c
              type: order.completed
              mode: test
              created: '2026-10-02T09:14:07Z'
              sequence: 44
              data:
                id: ord_test_3f9a1c07b2e4
                status: completed
                mode: test
                package_id: pkg_10482
                quantity: 1
                items:
                - package_id: pkg_10482
                  quantity: 1
                customer_ref: booking-78231
                total:
                  amount: '249.00'
                  currency: INR
                refunded:
                  amount: '0.00'
                  currency: INR
                balance_after:
                  amount: '999751.00'
                  currency: INR
                created: '2026-10-02T09:14:07Z'
                esims:
                - iccid: '8999999000000012345'
                  status: ready
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  order.failed:
    post:
      operationId: onOrderFailed
      tags:
      - Webhooks
      summary: Order failed
      description: 'An order ended as `failed`: nothing was issued and the amount was returned. `data` is the Order.'
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderEvent'
            example:
              id: evt_7e6d5c4b3a291807f6e5d4c3
              type: order.failed
              mode: test
              created: '2026-10-02T09:14:07Z'
              sequence: 45
              data:
                id: ord_test_9d8c7b6a5f4e
                status: failed
                mode: test
                package_id: pkg_10482
                quantity: 1
                items:
                - package_id: pkg_10482
                  quantity: 1
                customer_ref: test_order_failed-001
                total:
                  amount: '249.00'
                  currency: INR
                refunded:
                  amount: '249.00'
                  currency: INR
                balance_after:
                  amount: '1000000.00'
                  currency: INR
                created: '2026-10-02T09:14:07Z'
                esims: []
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  esim.activated:
    post:
      operationId: onEsimActivated
      tags:
      - Webhooks
      summary: eSIM activated
      description: The eSIM was used for the first time. `data` is the eSIM summary. In live these events come from
        a check that runs every 10 minutes, are produced only while the account has an enabled live endpoint, and
        only for changes seen in the last 72 hours.
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EsimEvent'
            example:
              id: evt_1f2e3d4c5b6a79880716253a
              type: esim.activated
              mode: test
              created: '2026-10-02T09:20:41Z'
              sequence: 46
              data:
                iccid: '8999999000000012345'
                status: active
                order_id: ord_test_3f9a1c07b2e4
                customer_ref: booking-78231
                data:
                  unlimited: false
                  total_mb: 5120
                  used_mb: 0
                  remaining_mb: 5120
                expires_at: '2026-10-17T09:20:41Z'
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  esim.usage_80:
    post:
      operationId: onEsimUsage80
      tags:
      - Webhooks
      summary: eSIM at 80% of its data
      description: '80% of the data has been used. `data` is the eSIM summary. Sent once per data allowance: after
        a completed top-up it can be sent again. Not sent for unlimited plans. In live these events come from a
        check that runs every 10 minutes, are produced only while the account has an enabled live endpoint, and
        only for changes seen in the last 72 hours.'
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EsimEvent'
            example:
              id: evt_0a1b2c3d4e5f60718293a4b5
              type: esim.usage_80
              mode: test
              created: '2026-10-02T09:40:12Z'
              sequence: 47
              data:
                iccid: '8999999000000012345'
                status: active
                order_id: ord_test_3f9a1c07b2e4
                customer_ref: booking-78231
                data:
                  unlimited: false
                  total_mb: 5120
                  used_mb: 4198
                  remaining_mb: 922
                expires_at: '2026-10-17T09:20:41Z'
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  esim.usage_100:
    post:
      operationId: onEsimUsage100
      tags:
      - Webhooks
      summary: eSIM out of data
      description: 'All of the data has been used. `data` is the eSIM summary. Sent once per data allowance: after
        a completed top-up it can be sent again. Not sent for unlimited plans. In live these events come from a
        check that runs every 10 minutes, are produced only while the account has an enabled live endpoint, and
        only for changes seen in the last 72 hours.'
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EsimEvent'
            example:
              id: evt_9a8b7c6d5e4f30211203f4e5
              type: esim.usage_100
              mode: test
              created: '2026-10-05T16:02:30Z'
              sequence: 48
              data:
                iccid: '8999999000000012345'
                status: depleted
                order_id: ord_test_3f9a1c07b2e4
                customer_ref: booking-78231
                data:
                  unlimited: false
                  total_mb: 5120
                  used_mb: 5120
                  remaining_mb: 0
                expires_at: '2026-10-17T09:20:41Z'
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  esim.expired:
    post:
      operationId: onEsimExpired
      tags:
      - Webhooks
      summary: eSIM expired
      description: The plan's validity ended. `data` is the eSIM summary. In live these events come from a check
        that runs every 10 minutes, are produced only while the account has an enabled live endpoint, and only for
        changes seen in the last 72 hours.
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EsimEvent'
            example:
              id: evt_3c4d5e6f708192a3b4c5d6e7
              type: esim.expired
              mode: test
              created: '2026-10-17T09:20:41Z'
              sequence: 49
              data:
                iccid: '8999999000000012345'
                status: expired
                order_id: ord_test_3f9a1c07b2e4
                customer_ref: booking-78231
                data:
                  unlimited: false
                  total_mb: 5120
                  used_mb: 4198
                  remaining_mb: 922
                expires_at: '2026-10-17T09:20:41Z'
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  topup.completed:
    post:
      operationId: onTopupCompleted
      tags:
      - Webhooks
      summary: Top-up completed
      description: A top-up was added to an eSIM. `data` is the Topup. A failed top-up sends no event.
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TopupEvent'
            example:
              id: evt_b1c2d3e4f5a697887960a1b2
              type: topup.completed
              mode: test
              created: '2026-10-02T09:31:55Z'
              sequence: 50
              data:
                id: top_test_5b1e9c2a7d40
                status: completed
                iccid: '8999999000000012345'
                package_id: pkg_10483
                customer_ref: booking-78231
                total:
                  amount: '159.00'
                  currency: INR
                balance_after:
                  amount: '999592.00'
                  currency: INR
                created: '2026-10-02T09:31:55Z'
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  balance.low:
    post:
      operationId: onBalanceLow
      tags:
      - Webhooks
      summary: Balance low
      description: An order or top-up took your balance below the threshold set in the Business portal. Sent
        once per crossing, in live and (against the virtual balance) in the sandbox. `data` is `{ balance, threshold }`.
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalanceLowEvent'
            example:
              id: evt_d4e5f6a7b8c90a1b2c3d4e5f
              type: balance.low
              mode: live
              created: '2026-10-02T11:05:19Z'
              sequence: 51
              data:
                balance:
                  amount: '742.50'
                  currency: INR
                threshold:
                  amount: '1000.00'
                  currency: INR
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
  ping:
    post:
      operationId: onPing
      tags:
      - Webhooks
      summary: Test event
      description: Sent only when you press "Send test event" in the portal. `data` is `{}` and `sequence` is 0.
        Not stored in `GET /v1/events`.
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookUserAgent'
      - $ref: '#/components/parameters/WebhookEventId'
      - $ref: '#/components/parameters/WebhookEventType'
      - $ref: '#/components/parameters/WebhookDeliveryId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PingEvent'
            example:
              id: evt_00112233445566778899aabb
              type: ping
              mode: test
              created: '2026-10-02T09:14:07Z'
              sequence: 0
              data: {}
      responses:
        2XX:
          description: Any 2xx status within 10 seconds marks the delivery as delivered. The response body is ignored.
        default:
          description: Any other status, a redirect, or no answer within 10 seconds (DNS lookup included) counts
            as a failure. The delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours
            and 24 hours (8 attempts in total) and then marked failed. After 5 failed attempts in a row deliveries
            to the endpoint are paused for 30 seconds, doubling up to 15 minutes. An endpoint is disabled automatically
            after 20 deliveries in a row that each used up all 8 attempts, or after 3 days of nothing but failures.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Your secret API key, e.g. `sk_test_EXAMPLEKEY000000000000`. Create and revoke keys in the Business
        portal → Developers.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 'Required on every `POST`. A unique value you generate per logical operation (a UUID works well).
        Retrying with the same key and the same body returns the original response with `Idempotent-Replayed: true`;
        the same key with a different body returns `409 idempotency_key_reused`. Keys are scoped to your account
        and mode.'
      schema:
        type: string
        minLength: 1
        maxLength: 255
      example: 7b0f6c1e-2a4d-4c8e-9f31-5d2e8a7c4b10
    Limit:
      name: limit
      in: query
      required: false
      description: Page size.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    Cursor:
      name: cursor
      in: query
      required: false
      description: 'Opaque cursor: the `next_cursor` of the previous page.'
      schema:
        type: string
    Iccid:
      name: iccid
      in: path
      required: true
      description: The eSIM's ICCID.
      schema:
        type: string
      example: '8999999000000012345'
    WebhookUserAgent:
      name: User-Agent
      in: header
      required: true
      schema:
        type: string
      example: eSIMify-Webhooks/1.0
    WebhookEventId:
      name: X-Esimify-Event-Id
      in: header
      required: true
      description: 'The event''s `id`. Use it to de-duplicate: a retried delivery carries the same event id.'
      schema:
        type: string
      example: evt_5c1d9e7a3b2f4c6d8e0a1b2c
    WebhookEventType:
      name: X-Esimify-Event-Type
      in: header
      required: true
      description: The event's `type`.
      schema:
        $ref: '#/components/schemas/EventType'
    WebhookDeliveryId:
      name: X-Esimify-Delivery-Id
      in: header
      required: true
      description: Identifier of this delivery.
      schema:
        type: string
    WebhookTimestamp:
      name: X-Esimify-Timestamp
      in: header
      required: true
      description: When the request was signed, in epoch seconds.
      schema:
        type: integer
      example: 1790932447
    WebhookSignature:
      name: X-Esimify-Signature
      in: header
      required: true
      description: '`t=<epoch seconds>,v1=<signature>`, where the signature is the hex HMAC-SHA256, keyed with the
        endpoint''s signing secret (`whsec_…`), of `t + "." + rawBody` (the exact bytes received, before any JSON
        parsing). Recompute and compare in constant time, and reject requests where `t` is more than 5 minutes from
        your clock.'
      schema:
        type: string
        pattern: ^t=\d+,v1=[0-9a-f]{64}$
      example: t=1790932447,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  headers:
    X-Request-Id:
      description: Unique id of this request (`req_<16 hex>`). On every response. Quote it when contacting support.
        Errors repeat it as `error.request_id`.
      schema:
        type: string
        pattern: ^req_[0-9a-f]{16}$
      example: req_8f3a1c9d2b7e4a60
      required: true
    X-RateLimit-Limit:
      description: 'The limit that applies to this request: the tighter of 120 per minute (all requests) and 30
        per minute (`POST`).'
      schema:
        type: integer
      example: 120
      required: true
    X-RateLimit-Remaining:
      description: Requests left in the current window.
      schema:
        type: integer
      example: 117
      required: true
    X-RateLimit-Reset:
      description: When the current window resets, in epoch seconds.
      schema:
        type: integer
      example: 1790932500
      required: true
    Retry-After:
      description: Seconds to wait before retrying.
      schema:
        type: integer
      example: 21
      required: true
    Idempotent-Replayed:
      description: 'Present with the value `true` when this response is a replay: the answer to an earlier request
        that used the same `Idempotency-Key` and body. Absent otherwise.'
      schema:
        type: string
        enum:
        - 'true'
      example: 'true'
      required: false
    X-RateLimit-Limit-IfKeyKnown:
      description: 'The limit that applies to this request: the tighter of 120 per minute (all requests) and 30
        per minute (`POST`). Absent when the request was refused before the API key was recognised.'
      schema:
        type: integer
      example: 120
      required: false
    X-RateLimit-Remaining-IfKeyKnown:
      description: Requests left in the current window. Absent when the request was refused before the API key was
        recognised.
      schema:
        type: integer
      example: 117
      required: false
    X-RateLimit-Reset-IfKeyKnown:
      description: When the current window resets, in epoch seconds. Absent when the request was refused before
        the API key was recognised.
      schema:
        type: integer
      example: 1790932500
      required: false
    WWW-Authenticate:
      description: Always `Bearer realm="eSIMify Partners API"`.
      required: true
      schema:
        type: string
      example: Bearer realm="eSIMify Partners API"
  responses:
    Unauthorized:
      description: 'The API key is missing, malformed, unknown, revoked or expired. The message is the same in every
        case. No `X-RateLimit-*` headers: no key was recognised. These requests are not shown in your request log.'
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        WWW-Authenticate:
          $ref: '#/components/headers/WWW-Authenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalid_api_key:
              summary: Missing, malformed, unknown, revoked or expired key
              value:
                error:
                  code: invalid_api_key
                  message: 'Invalid API key. Send your secret key as ''Authorization: Bearer sk_…''.'
                  request_id: req_8f3a1c9d2b7e4a60
    Forbidden:
      description: The key is valid but may not make this request. `account_inactive` is returned for every request,
        with test and live keys, while the partner account is deactivated.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            ip_not_allowed:
              summary: Caller IP not on the key's allowlist
              value:
                error:
                  code: ip_not_allowed
                  message: This API key cannot be used from your IP address.
                  request_id: req_8f3a1c9d2b7e4a60
            account_inactive:
              summary: Partner account deactivated
              value:
                error:
                  code: account_inactive
                  message: This partner account is inactive. Contact eSIMify support.
                  request_id: req_8f3a1c9d2b7e4a60
            live_mode_not_enabled:
              summary: Live key used before live mode is enabled
              value:
                error:
                  code: live_mode_not_enabled
                  message: Live mode is not enabled for this account yet. Use a test key, or complete verification
                    in the Business portal.
                  request_id: req_8f3a1c9d2b7e4a60
    RateLimited:
      description: 'Rate limit exceeded: 120 requests per minute per API key, 30 per minute for `POST`, in fixed
        one-minute windows. Also returned to every request from an IP address that made 60 requests with a bad key
        within 10 minutes, until those 10 minutes are over; that answer carries `Retry-After` but no `X-RateLimit-*`
        headers.'
      headers:
        X-Request-Id: *id001
        X-RateLimit-Limit: *id002
        X-RateLimit-Remaining: *id003
        X-RateLimit-Reset: *id004
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rate_limited:
              summary: Rate limit exceeded
              value:
                error:
                  code: rate_limited
                  message: Too many requests. Slow down and retry after the number of seconds in Retry-After.
                  request_id: req_8f3a1c9d2b7e4a60
            failed_authentication:
              summary: Too many requests with a bad key from this address
              value:
                error:
                  code: rate_limited
                  message: Too many failed authentication attempts. Try again later.
                  request_id: req_8f3a1c9d2b7e4a60
    InternalError:
      description: Unexpected error on eSIMify's side. Safe to retry with the same `Idempotency-Key`.
      headers:
        X-Request-Id: *id001
        X-RateLimit-Limit: *id002
        X-RateLimit-Remaining: *id003
        X-RateLimit-Reset: *id004
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            internal_error:
              summary: Unexpected error
              value:
                error:
                  code: internal_error
                  message: Something went wrong on our side. Please retry; if it persists, contact support with
                    the request_id.
                  request_id: req_8f3a1c9d2b7e4a60
  schemas:
    Money:
      type: object
      description: A monetary amount. `amount` is a decimal string with 2 decimal places.
      required:
      - amount
      - currency
      properties:
        amount:
          type: string
          pattern: ^-?\d+\.\d{2}$
          examples:
          - '249.00'
        currency:
          type: string
          description: ISO 4217 currency code.
          examples:
          - INR
      example:
        amount: '249.00'
        currency: INR
    Country:
      type: object
      required:
      - code
      - name
      - type
      - packages_count
      properties:
        code:
          type: string
          description: 'Code to filter packages by. For a country: ISO 3166-1 alpha-2. For a region: `region-<number>`,
            stable if the region is renamed.'
          examples:
          - TH
          - region-412
        name:
          type: string
          examples:
          - Thailand
        type:
          type: string
          enum:
          - country
          - region
        packages_count:
          type: integer
          minimum: 0
          description: Number of packages available for this destination. For a country this includes regional and
            global packages that cover it.
      example:
        code: TH
        name: Thailand
        type: country
        packages_count: 12
    Package:
      type: object
      description: A data plan you can order or use as a top-up. The list shows one package per destination, data
        size and validity; see `GET /v1/packages`.
      required:
      - id
      - name
      - type
      - country
      - countries
      - data_mb
      - unlimited
      - validity_days
      - price
      - retail_price
      - topup
      properties:
        id:
          type: string
          pattern: ^pkg_\d+$
          description: Package identifier, `pkg_<number>`.
          examples:
          - pkg_10482
        name:
          type: string
        type:
          type: string
          enum:
          - country
          - region
          - global
        country:
          type:
          - string
          - 'null'
          description: ISO 3166-1 alpha-2 code for single-country packages, otherwise `null`.
          examples:
          - TH
        countries:
          type: array
          items:
            type: string
          description: ISO 3166-1 alpha-2 codes of every country the package covers.
        data_mb:
          type:
          - integer
          - 'null'
          description: Data allowance in megabytes. `null` exactly when `unlimited` is `true`. For a daily-allowance
            plan it is the amount per day.
        unlimited:
          type: boolean
        validity_days:
          type: integer
        price:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: 'Your price for one eSIM: the retail price less your partner discount. This is what is debited.'
        retail_price:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: eSIMify's retail price for the same package.
        topup:
          type: boolean
          description: '`true` when the package can be bought as a top-up. Which packages a given eSIM can take
            is answered by `GET /v1/esims/{iccid}/topup-packages`.'
      example:
        id: pkg_10482
        name: Thailand 5 GB 15 Days
        type: country
        country: TH
        countries:
        - TH
        data_mb: 5120
        unlimited: false
        validity_days: 15
        price:
          amount: '249.00'
          currency: INR
        retail_price:
          amount: '349.00'
          currency: INR
        topup: true
    OrderEsim:
      type: object
      required:
      - iccid
      - status
      properties:
        iccid:
          type: string
        status:
          $ref: '#/components/schemas/EsimStatus'
      example:
        iccid: '8999999000000012345'
        status: ready
    OrderItem:
      type: object
      required:
      - package_id
      - quantity
      properties:
        package_id:
          type: string
          examples:
          - pkg_10482
        quantity:
          type: integer
          minimum: 1
      example:
        package_id: pkg_10482
        quantity: 1
    OrderStatus:
      type: string
      enum:
      - processing
      - completed
      - partially_completed
      - failed
      - cancelled
      description: '`processing` = accepted and debited, eSIMs still being issued. `completed` = every eSIM issued.
        `partially_completed` = some issued, the rest refunded. `failed` = none issued, amount refunded. `cancelled`
        = cancelled in the Business portal or by eSIMify support. The `order.completed` event is sent for `completed`
        AND `partially_completed`.'
    EsimStatus:
      type: string
      enum:
      - ready
      - active
      - depleted
      - expired
      - cancelled
      description: '`ready` = issued, not used yet. `active` = used for the first time, validity running. `depleted`
        = data fully used (never for unlimited packages); a top-up makes it `active` again. `expired` = validity
        ended. `cancelled` = cancelled in the Business portal or by eSIMify support.'
    Mode:
      type: string
      enum:
      - test
      - live
      description: '`test` for objects created with an `sk_test_` key (sandbox), `live` for `sk_live_` keys. A live
        key never sees sandbox objects and vice versa.'
    Order:
      type: object
      required:
      - id
      - status
      - mode
      - package_id
      - quantity
      - items
      - customer_ref
      - total
      - refunded
      - balance_after
      - created
      - esims
      properties:
        id:
          type: string
          description: 'Order identifier: `ord_` and lower-case letters and digits. Sandbox orders are `ord_test_<12
            hex>`. Ids are exact and case-sensitive.'
          examples:
          - ord_test_3f9a1c07b2e4
        status:
          $ref: '#/components/schemas/OrderStatus'
        mode:
          $ref: '#/components/schemas/Mode'
        package_id:
          type:
          - string
          - 'null'
          description: 'The package that was bought. `null` when the order holds more than one package (only possible
            for orders placed in the Business portal): read `items`.'
          examples:
          - pkg_10482
        quantity:
          type:
          - integer
          - 'null'
          minimum: 1
          description: 'Number of eSIMs ordered. `null` when the order holds more than one package: read `items`.'
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
          description: What was bought, one entry per package. An order created through the API always has exactly
            one entry.
        customer_ref:
          type:
          - string
          - 'null'
          maxLength: 120
          description: Your own reference, as sent when the order was created. `null` when none (or an empty string)
            was sent.
        total:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: Amount debited from your balance for the order.
        refunded:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: Amount returned to your balance for units that were not issued, or that were cancelled.
        balance_after:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: 'Your balance right after the order was debited. It does not include `refunded`: for a failed
            or partially completed order your balance is `balance_after` plus `refunded`.'
        created:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp.
          examples:
          - '2026-10-02T09:14:07Z'
        esims:
          type: array
          items:
            $ref: '#/components/schemas/OrderEsim'
          description: One entry per unit that was issued. Can be incomplete or empty while `processing`, is shorter
            than `quantity` for `partially_completed`, and empty for `failed` orders.
      example:
        id: ord_test_3f9a1c07b2e4
        status: completed
        mode: test
        package_id: pkg_10482
        quantity: 1
        items:
        - package_id: pkg_10482
          quantity: 1
        customer_ref: booking-78231
        total:
          amount: '249.00'
          currency: INR
        refunded:
          amount: '0.00'
          currency: INR
        balance_after:
          amount: '999751.00'
          currency: INR
        created: '2026-10-02T09:14:07Z'
        esims:
        - iccid: '8999999000000012345'
          status: ready
    OrderCreateRequest:
      type: object
      required:
      - package_id
      properties:
        package_id:
          type: string
          examples:
          - pkg_10482
          description: Exact id of a package you can sell, from `GET /v1/packages`.
        quantity:
          type: integer
          minimum: 1
          maximum: 50
          default: 1
        customer_ref:
          type: string
          maxLength: 120
          description: Your own reference (booking number, traveller id…). Returned on the order and its eSIMs and
            usable as a list filter. No control characters. In the sandbox, special prefixes trigger failures (see
            the operation).
        customer:
          type: object
          properties:
            name:
              type: string
              maxLength: 120
            email:
              type: string
              format: email
              maxLength: 254
          description: Optional end-customer details. Shown to your staff in the Business portal; not returned by
            the API. eSIMify never emails or messages your customer for API orders.
      example:
        package_id: pkg_10482
        quantity: 1
        customer_ref: booking-78231
        customer:
          name: Test Traveller
          email: traveller@example.com
    EsimData:
      type: object
      required:
      - unlimited
      - total_mb
      - used_mb
      - remaining_mb
      properties:
        unlimited:
          type: boolean
        total_mb:
          type:
          - integer
          - 'null'
          description: Total allowance, completed top-ups included. `null` for an unlimited plan.
        used_mb:
          type: integer
          minimum: 0
          description: Megabytes used. Always a number, never more than `total_mb`.
        remaining_mb:
          type:
          - integer
          - 'null'
          description: Megabytes left. `null` for an unlimited plan.
      example:
        unlimited: false
        total_mb: 5120
        used_mb: 0
        remaining_mb: 5120
    EsimSummary:
      type: object
      description: 'An eSIM as returned in lists: the full eSIM without the `activation` block.'
      required:
      - iccid
      - status
      - mode
      - order_id
      - customer_ref
      - package
      - qr_code_url
      - install_links
      - data
      - validity_days
      - activated_at
      - expires_at
      - created
      properties:
        iccid:
          type: string
          description: The eSIM's ICCID; this is its identifier. Sandbox ICCIDs start `8999999`.
          examples:
          - '8999999000000012345'
        status:
          $ref: '#/components/schemas/EsimStatus'
        mode:
          $ref: '#/components/schemas/Mode'
        order_id:
          type:
          - string
          - 'null'
          examples:
          - ord_test_3f9a1c07b2e4
        customer_ref:
          type:
          - string
          - 'null'
        package:
          type: object
          required:
          - id
          - name
          properties:
            id:
              type:
              - string
              - 'null'
            name:
              type: string
        qr_code_url:
          type:
          - string
          - 'null'
          format: uri
          description: Signed URL of a PNG QR code of the LPA string (`<base URL>/v1/qr/{token}.png`). Needs no
            API key, so it can be shown to the customer directly. It does not expire. `null` for a cancelled eSIM,
            and in the rare case the install details are not stored yet.
        install_links:
          type: object
          required:
          - ios
          - android
          properties:
            ios:
              type:
              - string
              - 'null'
              format: uri
            android:
              type:
              - string
              - 'null'
              format: uri
          description: 'One-tap install links that open the phone''s own eSIM setup screen. The LPA string inside
            is percent-encoded: use the link as returned. Both are `null` when `qr_code_url` is `null`.'
        data:
          $ref: '#/components/schemas/EsimData'
        validity_days:
          type: integer
          description: Days the plan lasts once it starts, completed top-ups included.
        activated_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the eSIM was first used. `null` until then.
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
          description: 'When the plan ends: first use plus validity. `null` until the eSIM is activated.'
        created:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp.
          examples:
          - '2026-10-02T09:14:07Z'
      example:
        iccid: '8999999000000012345'
        status: ready
        mode: test
        order_id: ord_test_3f9a1c07b2e4
        customer_ref: booking-78231
        package:
          id: pkg_10482
          name: Thailand 5 GB 15 Days
        qr_code_url: https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png
        install_links:
          ios: https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
          android: https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
        data:
          unlimited: false
          total_mb: 5120
          used_mb: 0
          remaining_mb: 5120
        validity_days: 15
        activated_at: null
        expires_at: null
        created: '2026-10-02T09:14:07Z'
    EsimActivation:
      type: object
      required:
      - lpa
      - smdp_address
      - activation_code
      properties:
        lpa:
          type:
          - string
          - 'null'
          description: Full LPA string (the QR code content).
          examples:
          - LPA:1$sandbox.esimify.in$TEST-7K2M9QX4B1TZ
        smdp_address:
          type:
          - string
          - 'null'
          description: SM-DP+ address for manual entry.
        activation_code:
          type:
          - string
          - 'null'
          description: Activation code for manual entry.
      description: Install details. All three fields are `null` for a cancelled eSIM, and in the rare case the details
        are not stored yet.
    Esim:
      description: A full eSIM, including the `activation` block needed to install it.
      allOf:
      - $ref: '#/components/schemas/EsimSummary'
      - type: object
        required:
        - activation
        properties:
          activation:
            $ref: '#/components/schemas/EsimActivation'
      example:
        iccid: '8999999000000012345'
        status: ready
        mode: test
        order_id: ord_test_3f9a1c07b2e4
        customer_ref: booking-78231
        package:
          id: pkg_10482
          name: Thailand 5 GB 15 Days
        qr_code_url: https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png
        activation:
          lpa: LPA:1$sandbox.esimify.in$TEST-7K2M9QX4B1TZ
          smdp_address: sandbox.esimify.in
          activation_code: TEST-7K2M9QX4B1TZ
        install_links:
          ios: https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
          android: https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-7K2M9QX4B1TZ
        data:
          unlimited: false
          total_mb: 5120
          used_mb: 0
          remaining_mb: 5120
        validity_days: 15
        activated_at: null
        expires_at: null
        created: '2026-10-02T09:14:07Z'
    EsimEventData:
      type: object
      description: 'The eSIM summary carried in `esim.*` webhook events: the eSIM as `GET /v1/esims/{iccid}` shows
        it when the event is created.'
      required:
      - iccid
      - status
      - order_id
      - customer_ref
      - data
      - expires_at
      properties:
        iccid:
          type: string
        status:
          $ref: '#/components/schemas/EsimStatus'
        order_id:
          type:
          - string
          - 'null'
        customer_ref:
          type:
          - string
          - 'null'
        data:
          $ref: '#/components/schemas/EsimData'
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
      example:
        iccid: '8999999000000012345'
        status: active
        order_id: ord_test_3f9a1c07b2e4
        customer_ref: booking-78231
        data:
          unlimited: false
          total_mb: 5120
          used_mb: 4198
          remaining_mb: 922
        expires_at: '2026-10-17T09:20:41Z'
    Topup:
      type: object
      description: 'A package added to an existing eSIM. A top-up answered with `201` can still have the status
        `failed`: check `status`.'
      required:
      - id
      - status
      - iccid
      - package_id
      - customer_ref
      - total
      - balance_after
      - created
      properties:
        id:
          type: string
          description: 'Top-up identifier: `top_` and lower-case letters and digits. Sandbox top-ups are `top_test_<12
            hex>`.'
          examples:
          - top_test_5b1e9c2a7d40
        status:
          type: string
          enum:
          - completed
          - processing
          - failed
          description: '`completed` = the package was added. `processing` = still being applied. `failed` = nothing
            was added and the amount was returned to your balance.'
        iccid:
          type: string
        package_id:
          type: string
        customer_ref:
          type:
          - string
          - 'null'
          maxLength: 120
          description: Your own reference for the top-up.
        total:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: Price of the top-up.
        balance_after:
          allOf:
          - $ref: '#/components/schemas/Money'
          description: 'Your balance right after the top-up was debited. For a `failed` top-up: the balance after
            the amount was returned.'
        created:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp.
          examples:
          - '2026-10-02T09:14:07Z'
      example:
        id: top_test_5b1e9c2a7d40
        status: completed
        iccid: '8999999000000012345'
        package_id: pkg_10483
        customer_ref: booking-78231
        total:
          amount: '159.00'
          currency: INR
        balance_after:
          amount: '999592.00'
          currency: INR
        created: '2026-10-02T09:31:55Z'
    TopupCreateRequest:
      type: object
      required:
      - package_id
      properties:
        package_id:
          type: string
          description: A package returned by `GET /v1/esims/{iccid}/topup-packages` for this eSIM.
        customer_ref:
          type: string
          maxLength: 120
          description: Your own reference for the top-up. No control characters. The sandbox `customer_ref` triggers
            work here too.
      example:
        package_id: pkg_10483
        customer_ref: booking-78231
    SimulateRequest:
      type: object
      required:
      - event
      properties:
        event:
          type: string
          enum:
          - activated
          - usage_80
          - usage_100
          - expired
      example:
        event: usage_80
    Balance:
      type: object
      required:
      - balance
      - low_balance_threshold
      - mode
      properties:
        balance:
          $ref: '#/components/schemas/Money'
        low_balance_threshold:
          oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
          description: 'The level set in the Business portal under Settings. `null` when none is set (or it is 0):
            then no `balance.low` event is sent.'
        mode:
          $ref: '#/components/schemas/Mode'
      example:
        balance:
          amount: '999751.00'
          currency: INR
        low_balance_threshold: null
        mode: test
    BalanceLowData:
      type: object
      required:
      - balance
      - threshold
      properties:
        balance:
          $ref: '#/components/schemas/Money'
        threshold:
          $ref: '#/components/schemas/Money'
      example:
        balance:
          amount: '742.50'
          currency: INR
        threshold:
          amount: '1000.00'
          currency: INR
    EventType:
      type: string
      enum:
      - order.completed
      - order.failed
      - esim.activated
      - esim.usage_80
      - esim.usage_100
      - esim.expired
      - topup.completed
      - balance.low
      - ping
      description: '`ping` is only ever sent by the portal''s "Send test event" button. It is not stored and cannot
        be used as a filter.'
    Event:
      type: object
      description: A record of something that happened on your account. The body of every webhook is an Event.
      required:
      - id
      - type
      - mode
      - created
      - sequence
      - data
      properties:
        id:
          type: string
          pattern: ^evt_[0-9a-f]{24}$
          examples:
          - evt_5c1d9e7a3b2f4c6d8e0a1b2c
        type:
          $ref: '#/components/schemas/EventType'
        mode:
          $ref: '#/components/schemas/Mode'
        created:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp.
          examples:
          - '2026-10-02T09:14:07Z'
        sequence:
          type: integer
          minimum: 0
          description: Grows with every event on your account, counted separately for test and live. Order events
            by it, not by `created`. Numbers can be skipped. `0` on a `ping`.
          examples:
          - 41
        data:
          type: object
          description: 'Depends on `type`: an Order for `order.*`, an eSIM summary (`EsimEventData`) for `esim.*`,
            a Topup for `topup.completed`, `{ balance, threshold }` for `balance.low`, `{}` for `ping`.'
      example:
        id: evt_5c1d9e7a3b2f4c6d8e0a1b2c
        type: order.completed
        mode: test
        created: '2026-10-02T09:14:07Z'
        sequence: 53
        data:
          id: ord_test_3f9a1c07b2e4
          status: completed
          mode: test
          package_id: pkg_10482
          quantity: 1
          items:
          - package_id: pkg_10482
            quantity: 1
          customer_ref: booking-78231
          total:
            amount: '249.00'
            currency: INR
          refunded:
            amount: '0.00'
            currency: INR
          balance_after:
            amount: '999751.00'
            currency: INR
          created: '2026-10-02T09:14:07Z'
          esims:
          - iccid: '8999999000000012345'
            status: ready
    Error:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          - request_id
          properties:
            code:
              type: string
              enum:
              - invalid_request
              - idempotency_key_required
              - invalid_api_key
              - insufficient_balance
              - ip_not_allowed
              - account_inactive
              - live_mode_not_enabled
              - not_found
              - package_not_found
              - order_not_found
              - esim_not_found
              - topup_not_found
              - idempotency_key_reused
              - esim_not_topupable
              - request_in_progress
              - invalid_state
              - rate_limited
              - internal_error
              - supplier_unavailable
              description: Stable machine-readable code. Branch on this, not on `message`.
            message:
              type: string
              description: Human-readable explanation. May change.
            request_id:
              type: string
              pattern: ^req_[0-9a-f]{16}$
              description: Same value as the `X-Request-Id` response header.
            param:
              type: string
              description: The offending field, query parameter or header, when one is to blame. Present on `invalid_request`
                for a field, and on the idempotency errors (`Idempotency-Key`).
      example:
        error:
          code: invalid_request
          message: quantity must be an integer between 1 and 50.
          request_id: req_8f3a1c9d2b7e4a60
          param: quantity
    CountryList:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Country'
    TopupPackageList:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Package'
    PackageList:
      type: object
      required:
      - data
      - has_more
      - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Package'
        has_more:
          type: boolean
          description: '`true` when more results exist after this page.'
        next_cursor:
          type:
          - string
          - 'null'
          description: Pass as `cursor` to fetch the next page. `null` on the last page.
    OrderList:
      type: object
      required:
      - data
      - has_more
      - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Order'
        has_more:
          type: boolean
          description: '`true` when more results exist after this page.'
        next_cursor:
          type:
          - string
          - 'null'
          description: Pass as `cursor` to fetch the next page. `null` on the last page.
    EsimList:
      type: object
      required:
      - data
      - has_more
      - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/EsimSummary'
        has_more:
          type: boolean
          description: '`true` when more results exist after this page.'
        next_cursor:
          type:
          - string
          - 'null'
          description: Pass as `cursor` to fetch the next page. `null` on the last page.
    EventList:
      type: object
      required:
      - data
      - has_more
      - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Event'
        has_more:
          type: boolean
          description: '`true` when more results exist after this page.'
        next_cursor:
          type:
          - string
          - 'null'
          description: Pass as `cursor` to fetch the next page. `null` on the last page.
    OrderEvent:
      description: Event whose `data` is the Order.
      allOf:
      - $ref: '#/components/schemas/Event'
      - type: object
        properties:
          type:
            type: string
            enum:
            - order.completed
            - order.failed
          data:
            $ref: '#/components/schemas/Order'
    EsimEvent:
      description: Event whose `data` is the eSIM summary.
      allOf:
      - $ref: '#/components/schemas/Event'
      - type: object
        properties:
          type:
            type: string
            enum:
            - esim.activated
            - esim.usage_80
            - esim.usage_100
            - esim.expired
          data:
            $ref: '#/components/schemas/EsimEventData'
    TopupEvent:
      description: Event whose `data` is the Topup.
      allOf:
      - $ref: '#/components/schemas/Event'
      - type: object
        properties:
          type:
            type: string
            enum:
            - topup.completed
          data:
            $ref: '#/components/schemas/Topup'
    BalanceLowEvent:
      description: Event whose `data` is `{ balance, threshold }`.
      allOf:
      - $ref: '#/components/schemas/Event'
      - type: object
        properties:
          type:
            type: string
            enum:
            - balance.low
          data:
            $ref: '#/components/schemas/BalanceLowData'
    PingEvent:
      description: Test event sent by the portal's "Send test event" button. Never stored; `sequence` is 0.
      allOf:
      - $ref: '#/components/schemas/Event'
      - type: object
        properties:
          type:
            type: string
            enum:
            - ping
          data:
            type: object
    StoredEventType:
      type: string
      enum:
      - order.completed
      - order.failed
      - esim.activated
      - esim.usage_80
      - esim.usage_100
      - esim.expired
      - topup.completed
      - balance.low
      description: The event types that are stored and can be listed.
    TopupList:
      type: object
      required:
      - data
      - has_more
      - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Topup'
        has_more:
          type: boolean
        next_cursor:
          type:
          - string
          - 'null'
