openapi: 3.1.0
info:
  title: Citrus Mobile Reseller API
  version: '2.0'
  summary: Build your own eSIM platform with pay-as-you-go connectivity in 200+ countries.
  description: |
    The Citrus Mobile Reseller API lets you provision, manage, and bill for eSIMs
    programmatically. Build a travel eSIM storefront, IoT connectivity platform,
    corporate travel solution, or any product that needs cellular data in 200+
    countries.

    ## Quick Start

    1. Create a developer account at [citrusmobile.com/developer](https://citrusmobile.com/developer).
    2. Top up your reseller balance via the developer dashboard. Minimum **$10**.
    3. Check available rates with `GET /rates`.
    4. Provision your first eSIM with `POST /esim/provision`.
    5. Fund the eSIM with `POST /esim/{iccid}/fund` — the SIM starts with a $0 wallet.

    ## Pricing

    - **No monthly fee** — API access is free for all registered developers
    - **$1.75 per eSIM** provisioning fee
    - Data rates are **10% below** the retail rates shown on citrusmobile.com
    - Volume-commitment discounts start at $1,000/month — contact sales

    ## Authentication

    All requests require an API key in the `Authorization` header. API keys use
    the prefix `rsk_` and are 68 characters long.

    ```
    Authorization: Bearer rsk_a1b2c3d4e5f6...
    ```

    ### Access levels

    | Operation | API Key | Balance |
    |-----------|---------|---------|
    | Read endpoints (list, status, rates, balance) | Required | — |
    | Provision eSIM | Required | ≥ $1.75 |
    | Enable data | Required | > $0 |
    | Throttle speed | Required | — |
    | Fund eSIM | Required | ≥ funded amount |
    | Disable / Terminate | Required | — |
    | Webhook management | Required | — |

    ## Rate Limits

    API requests are limited to **100 requests per minute** per API key.
    Exceeding this returns `429 Too Many Requests` with a `Retry-After` header.
    Need higher limits? Email [support@citrusmobile.com](mailto:support@citrusmobile.com).

    ## Display Precision

    `wallet_balance_usd` is always rounded **down** to the nearest cent and may
    be up to ~5¢ below the exact remaining credit — never above it. Funding
    `$20` will show `$19.99` right after funding; funding `$9`, `$18`, or `$27`
    round-trips exactly. Your customer is never shortchanged — the SIM always
    has at least the displayed amount of credit.

    ## Propagation Delay

    Our upstream network takes ~2–3 minutes to reflect credit changes after a
    fund call. `POST /fund` returns immediately and your customer can start
    using data right away, but the reporting pipeline (`total_data_charged_usd`
    updates, `esim.balance_low` / `esim.balance_depleted` webhooks) pauses for
    **5 minutes after every fund** so the upstream propagation can settle.
    Usage during the cooldown is still charged — it's reflected on the next
    sync cycle after the window ends.
  contact:
    email: support@citrusmobile.com
    url: https://citrusmobile.com/developer

servers:
  - url: https://citrusmobile.com/api/v2/reseller
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: eSIMs
    description: Provision and manage individual eSIM profiles.
  - name: Wallet
    description: Check your reseller account balance and usage history.
  - name: Rates
    description: Per-country data rates charged to your account.
  - name: Account
    description: Account details and eSIM counts.
  - name: Webhooks
    description: Manage webhook endpoints for real-time event notifications.

paths:
  /esim/provision:
    post:
      tags: [eSIMs]
      summary: Provision a new eSIM
      description: |
        Reserves an eSIM from inventory, assigns it to your billing group, and
        returns installation details including a QR code (base64 PNG data URL)
        and an iOS direct install URL.

        The SIM is created with a **$0 wallet** — call `POST /esim/{iccid}/fund`
        before your customer can use data. Delivery of the QR code or install
        URL is your responsibility (e.g. via email).
      operationId: provisionEsim
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                end_user_reference:
                  type: string
                  description: Your own customer/order ID for tracking. Surfaced in webhook payloads.
                  example: order_789
                label:
                  type: string
                  description: Human-friendly label shown in dashboards and webhook payloads.
                  example: Jane Smith - Tokyo
      responses:
        '201':
          description: eSIM created and reserved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedEsim'
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '503': { $ref: '#/components/responses/NoEsimsAvailable' }

  /esim/list:
    get:
      tags: [eSIMs]
      summary: List your eSIMs
      description: |
        List all eSIMs provisioned by your account with pagination and optional
        status filtering. The returned items include the per-SIM
        `wallet_balance_usd` so you can drive a dashboard without a follow-up
        lookup.
      operationId: listEsims
      parameters:
        - name: status
          in: query
          description: Filter by status
          schema:
            type: string
            enum: [pending, active, suspended, terminated]
        - name: limit
          in: query
          description: Max results per page (up to 100).
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          description: Pagination offset.
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: eSIM list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  esims:
                    type: array
                    items:
                      $ref: '#/components/schemas/EsimListItem'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
                example:
                  esims:
                    - id: 550e8400-e29b-41d4-a716-446655440000
                      iccid: '8999201200000000001'
                      status: active
                      euicc_state: ENABLED
                      end_user_reference: order_789
                      label: Jane Smith - Tokyo
                      wallet_balance_usd: 14.40
                      total_data_charged_usd: 3.60
                      activated_at: '2026-04-07T12:00:00.000Z'
                      terminated_at: null
                      created_at: '2026-04-07T10:30:00.000Z'
                  total: 1
                  limit: 50
                  offset: 0
        '401': { $ref: '#/components/responses/InvalidApiKey' }

  /esim/{iccid}:
    get:
      tags: [eSIMs]
      summary: Get eSIM details
      description: |
        Get full detail for one eSIM, including a live activation state read
        from the network (with graceful fallback to the cached state).

        **`wallet_balance_usd`** is the remaining balance on this SIM's wallet
        in the same units you funded it with. When it hits 0, data stops. See
        the display-precision note in the intro.

        **`total_data_charged_usd`** is the lifetime data usage charged for
        this SIM at the rates returned by `/rates`. Use it as a running meter
        for per-customer throttle or quota logic.
      operationId: getEsim
      parameters:
        - $ref: '#/components/parameters/IccidPath'
      responses:
        '200':
          description: eSIM detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EsimDetail'
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/EsimNotFound' }

  /esim/{iccid}/enable:
    post:
      tags: [eSIMs]
      summary: Enable data on an eSIM
      description: Enable cellular data. Use this to resume connectivity after a disable or suspension.
      operationId: enableEsim
      parameters:
        - $ref: '#/components/parameters/IccidPath'
      responses:
        '200':
          description: Data enabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  iccid: { type: string }
                  data_enabled: { type: boolean }
                example:
                  success: true
                  iccid: '8999201200000000001'
                  data_enabled: true
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '404': { $ref: '#/components/responses/EsimNotFound' }

  /esim/{iccid}/disable:
    post:
      tags: [eSIMs]
      summary: Disable data on an eSIM
      description: Disable cellular data. The eSIM remains provisioned but cannot pass data until re-enabled.
      operationId: disableEsim
      parameters:
        - $ref: '#/components/parameters/IccidPath'
      responses:
        '200':
          description: Data disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  iccid: { type: string }
                  data_enabled: { type: boolean }
                example:
                  success: true
                  iccid: '8999201200000000001'
                  data_enabled: false
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/EsimNotFound' }

  /esim/{iccid}/terminate:
    post:
      tags: [eSIMs]
      summary: Terminate an eSIM
      description: |
        Permanently terminate an eSIM. Disables data and marks the profile as
        terminated. **This action cannot be undone.** Any remaining wallet
        balance is forfeited.
      operationId: terminateEsim
      parameters:
        - $ref: '#/components/parameters/IccidPath'
      responses:
        '200':
          description: eSIM terminated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  iccid: { type: string }
                  status: { type: string, enum: [terminated] }
                example:
                  success: true
                  iccid: '8999201200000000001'
                  status: terminated
        '400': { $ref: '#/components/responses/EsimAlreadyTerminated' }
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/EsimNotFound' }

  /esim/{iccid}/throttle:
    post:
      tags: [eSIMs]
      summary: Throttle SIM speed
      description: |
        Set the data speed on an eSIM. Use this to implement fair-use policies
        (e.g., full speed for the first 2 GB/day, then throttle to 512 Kbps).
        Set to `NO_LIMIT` to restore full speed.

        Valid speeds: `NO_LIMIT`, `SPEED_100_KBPS` through `SPEED_3000_KBPS`
        in 100 Kbps increments, plus `SPEED_3500_KBPS`, `SPEED_4000_KBPS`,
        `SPEED_4500_KBPS`, and `SPEED_5000_KBPS`.

        Common choices:
        - `SPEED_500_KBPS` — basic browsing and messaging
        - `SPEED_1000_KBPS` — standard definition video
        - `SPEED_2000_KBPS` — light HD streaming
      operationId: throttleEsim
      parameters:
        - $ref: '#/components/parameters/IccidPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [speed]
              properties:
                speed:
                  type: string
                  enum:
                    - NO_LIMIT
                    - SPEED_100_KBPS
                    - SPEED_500_KBPS
                    - SPEED_1000_KBPS
                    - SPEED_2000_KBPS
                    - SPEED_5000_KBPS
                  description: Throttle speed. See the full list in the description.
            example:
              speed: SPEED_500_KBPS
      responses:
        '200':
          description: Throttle applied.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  iccid: { type: string }
                  throttle: { type: string }
                example:
                  success: true
                  iccid: '8999201200000000001'
                  throttle: SPEED_500_KBPS
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/EsimNotFound' }

  /esim/{iccid}/fund:
    post:
      tags: [eSIMs]
      summary: Fund an eSIM wallet
      description: |
        Fund an eSIM's data wallet. Deducts from your reseller account balance
        and credits the SIM's individual wallet. The eSIM can use data until
        this funded amount is consumed at the rates shown in `/rates`.

        Your reseller account is **not** debited again during usage — the fund
        call is the only charge. To top a customer up mid-trip, just call
        this endpoint again with the additional amount.

        See the **Display Precision** and **Propagation Delay** notes in the
        intro for details on how the balance number you get back relates to
        the amount you funded.
      operationId: fundEsim
      parameters:
        - $ref: '#/components/parameters/IccidPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: number
                  minimum: 0.01
                  maximum: 10000
                  description: Amount in USD to fund this eSIM (deducted from your reseller account balance).
            example:
              amount: 18
      responses:
        '200':
          description: eSIM funded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  iccid: { type: string }
                  funded_usd: { type: number }
                  wallet_balance_usd: { type: number }
                  account_balance_remaining: { type: number }
                example:
                  success: true
                  iccid: '8999201200000000001'
                  funded_usd: 18.00
                  wallet_balance_usd: 18.00
                  account_balance_remaining: 80.25
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '404': { $ref: '#/components/responses/EsimNotFound' }

  /wallet/balance:
    get:
      tags: [Wallet]
      summary: Get reseller account balance
      description: |
        Check your current reseller account balance and lifetime spending
        statistics.

        `data_suspended` is `true` **only** if Citrus support has manually
        frozen your account. A zero main-account balance does **not** disable
        existing eSIMs — they keep running as long as their per-SIM wallets
        have credit. Use the per-SIM `esim.balance_depleted` webhook to
        detect individual SIM cutoffs.

        `lifetime_usage_usd` is the total you've spent out of your reseller
        account (provisioning fees + every `/fund` call). It doesn't include
        pending top-ups.
      operationId: getWalletBalance
      responses:
        '200':
          description: Wallet balance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletBalance'
        '401': { $ref: '#/components/responses/InvalidApiKey' }

  /rates:
    get:
      tags: [Rates]
      summary: Get per-country data rates
      description: |
        Get per-country data rates — 10% below the retail rates on
        citrusmobile.com. These are the rates charged to your account as
        your customers consume data. Filter by country or continent.
      operationId: getRates
      parameters:
        - name: country
          in: query
          description: Filter by ISO2 code, slug, or name (e.g. `US`, `japan`).
          schema: { type: string }
        - name: continent
          in: query
          description: Filter by continent (e.g. `Europe`, `Asia`).
          schema: { type: string }
      responses:
        '200':
          description: Rate card.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RatesResponse'
              example:
                total_countries: 1
                discount: '10% off retail rates'
                countries:
                  - name: Japan
                    iso2: JP
                    iso3: JPN
                    flag: 🇯🇵
                    continent: Asia
                    has_data: true
                    cheapest_per_gb_usd: 1.38
                    cheapest_operator: KDDI Corporation
                    networks:
                      - { operator: KDDI Corporation, per_mb_usd: 0.0013, per_gb_usd: 1.38 }
                      - { operator: Rakuten, per_mb_usd: 0.0016, per_gb_usd: 1.70 }
                      - { operator: DOCOMO, per_mb_usd: 0.0022, per_gb_usd: 2.30 }
                      - { operator: SoftBank, per_mb_usd: 0.0028, per_gb_usd: 2.95 }
        '401': { $ref: '#/components/responses/InvalidApiKey' }

  /account:
    get:
      tags: [Account]
      summary: Get account details
      description: Returns your account details and eSIM counts.
      operationId: getAccount
      responses:
        '200':
          description: Account detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
              example:
                company_name: TravelConnect Inc.
                contact_email: api@travelconnect.com
                balance_usd: 142.50
                esim_count: { active: 47, total: 123 }
                account_status: active
                created_at: '2026-03-15T09:00:00.000Z'
        '401': { $ref: '#/components/responses/InvalidApiKey' }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      description: List all registered webhook endpoints for your account. Also returns the full set of subscribable event names.
      operationId: listWebhooks
      responses:
        '200':
          description: Webhook list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Webhook'
                  available_events:
                    type: array
                    items: { type: string }
        '401': { $ref: '#/components/responses/InvalidApiKey' }
    post:
      tags: [Webhooks]
      summary: Register a new webhook endpoint
      description: |
        Register a new webhook endpoint. You'll receive a `signing_secret` in
        the response — **save it immediately as it won't be shown again**.
        Maximum 5 webhooks per account.

        Subscribe to specific events or pass `["*"]` to receive all events.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS URL that receives webhook POSTs.
                events:
                  type: array
                  items: { type: string }
                  description: Events to subscribe to, or `["*"]` for all.
            example:
              url: 'https://your-server.com/webhooks/citrus'
              events: [esim.activated, balance.low, balance.depleted]
      responses:
        '201':
          description: Webhook registered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookWithSecret'
              example:
                id: 'wh_550e8400-...'
                url: 'https://your-server.com/webhooks/citrus'
                events: [esim.activated, balance.low, balance.depleted]
                active: true
                signing_secret: 'whsec_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6'
                note: Save the signing_secret — it will not be shown again.
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/InvalidApiKey' }

  /webhooks/{id}:
    get:
      tags: [Webhooks]
      summary: Get webhook detail
      operationId: getWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Webhook detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Webhooks]
      summary: Update a webhook
      description: Update URL, events, or active state. Setting `active` to `true` resets `failure_count` to 0.
      operationId: updateWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string }
                active: { type: boolean }
            example:
              active: true
      responses:
        '200':
          description: Webhook updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      operationId: deleteWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                example:
                  success: true
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/NotFound' }

  /webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send a test event
      description: |
        Send a test event to a webhook endpoint to verify it's receiving and
        processing correctly. The test event uses the special event type
        `"test"` and does not require a subscription to any specific event.
      operationId: testWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Test delivery result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  status: { type: integer, description: The HTTP status code returned by your endpoint. }
                example:
                  success: true
                  status: 200
        '401': { $ref: '#/components/responses/InvalidApiKey' }
        '404': { $ref: '#/components/responses/NotFound' }

webhooks:
  esim.provisioned:
    post:
      summary: eSIM provisioned
      description: Fires after a new eSIM is successfully reserved and assigned to your account.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
            example:
              id: 'evt_a1b2c3d4e5f6'
              event: esim.provisioned
              created_at: '2026-04-07T10:30:00.000Z'
              data:
                iccid: '8999201200000000001'
                lpa_string: 'LPA:1$sm-dp-plus.example.com$ACTIVATION-CODE-EXAMPLE'
                end_user_reference: order_789
                label: Jane Smith - Tokyo
                status: pending
                created_at: '2026-04-07T10:30:00.000Z'
      responses:
        '200':
          description: Your server acknowledges receipt.

  esim.activated:
    post:
      summary: eSIM activated on a device
      description: Fires when a customer installs and boots the eSIM profile for the first time.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
            example:
              id: 'evt_...'
              event: esim.activated
              created_at: '2026-04-07T12:00:00.000Z'
              data:
                iccid: '8999201200000000001'
                activated_at: '2026-04-07T12:00:00.000Z'
                euicc_state: ENABLED
      responses:
        '200':
          description: Acknowledged.

  esim.terminated:
    post:
      summary: eSIM terminated
      description: Fires after a successful `POST /esim/{iccid}/terminate` call.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged.

  esim.data_suspended:
    post:
      summary: eSIM data manually disabled
      description: Fires when data is manually disabled via `POST /esim/{iccid}/disable`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged.

  esim.data_resumed:
    post:
      summary: eSIM data re-enabled
      description: Fires when data is re-enabled via `POST /esim/{iccid}/enable`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged.

  esim.balance_low:
    post:
      summary: SIM wallet balance low
      description: |
        Fires **once** when an individual SIM's wallet balance crosses below $5.
        Use this to prompt your end customer to top up their SIM before it cuts off.
        Fires only on the transition (high → low), not every sync cycle.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
            example:
              id: 'evt_...'
              event: esim.balance_low
              created_at: '2026-04-07T15:00:00.000Z'
              data:
                iccid: '8999201200000000001'
                wallet_balance_usd: 4.25
                threshold_usd: 5.0
      responses:
        '200':
          description: Acknowledged.

  esim.balance_depleted:
    post:
      summary: SIM wallet depleted
      description: |
        Fires when an individual SIM's wallet hits $0. The upstream network
        will stop passing data for that SIM. Call `/esim/{iccid}/fund` to
        restore connectivity, or `/esim/{iccid}/terminate` to clean up.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged.

  balance.low:
    post:
      summary: Reseller account balance low
      description: |
        Fires when your **reseller account** top-up balance drops below $5
        (first crossing only). Use this to know when you won't be able to
        fund new SIMs.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged.

  balance.depleted:
    post:
      summary: Reseller account balance depleted
      description: Fires when your reseller account top-up balance hits $0.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged.

  balance.topped_up:
    post:
      summary: Top-up payment received
      description: Fires after a top-up payment succeeds — either manual or triggered by auto-refill.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
            example:
              id: 'evt_...'
              event: balance.topped_up
              created_at: '2026-04-07T16:00:00.000Z'
              data:
                amount_usd: 100.00
                new_balance_usd: 242.50
      responses:
        '200':
          description: Acknowledged.

  balance.auto_refill_succeeded:
    post:
      summary: Auto-refill charge succeeded
      description: |
        Fires alongside `balance.topped_up` when an auto-refill charge
        succeeds. The `payment_intent_id` lets you correlate with your
        Stripe records.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
            example:
              id: 'evt_...'
              event: balance.auto_refill_succeeded
              created_at: '2026-04-07T16:00:00.000Z'
              data:
                amount_usd: 100.00
                payment_intent_id: 'pi_3OaBc...'
      responses:
        '200':
          description: Acknowledged.

  balance.auto_refill_failed:
    post:
      summary: Auto-refill charge failed
      description: |
        Fires when an auto-refill charge fails (card declined, requires 3DS,
        expired, etc.). **Auto-refill has been turned off** — the reseller
        must complete a manual top-up and re-enable auto-refill.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
            example:
              id: 'evt_...'
              event: balance.auto_refill_failed
              created_at: '2026-04-07T16:00:00.000Z'
              data:
                amount_usd: 100.00
                error_code: card_declined
                error_message: Your card was declined.
      responses:
        '200':
          description: Acknowledged.

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: rsk_a1b2c3d4e5f6...
      description: |
        API key passed as a bearer token. Keys use the `rsk_` prefix and are
        68 characters long. Manage keys at [citrusmobile.com/developer](https://citrusmobile.com/developer).

  parameters:
    IccidPath:
      name: iccid
      in: path
      required: true
      description: The ICCID of the eSIM.
      schema: { type: string, example: '8999201200000000001' }

  schemas:
    ProvisionedEsim:
      type: object
      properties:
        id: { type: string, format: uuid }
        iccid: { type: string }
        lpa_string:
          type: string
          description: The LPA activation code, suitable for generating a QR or for manual entry.
        qr_code:
          type: string
          description: Base64-encoded PNG data URL you can drop directly into an `<img src>`.
        direct_install_url:
          type: string
          description: iOS 17.4+ one-tap install link.
        status: { type: string, enum: [pending, active, suspended, terminated] }
        euicc_state: { type: string, description: 'One of RELEASED, DOWNLOADED, INSTALLED, ENABLED.' }
        end_user_reference: { type: string, nullable: true }
        label: { type: string, nullable: true }
        cost:
          type: number
          description: Provisioning cost deducted from your reseller balance ($1.75).
        balance_remaining:
          type: number
          description: Your reseller account balance after provisioning.
        created_at: { type: string, format: date-time }

    EsimListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        iccid: { type: string }
        status: { type: string }
        euicc_state: { type: string, nullable: true }
        end_user_reference: { type: string, nullable: true }
        label: { type: string, nullable: true }
        wallet_balance_usd:
          type: number
          description: Remaining balance on this SIM's wallet (rounded down to the cent).
        total_data_charged_usd:
          type: number
          description: Lifetime data usage charged for this SIM.
        activated_at: { type: string, format: date-time, nullable: true }
        terminated_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    EsimDetail:
      allOf:
        - $ref: '#/components/schemas/EsimListItem'
        - type: object
          properties:
            lpa_string: { type: string }

    WalletBalance:
      type: object
      properties:
        balance_usd: { type: number, description: Current reseller account balance. }
        lifetime_topup_usd: { type: number }
        lifetime_usage_usd:
          type: number
          description: Total spent on provisioning fees and `/fund` calls. Does not include pending top-ups.
        data_suspended:
          type: boolean
          description: True only if Citrus support has manually frozen your account. A zero balance does NOT auto-suspend existing eSIMs.
        currency: { type: string, example: USD }
      example:
        balance_usd: 142.50
        lifetime_topup_usd: 500.00
        lifetime_usage_usd: 357.50
        data_suspended: false
        currency: USD

    RatesResponse:
      type: object
      properties:
        total_countries: { type: integer }
        discount: { type: string, example: '10% off retail rates' }
        countries:
          type: array
          items: { $ref: '#/components/schemas/Country' }

    Country:
      type: object
      properties:
        name: { type: string }
        iso2: { type: string }
        iso3: { type: string }
        flag: { type: string }
        continent: { type: string }
        has_data: { type: boolean }
        cheapest_per_gb_usd: { type: number }
        cheapest_operator: { type: string }
        networks:
          type: array
          items: { $ref: '#/components/schemas/NetworkRate' }

    NetworkRate:
      type: object
      properties:
        operator: { type: string }
        per_mb_usd: { type: number }
        per_gb_usd: { type: number }

    Account:
      type: object
      properties:
        company_name: { type: string, nullable: true }
        contact_email: { type: string }
        balance_usd: { type: number }
        esim_count:
          type: object
          properties:
            active: { type: integer }
            total: { type: integer }
        account_status: { type: string }
        created_at: { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        active: { type: boolean }
        failure_count:
          type: integer
          description: Consecutive delivery failures. Auto-disables at 10.
        last_triggered_at: { type: string, format: date-time, nullable: true }
        last_failure_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    WebhookWithSecret:
      allOf:
        - $ref: '#/components/schemas/Webhook'
        - type: object
          properties:
            signing_secret:
              type: string
              description: HMAC-SHA256 signing secret. Save this — it is only returned once.
            note: { type: string }

    WebhookEnvelope:
      type: object
      description: |
        Every webhook delivery follows the same envelope shape. Verify the
        signature using HMAC-SHA256 of the raw request body and the
        `signing_secret` from webhook registration, compared against the
        `X-Citrus-Signature` header.
      properties:
        id: { type: string, description: Unique event ID (`evt_...`). }
        event: { type: string, description: The event type (e.g. `esim.provisioned`). }
        created_at: { type: string, format: date-time }
        data:
          type: object
          description: Per-event payload. See individual webhook definitions for the shape.

    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string }
        message: { type: string }

  responses:
    InvalidApiKey:
      description: API key is missing, invalid, or revoked.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: Invalid API key
            code: INVALID_API_KEY

    InsufficientBalance:
      description: Reseller balance is insufficient for the requested operation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: Insufficient balance
            code: INSUFFICIENT_BALANCE
            message: Requires $1.75 but your balance is $0.50.

    EsimNotFound:
      description: ICCID not found or doesn't belong to your account.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: eSIM not found
            code: ESIM_NOT_FOUND

    EsimAlreadyTerminated:
      description: eSIM is already terminated. Cannot un-terminate; provision a new one.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: eSIM is already terminated
            code: ESIM_ALREADY_TERMINATED

    NoEsimsAvailable:
      description: eSIM inventory depleted. Try again later or contact support.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: No eSIMs available in inventory. Please try again later.
            code: NO_ESIMS_AVAILABLE

    ValidationError:
      description: Invalid request body or parameters.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: 'Amount must be between $0.01 and $10,000'
            code: VALIDATION_ERROR

    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: Not found
            code: NOT_FOUND

    RateLimited:
      description: Too many requests — 100 req/min per API key.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: Rate limit exceeded
            code: RATE_LIMITED
            message: Too many requests. Please retry after 30 seconds.
