openapi: 3.1.0
info:
  title: co:sim Travel eSIM API
  version: "1.0"
  description: |
    Help travelers find and buy affordable eSIM data plans for 200+ countries.

    **Workflow for AI agents:**
    1. Search destinations by country name or region
    2. List and compare packages (data, duration, price, network)
    3. Optionally calculate price with discount codes
    4. Create a checkout link — share it with the user
    5. The user clicks the link to review the order and pay via Stripe
    6. eSIM QR code is delivered to their email

    A checkout is never created below the platform minimum every payment method
    requires — that request is refused with `ORDER_BELOW_MINIMUM` instead, so a
    `checkoutUrl` you receive is always payable.

    All browse endpoints require no authentication. The checkout endpoint is rate-limited by IP.

    **MCP server** — MCP-capable agents can connect to https://cosim.io/mcp
    (Model Context Protocol, Streamable HTTP, no auth) for typed tools covering
    the same catalog and checkout capabilities. Registry name: io.cosim/esim.

    **Required: User-Agent header** — All requests must include a User-Agent identifying your client and model:
    `{ClientName}/{ClientVersion} ({Model}/{ModelVersion})`
    Example: `ChatGPT-Plugins/1.2.0 (gpt-4o/2024-08-06)`

    Full documentation: https://cosim.io/llms-full.txt
  contact:
    name: co:sim Support
    url: https://cosim.io
    email: support@cosim.io
  # Short inline guidance for agent-discovery crawlers (agentcash `info.x-guidance`).
  # The long-form guide lives in llms-full.txt; keep this one paragraph-sized.
  x-guidance: >-
    Browse the catalog with GET /api/v1/esim/locations and
    /api/v1/esim/locations/{code}/packages, or let GET /api/v1/esim/recommend pick
    plans for a trip. To buy, POST /api/v1/ai/checkout with a packageId and the
    traveller's email; it returns a checkoutUrl a human can pay at, plus a
    checkoutToken. A total under the platform minimum is refused at creation
    (`ORDER_BELOW_MINIMUM`) rather than sold, so any checkoutUrl you hold is
    payable. An agent holding an MPP or x402 wallet can instead pay that
    order machine-to-machine at POST /api/v1/ai/checkout/{token}/mpp or
    /api/v1/ai/checkout/{token}/x402, which answer HTTP 402 with a challenge for
    that order's exact total. Payment is always order-scoped: the checkoutToken
    must exist before either 402 endpoint can quote a price, so probing the
    literal {token} template returns 400, not a challenge. Poll
    /api/v1/ai/checkout/{token}/status until `completed`; the eSIM QR code is
    emailed to the address on the order. Always relay the response's refundPolicy
    and installValidity to the user before paying.

# Namespaced agentcash discovery extensions. `ownershipProofs` lists artifacts
# served from this origin that tie it to the operator of record; discovery
# clients surface them as provenance, they are not a substitute for the
# registry's own checks.
x-agentcash-provenance:
  ownershipProofs:
    - https://cosim.io/.well-known/api-catalog
    - https://cosim.io/.well-known/ai-plugin.json
    - mailto:support@cosim.io

x-agentcash-guidance:
  llmsTxtUrl: https://cosim.io/llms.txt

# The server URL is the bare origin and every `paths` key carries the full
# `/api/v1` prefix. Agent-discovery clients resolve an endpoint by looking the
# request pathname up in `paths` WITHOUT re-applying a server base path, so a
# non-root `servers[0].url` makes every route unresolvable at endpoint-detail
# level even though the route list itself looks correct.
servers:
  - url: https://cosim.io
    description: Production

paths:
  /api/v1/esim/locations:
    get:
      operationId: searchDestinations
      security: []
      summary: Search eSIM destinations by country name or region
      description: |
        Returns destinations with package counts and starting prices.
        Supports multilingual search — try "Japan", "日本", "Japon", "일본".
        Results are sorted by relevance to the user's location.
      tags: [Browse]
      parameters:
        - name: search
          in: query
          description: Country or region name in any language.
          schema:
            type: string
          example: Japan
        - name: region
          in: query
          description: >-
            Filter by geographic region. Use `regional` to list only
            multi-country packages; `global` covers worldwide packages.
          schema:
            type: string
            enum: [asia, europe, americas, oceania, africa, middle-east, global, other, regional]
      responses:
        "200":
          description: List of matching destinations.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                              description: "Destination code (e.g. 'JP', 'EU-42')."
                            name:
                              type: string
                            region:
                              type: string
                            isRegional:
                              type: boolean
                            packageCount:
                              type: integer
                            startPrice:
                              type: object
                              nullable: true
                              properties:
                                amount:
                                  type: number
                                currency:
                                  type: string
                      regions:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                            name:
                              type: string
                            count:
                              type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"

  /api/v1/esim/locations/{code}/packages:
    get:
      operationId: listPackages
      security: []
      summary: List all eSIM packages for a destination
      description: |
        Returns detailed plan information. Use the package `id` in calculatePrice or createCheckout.
        Key fields to present to users: dataVolume.display, duration.days, price.amount, speed, supportTopup.
      tags: [Browse]
      parameters:
        - name: code
          in: path
          required: true
          description: "Destination code (e.g. 'JP', 'TH', 'EU-42')."
          schema:
            type: string
        - name: sort
          in: query
          description: Sort order for packages.
          schema:
            type: string
            enum: [price, data, validity]
            default: price
        - name: data_type
          in: query
          description: Filter by data type.
          schema:
            type: string
            enum: [total, daily]
      responses:
        "200":
          description: Packages for the destination.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      location:
                        type: object
                        properties:
                          code:
                            type: string
                          name:
                            type: string
                          region:
                            type: string
                          isRegional:
                            type: boolean
                      packages:
                        type: array
                        items:
                          $ref: "#/components/schemas/PackageSummary"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Destination not found.

  /api/v1/esim/packages/{id}:
    get:
      operationId: getPackage
      security: []
      summary: Get detailed info about a specific eSIM package
      tags: [Browse]
      parameters:
        - name: id
          in: path
          required: true
          description: Package ID or slug.
          schema:
            type: string
      responses:
        "200":
          description: Package details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      slug:
                        type: string
                        nullable: true
                      name:
                        type: string
                      destination:
                        type: object
                        properties:
                          code:
                            type: string
                          name:
                            type: string
                          region:
                            type: string
                      coverage:
                        type: array
                        items:
                          type: string
                        description: >-
                          ISO-3166 alpha-2 codes covered by this plan, deduplicated and
                          uppercase. An EMPTY array means coverage is unknown for this
                          plan — never treat it as "covers everything", and do not
                          substitute the destination code, which for a regional plan is a
                          bundle id (EU-33) rather than a country.
                      operatorsByCountry:
                        type: object
                        additionalProperties:
                          type: array
                          items:
                            type: object
                            properties:
                              name:
                                type: string
                              network:
                                type: string
                                description: e.g. "5G", "4G", "3G/4G". Empty when the supplier omits it.
                        description: >-
                          Host carriers per covered country code. Best-effort supplier
                          metadata: a code present in `coverage` may be absent here, which
                          means "no carrier data", NOT "not covered". `coverage` is the
                          authority on coverage.
                      dataVolume:
                        type: object
                        properties:
                          mb:
                            type: integer
                          display:
                            type: string
                          type:
                            type: string
                          description:
                            type: string
                      validity:
                        type: object
                        properties:
                          days:
                            type: integer
                          display:
                            type: string
                      price:
                        type: object
                        properties:
                          amount:
                            type: number
                          currency:
                            type: string
                          formatted:
                            type: string
                            description: "Formatted price string (e.g. '$3.50')."
                      network:
                        type: object
                        properties:
                          type:
                            type: string
                            nullable: true
                          speed:
                            type: string
                            nullable: true
                      features:
                        type: object
                        properties:
                          supportTopup:
                            type: boolean
                          billingStart:
                            type: string
                            nullable: true
                            description: >-
                              first_connection | first_install | purchase.
                              'purchase' bills immediately and is non-refundable, and has NO
                              activation deadline — its validity runs from payment, so do not
                              quote an install-by date for it.
                      voice:
                        type: object
                        nullable: true
                        description: >-
                          Calling / messaging entitlements. null for an ordinary data-only plan,
                          which is almost all of them. Do not state that a plan has no voice or
                          SMS without checking this field.
                        properties:
                          localCalls:
                            type: string
                            nullable: true
                            description: "'unlimited' when calls within the destination are included."
                          intlMinutes:
                            type: integer
                            nullable: true
                          intlDestinations:
                            type: array
                            description: ISO-3166 alpha-2 codes the international minutes cover.
                            items:
                              type: string
                          sms:
                            type: string
                            nullable: true
                            description: "'unlimited' when SMS/MMS are included."
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Package not found.

  /api/v1/esim/recommend:
    get:
      operationId: recommendPlans
      security: []
      summary: Recommend up to 3 plans for a trip (cheapest / best value / most data)
      description: |
        Needs-driven plan selector. Provide trip countries, duration, and data appetite;
        the engine filters the full catalog to plans that cover all requested countries
        and have enough data for the trip, then returns up to 3 distinct slots.

        The `checkoutPath` in each slot can be shared directly with the user — just
        prepend the locale prefix (e.g. `/en${checkoutPath}`).

        Rate limit: 30 requests per minute per IP.
      tags: [Browse]
      parameters:
        - name: countries
          in: query
          required: true
          description: >-
            Comma-separated ISO-3166-1 alpha-2 country codes (e.g. "JP,KR").
            1–10 countries. Matched against plan coverage.
          schema:
            type: string
            maxLength: 40
          example: JP,KR
        - name: days
          in: query
          required: true
          description: Trip duration in days (1–90).
          schema:
            type: integer
            minimum: 1
            maximum: 90
          example: 7
        - name: usage
          in: query
          required: false
          description: >-
            Data appetite tier. light ≈ maps/messaging (~0.3 GB/day),
            medium ≈ social/photos (~0.7 GB/day, default),
            heavy ≈ video/hotspot (~1.5 GB/day).
          schema:
            type: string
            enum: [light, medium, heavy]
            default: medium
      responses:
        "200":
          description: Recommendation result. `slots` is empty when no plan covers the trip.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      slots:
                        type: array
                        description: Up to 3 distinct recommendation slots.
                        items:
                          type: object
                          properties:
                            slot:
                              type: string
                              enum: [cheapest, best_value, most_data]
                            plan:
                              type: object
                              properties:
                                id:
                                  type: string
                                slug:
                                  type: string
                                  nullable: true
                                name:
                                  type: string
                                destinationCode:
                                  type: string
                                dataDisplay:
                                  type: string
                                  description: "Human-readable data amount (e.g. '5GB', '1GB/day')."
                                dataType:
                                  type: string
                                  description: "total | daily_throttle | daily_cutoff | daily_unlimited"
                                validityDays:
                                  type: integer
                                preInstallDays:
                                  type: integer
                                  nullable: true
                                  description: >-
                                    Days from purchase within which the eSIM must be
                                    installed AND activated. Null when the plan has no
                                    activation window at all (billingStart 'purchase');
                                    a null means "quote no deadline", never zero days.
                                billingStart:
                                  type: string
                                  nullable: true
                                  enum: [first_connection, first_install, purchase]
                                  description: >-
                                    When the validity timer starts. Shipped alongside
                                    preInstallDays so a null there is interpretable:
                                    on 'purchase' it means no activation deadline
                                    exists, which is a different statement from the
                                    window merely being unknown.
                            usableGb:
                              type: number
                              description: Total usable GB for this trip (daily plans × days).
                            tripPriceUsd:
                              type: number
                              description: Effective price for this trip after pricing-program discounts.
                            pricePerGb:
                              type: number
                              description: tripPriceUsd / usableGb.
                            coverageCount:
                              type: integer
                              description: Number of countries this plan covers.
                            redundancy:
                              type: number
                              description: "0 = exact fit; >0 = plan covers more countries than requested."
                            checkoutPath:
                              type: string
                              description: >-
                                Relative path to the checkout page. Prepend locale prefix before sharing
                                (e.g. /en/data-only/jp/checkout?plan=<id>). Daily plans include &days=<n>.
                            detailsPath:
                              type: string
                              description: Relative path to the destination detail page.
                      meta:
                        type: object
                        properties:
                          countries:
                            type: array
                            items:
                              type: string
                          days:
                            type: integer
                          usage:
                            type: string
                          candidateCount:
                            type: integer
                            description: Number of plans that passed the coverage/tier/validity filter.
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error (invalid countries, days out of range, unknown usage tier).
        "429":
          description: Rate limit exceeded (30 req/min per IP).

  /api/v1/orders/data-only/calculate:
    post:
      operationId: calculatePrice
      security: []
      summary: Calculate order price with optional discount codes
      description: |
        Use this to show the user a price breakdown before creating a checkout.
        Supports quantity and custom days for daily plans.
        Batch mode: send packageIds (array, max 250) instead of packageId to
        get pricing for multiple plans in one call. When daily plans in the
        same batch need different durations, send packageRequests with a
        packageId and optional customDays per item. Response wraps per-plan
        results in a packages[] array preserving input order.
        Rejected discounts are returned only for codes explicitly supplied in
        discountCodes. Automatic promotion failures are never enumerated.
        Rate limit: 60 requests per minute per IP.
      tags: [Order]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                packageId:
                  type: string
                  description: Package ID from listPackages (single mode).
                packageIds:
                  type: array
                  items:
                    type: string
                  maxItems: 250
                  description: Package IDs for batch pricing (mutually exclusive with packageId and packageRequests).
                packageRequests:
                  type: array
                  minItems: 1
                  maxItems: 250
                  description: Per-package batch pricing inputs (mutually exclusive with packageId and packageIds).
                  items:
                    type: object
                    required: [packageId]
                    properties:
                      packageId:
                        type: string
                      customDays:
                        type: integer
                        minimum: 1
                        maximum: 365
                        description: Custom duration for this daily plan only.
                quantity:
                  type: integer
                  default: 1
                customDays:
                  type: integer
                  description: Custom validity days (for daily plans only).
                discountCodes:
                  type: array
                  items:
                    type: string
                  description: Discount/coupon codes to apply.
      responses:
        "200":
          description: Price calculation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      package:
                        type: object
                        properties:
                          id:
                            type: string
                          name:
                            type: string
                          destination:
                            type: object
                            properties:
                              id:
                                type: string
                              name:
                                type: string
                              region:
                                type: string
                          data:
                            type: object
                            properties:
                              amount:
                                type: number
                              unit:
                                type: string
                              type:
                                type: string
                          validity:
                            type: object
                            properties:
                              days:
                                type: integer
                          quantity:
                            type: integer
                          unitPrice:
                            type: number
                          subtotal:
                            type: number
                          preInstall:
                            type: object
                            description: >-
                              Activation window for this quote, in days from payment.
                              `base` is the plan default and `effective` swaps in the
                              smallest applied coupon override, so an override can
                              shorten OR extend the window. BOTH are null when the plan
                              has no activation window at all — i.e. its billingStart is
                              'purchase'. A null means "quote no deadline", never zero
                              days and never "unknown".
                            properties:
                              base:
                                type: integer
                                nullable: true
                              effective:
                                type: integer
                                nullable: true
                      discounts:
                        type: object
                        properties:
                          applied:
                            type: array
                            items:
                              type: object
                              properties:
                                code:
                                  type: string
                                displayName:
                                  type: string
                                discountAmount:
                                  type: number
                          rejected:
                            type: array
                            description: Rejections for explicitly submitted discountCodes only.
                            items:
                              type: object
                              properties:
                                code:
                                  type: string
                                errorCode:
                                  type: string
                                message:
                                  type: string
                                estimatedDiscount:
                                  type: number
                          loginOffer:
                            type: object
                            description: Aggregated guest offer available after login; contains no coupon identifiers or rules.
                            properties:
                              estimatedDiscount:
                                type: number
                              preInstallDays:
                                type: integer
                                description: >-
                                  Activation window this offer carries, in days from payment.
                                  Quote it alongside the offer's price: it is what the order
                                  will be created with once the customer signs in, and it can
                                  differ from the plan default in either direction. OMITTED
                                  entirely (never null, never 0) for a plan that has no
                                  activation window — i.e. one whose billingStart is
                                  'purchase'. Absence means "quote no deadline", not "unknown".
                          total:
                            type: number
                      summary:
                        type: object
                        properties:
                          subtotal:
                            type: number
                          discountAmount:
                            type: number
                          total:
                            type: number
                          currency:
                            type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "429":
          description: Rate limit exceeded (60 req/min per IP).

  /api/v1/ai/checkout:
    post:
      operationId: createCheckout
      security: []
      summary: Create a checkout link for the user to pay
      description: |
        Creates an order and returns a checkout URL. Share this URL with the user.
        The user opens the link, reviews the order, selects a payment method, and pays.
        The eSIM QR code is delivered to the provided email after payment.

        A total under the platform minimum is refused here with
        `ORDER_BELOW_MINIMUM` rather than sold, so a `checkoutUrl` in a 201 is
        always payable and needs no check of your own.

        Rate limit: 10 requests per 5 minutes per IP.
        Orders expire after 30 minutes if not paid.
      tags: [Checkout]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [packageId, email]
              properties:
                packageId:
                  type: string
                  description: Package ID or slug from listPackages.
                email:
                  type: string
                  format: email
                  description: Email address for eSIM QR code delivery.
                quantity:
                  type: integer
                  default: 1
                  maximum: 10
                customDays:
                  type: integer
                  description: Custom validity days for daily plans.
                discountCode:
                  type: string
                  description: Optional discount code.
                locale:
                  type: string
                  enum: [en, zh-CN, zh-TW, ja, ko, fr]
                  default: en
                  description: Language for checkout page and emails.
      responses:
        "201":
          description: Checkout created. Present checkoutUrl to the user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      checkoutUrl:
                        type: string
                        format: uri
                        description: >-
                          URL for the user to complete payment. Always payable: a total under
                          the platform minimum is refused at creation, so it never reaches this
                          response.
                      checkoutToken:
                        type: string
                        description: Token to poll status via getCheckoutStatus.
                      expiresAt:
                        type: string
                        format: date-time
                      expiresInMinutes:
                        type: integer
                        description: Minutes until the checkout link expires.
                      summary:
                        type: object
                        properties:
                          orderId:
                            type: string
                          orderNumber:
                            type: string
                          destination:
                            type: string
                          destinationCode:
                            type: string
                          plan:
                            type: string
                            description: "Data volume display (e.g. '5GB')."
                          duration:
                            type: string
                            description: "Validity period (e.g. '7 days')."
                          networkType:
                            type: string
                          billingStart:
                            type: string
                            enum: [first_connection, first_install, purchase]
                            description: |
                              When billing begins. "purchase" means the plan starts immediately
                              and is non-refundable. Check refundPolicy.eligible.
                          quantity:
                            type: integer
                          unitPrice:
                            type: string
                            description: "Formatted price (e.g. '$3.50')."
                          subtotal:
                            type: string
                          discount:
                            type: string
                            description: "Formatted discount (e.g. '-$1.00'). Only present when discount applied."
                          total:
                            type: string
                          currency:
                            type: string
                          email:
                            type: string
                      installValidity:
                        type: object
                        description: |
                          IMPORTANT: relay `note` to the user before checkout.
                          WHEN `days` is a number: the eSIM must be installed AND activated
                          within that many days of purchase (coupons can shorten this); an eSIM
                          still unactivated after the deadline is automatically cancelled even if
                          it was already installed on a device, which also ends refund
                          eligibility.
                          WHEN `days` and `installBy` are null: this plan has NO activation
                          deadline at all — that is every plan whose billingStart is 'purchase',
                          whose validity runs from payment and which nothing cancels. Do not
                          state or imply a deadline for those, and never substitute a default:
                          null is not zero days and not "unknown".
                        properties:
                          days:
                            type: integer
                            nullable: true
                            description: Days from payment within which the eSIM must be installed and activated.
                          installBy:
                            type: string
                            nullable: true
                            description: Absolute deadline (YYYY-MM-DD). Estimate before payment; exact after.
                          installByIsEstimate:
                            type: boolean
                            description: true before payment (assumes purchase today), false once paid.
                          note:
                            type: string
                            description: Agent-facing sentence to relay to the user verbatim.
                      refundPolicy:
                        type: object
                        description: |
                          IMPORTANT: AI agents MUST present this refund policy to the user
                          before they click the checkout link. The mustInformUser flag is always true.
                          When eligible is false, the order is non-refundable (billing starts at
                          purchase, or the item is free).
                        properties:
                          eligible:
                            type: boolean
                            description: |
                              Whether this order is eligible for refund. false for immediate-billing
                              plans (billingStart: "purchase") and free (100%-discounted) items.
                              Always check this field.
                          reasonCode:
                            type: string
                            enum: [billed_at_purchase, free_item, refundable]
                            description: Machine-readable reason behind `eligible`.
                          reason:
                            type: string
                            description: Human-readable explanation of the refund policy for this order.
                          windowDays:
                            type: integer
                            nullable: true
                            description: |
                              Effective refund window in days from payment — already capped by the
                              install deadline (min of the 30-day guarantee and installValidity.days).
                          refundableUntil:
                            type: string
                            nullable: true
                            description: Absolute refund deadline (YYYY-MM-DD). Estimate before payment; exact after.
                          refundableUntilIsEstimate:
                            type: boolean
                          guestOrderNote:
                            type: string
                            description: |
                              How AI/guest orders request a refund. Primary path: the user signs in
                              (or creates an account) at cosim.io with the same email this order was
                              placed with — past guest orders are automatically claimed on first
                              sign-in, after which the user can refund eligible eSIMs from "My eSIMs"
                              without contacting support. Live chat at cosim.io remains a fallback
                              for users who cannot or do not want to sign in.
                          processingTime:
                            type: string
                          mustInformUser:
                            type: boolean
                            description: Always true. Agents must share this info before checkout.
                      machinePayment:
                        type: object
                        description: |
                          Present only when machine payment rails are live AND this order's
                          total is within the `x-payment-info.price` min/max bounds published
                          on the protocol operations. Its absence on
                          a paid order therefore means the rails would refuse this one — do not
                          call them, share checkoutUrl instead. Only the `max` side is reachable
                          on an order this API created: a total under the published `min` is
                          refused at creation with `ORDER_BELOW_MINIMUM`, so it never reaches a
                          201. Agents that control an x402- or
                          MPP-capable wallet may pay the order programmatically at these
                          HTTP-402 endpoints (USD, settled in USDC on crypto rails) — but
                          ONLY with the user's explicit consent, after relaying the total and
                          refund policy. All other agents ignore this block and share
                          checkoutUrl.
                        properties:
                          note:
                            type: string
                          protocols:
                            type: object
                            properties:
                              x402:
                                type: object
                                properties:
                                  url:
                                    type: string
                                    description: x402 protocol endpoint for this order.
                                  network:
                                    type: string
                                    description: CAIP-2 network id (eip155:8453 = Base mainnet).
                                  asset:
                                    type: string
                                    enum: [USDC]
                              mpp:
                                type: object
                                properties:
                                  url:
                                    type: string
                                    description: MPP protocol endpoint for this order.
                                  rails:
                                    type: array
                                    items:
                                      type: string
                                      enum: [tempo_usdc, spt]
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: |
            Validation error, or `ORDER_BELOW_MINIMUM` — discounts brought the
            total under the platform minimum every payment method requires, so
            no order was created. `error.details` carries `total`, `minimum` and
            `currency`. The discount is not necessarily a `discountCode` you
            sent: pricing programs and automatic promotions apply on their own,
            so retry with a different plan or quantity rather than by omitting
            a code.
        "404":
          description: Package not found.
        "429":
          description: Rate limit exceeded.

  /api/v1/ai/checkout/{token}/x402:
    post:
      operationId: payCheckoutX402
      security: []
      # The exact amount is the order total established by createCheckout, so
      # the live 402 challenge stays the source of truth for it. The BOUNDS are
      # not advisory: `loadMachineOrder` answers 409 `amount-under-minimum`
      # below `min` and 409 `amount-over-limit` above `max`, and the create
      # response withholds its `machinePayment` block for either. Both come from
      # `MACHINE_PAYMENT_*_ORDER_TOTAL_USD` in lib/payments/machine/config.ts —
      # where `min` is itself derived from the rails' configured `minAmount` in
      # lib/payments/config.ts — and a guard test fails if they drift. Never
      # edit one side alone: widening a bound here without moving the constant
      # hands an agent that pre-authorized it a challenge it never agreed to.
      x-payment-info:
        price:
          mode: dynamic
          currency: USD
          min: "0.99"
          max: "2000.00"
        protocols:
          - x402: {}
      summary: Pay an order via the x402 protocol (machine payment, Base USDC)
      description: |
        HTTP-402 machine payment endpoint. First request (no payment credential)
        returns 402 Payment Required with a base64 PAYMENT-REQUIRED header naming
        a one-time Stripe deposit address for this order (exact scheme, order
        total in USDC on Base). Pay with any x402 client and retry with the
        PAYMENT-SIGNATURE credential to receive a receipt. Settlement is
        confirmed on-chain asynchronously — poll the status endpoint until
        `completed`. Requires the user's explicit consent before paying.
      tags: [AI Checkout]
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
        - name: PAYMENT-SIGNATURE
          in: header
          required: false
          description: >-
            The x402 credential, on the RETRY leg only. Absent on the first
            request, which is what produces the 402 challenge. `X-PAYMENT` is
            accepted as an alias. There is no request body: the credential is
            the entire input.
          schema:
            type: string
      responses:
        "200":
          description: Payment accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MachineReceipt"
        "402":
          description: |
            Payment required. Emitted by the x402 protocol layer, so the BODY IS
            EMPTY (`{}`, `application/json`) — everything payable is in the
            `PAYMENT-REQUIRED` header. Do not parse this as problem+json. The
            middleware also falls back to this status and body if settlement
            throws unexpectedly, in which case the header is absent — treat a
            402 without it as a retryable failure, not as a challenge.
          headers:
            PAYMENT-REQUIRED:
              description: >-
                Base64 x402 v2 payment requirements. `accepts[].amount` is in
                token atomic units, and `resource.url` is this endpoint's public
                URL — a client that pins the resource can check it matches.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                description: Always empty; the challenge is the header.
        "400":
          description: Malformed checkout token (`invalid-token`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "404":
          description: Order not found, or the x402 rail is not enabled.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "409":
          description: |
            Order already processed / free order (nothing to pay) / order total
            outside the published `x-payment-info.price` bounds / a coupon on
            the order restricts payment methods away from machine rails
            (application/problem+json with a type ending in the specific slug).
            Only `amount-over-limit` and `coupon-method-restricted` are payable
            via the human checkoutUrl. `amount-under-minimum` and `free-order`
            are NOT: every payment method enforces the same platform minimum and
            the checkout page has no $0 branch, so those orders have to be
            recreated for a different plan or quantity — pricing programs and
            automatic promotions apply with no `discountCode`, so omitting one
            need not change the total. Read the specific slug — do not fall back
            to checkoutUrl for every 409.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "410":
          description: Order expired — create a new checkout.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "429":
          description: Rate limit exceeded.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "502":
          description: |
            Payment could not be processed; the order remains unpaid. TWO shapes:
            our own problem+json, and — when the x402 facilitator's verify or
            settle call fails — a plain `application/json` `{ "error": "..." }`
            emitted by the protocol middleware, which does not pass through our
            error handling.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
            application/json:
              schema:
                type: object
                description: Facilitator failure, straight from the x402 middleware.
                properties:
                  error:
                    type: string

  /api/v1/ai/checkout/{token}/mpp:
    post:
      operationId: payCheckoutMpp
      security: []
      # See the x402 operation above for where these bounds come from and why
      # they are enforced rather than advisory. Only the always-on Tempo rail is
      # declared here; the live challenge additionally offers `method="stripe"`
      # (SPT) when that rail is configured.
      x-payment-info:
        price:
          mode: dynamic
          currency: USD
          min: "0.99"
          max: "2000.00"
        protocols:
          - mpp:
              method: tempo
              intent: charge
              currency: USD
      summary: Pay an order via MPP (machine payment, Tempo USDC or SPT card/Link)
      description: |
        MPP (Machine Payments Protocol) endpoint. First request returns 402 with
        WWW-Authenticate Payment challenges — Tempo USDC (crypto) and, when
        configured, stripe SPT (Shared Payment Token fiat rail). Pay with an MPP
        client (mppx) and retry with the Authorization Payment credential. SPT
        settles synchronously; crypto settles on-chain asynchronously. Poll the
        status endpoint until `completed`. Requires the user's explicit consent
        before paying.
      tags: [AI Checkout]
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
        - name: Authorization
          in: header
          required: false
          description: >-
            The MPP credential, `Payment <credential>`, on the RETRY leg only.
            Absent on the first request, which is what produces the 402
            challenge. There is no request body: the credential is the entire
            input.
          schema:
            type: string
      responses:
        "200":
          description: Payment accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MachineReceipt"
        "402":
          description: |
            Payment required. Emitted by the MPP protocol layer, so the body is
            a paymentauth.org problem rather than one of our machine-payment
            slugs, and the payable content is in `WWW-Authenticate`.
          headers:
            WWW-Authenticate:
              description: >-
                One `Payment` challenge per offered method, comma-separated.
                Each carries `id`, `realm`, `method` (`tempo` or `stripe`),
                `intent`, a base64 `request` and `expires`.
              schema:
                type: string
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PaymentChallengeProblem"
        "400":
          description: Malformed checkout token (`invalid-token`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "404":
          description: Order not found, or the MPP rail is not enabled.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "409":
          description: |
            Order already processed / free order / order total outside the
            published `x-payment-info.price` bounds / coupon method restriction.
            Only `amount-over-limit` and `coupon-method-restricted` are payable
            via the human checkoutUrl; `amount-under-minimum` and `free-order`
            have to be recreated for a different plan or quantity instead. See
            the x402 operation's 409 for the full breakdown.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "410":
          description: Order expired — create a new checkout.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "429":
          description: Rate limit exceeded.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"
        "502":
          description: Payment could not be processed (order remains unpaid).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/MachineProblem"

  /api/v1/ai/checkout/{token}/status:
    get:
      operationId: getCheckoutStatus
      security: []
      summary: Check if the user has completed payment
      description: |
        Poll this endpoint after sharing the checkout link.
        - `awaiting_payment` — waiting for user to open the link and pay
        - `paid` — payment received, eSIM being provisioned
        - `completed` — eSIM delivered to email
        - `expired` — link expired, create a new checkout
        - `cancelled` — order was cancelled
        - `failed` — payment or provisioning failed; user can retry
        - `needs_review` — payment was taken but auto-refund failed during a coupon-abuse check; ops will resolve manually within one business day. Stop polling and direct the user to contact support@cosim.io with the orderNumber.
      tags: [Checkout]
      parameters:
        - name: token
          in: path
          required: true
          description: The checkoutToken from createCheckout.
          schema:
            type: string
      responses:
        "200":
          description: Current checkout status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      orderNumber:
                        type: string
                      status:
                        type: string
                        enum: [awaiting_payment, paid, completed, expired, cancelled, failed, needs_review]
                      total:
                        type: string
                        description: "Formatted total (e.g. '$9.99')."
                      currency:
                        type: string
                        nullable: true
                      email:
                        type: string
                        nullable: true
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            name:
                              type: string
                            description:
                              type: string
                              nullable: true
                            quantity:
                              type: integer
                            unitPrice:
                              type: string
                            total:
                              type: string
                      paidAt:
                        type: string
                        format: date-time
                        nullable: true
                      createdAt:
                        type: string
                        format: date-time
                        nullable: true
                      expiresAt:
                        type: string
                        format: date-time
                        nullable: true
                      installValidity:
                        type: object
                        description: |
                          Activation-deadline info. After payment `installBy` is the exact date
                          (from the fulfilled eSIM profile) — relay it to the user, e.g.
                          "install and activate by 2026-08-01; still unactivated after that and
                          the eSIM is cancelled, even if it was installed".
                          `days` and `installBy` are BOTH null when the plan has no activation
                          deadline at all (billingStart 'purchase'), and payment does not change
                          that — there is no exact date to return for those, because none exists.
                          Relay `note` instead, and never present a null as zero days.
                        properties:
                          days:
                            type: integer
                            nullable: true
                          installBy:
                            type: string
                            nullable: true
                          installByIsEstimate:
                            type: boolean
                          note:
                            type: string
                      refundPolicy:
                        type: object
                        description: |
                          Same shape as createCheckout's refundPolicy. After payment
                          `refundableUntil` is exact (min of 30-day guarantee and install deadline).
                  meta:
                    $ref: "#/components/schemas/Meta"
        "404":
          description: Checkout token not found.

  /api/v1/ai/checkout/{token}/pay:
    post:
      operationId: payCheckout
      security: []
      summary: Initiate payment for an AI checkout order
      description: |
        Called from the AI checkout page when the user clicks "Pay".
        This endpoint is NOT typically called by AI agents directly — the user
        interacts with the checkout page UI instead.
        Supports multiple payment methods: card, alipay, wechat, amazon_pay.
      tags: [Checkout]
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                locale:
                  type: string
                  enum: [en, zh-CN, zh-TW, ja, ko, fr]
                  default: en
                paymentMethod:
                  type: string
                  enum: [card, alipay, wechat, amazon_pay]
                  default: card
                  description: |
                    Payment method. card uses Stripe Checkout (includes Apple Pay/Google Pay).
                    alipay and amazon_pay redirect to the provider.
                    wechat returns a QR code for inline display.
      responses:
        "200":
          description: Payment session created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      paymentType:
                        type: string
                        enum: [redirect, qr_code]
                      checkoutUrl:
                        type: string
                        format: uri
                        description: Redirect URL (card, alipay, amazon_pay).
                      sessionId:
                        type: string
                        description: Stripe session ID (card only).
                      qrCodeUrl:
                        type: string
                        description: QR code image URL (wechat only).
                      qrCodeData:
                        type: string
                        description: Raw weixin:// payment URL (wechat only).
                      qrCodeExpiresAt:
                        type: integer
                        description: Unix timestamp when QR expires (wechat only).
                      orderId:
                        type: string
                        description: Order ID (wechat only, for polling).
        "400":
          description: Invalid request or payment creation failed.
        "404":
          description: Order not found.
        "410":
          description: Order expired.
        "429":
          description: Rate limit exceeded.

components:
  schemas:
    MachineReceipt:
      type: object
      description: |
        Returned by both machine-payment operations once a credential verifies.
        Built by `lib/payments/machine/receipt.ts`; the shape is identical on
        both rails, only `protocol` and `settlement` differ.
      properties:
        orderId:
          type: string
        orderNumber:
          type: string
        amount:
          type: string
          description: "Amount settled, decimal USD as a string (e.g. '12.34')."
        currency:
          type: string
        protocol:
          type: string
          enum: [x402, mpp]
        settlement:
          type: string
          enum: [onchain_async, settled]
          description: >-
            `settled` means the charge completed synchronously (MPP SPT).
            `onchain_async` means funds are still confirming on-chain — the eSIM
            is provisioned once Stripe captures the deposit, so keep polling.
        status:
          type: string
          enum: [payment_submitted]
          description: >-
            Always this value. Payment being ACCEPTED is not the eSIM being
            delivered; `statusUrl` is what reports that.
        statusUrl:
          type: string
          format: uri
          description: Poll until the checkout status is `completed`.
        note:
          type: string
          description: Human-readable next step, safe to relay to the user.
      required:
        [orderId, orderNumber, amount, currency, protocol, settlement, status, statusUrl, note]

    MachineProblem:
      type: object
      description: |
        RFC 9457 problem+json, returned by our OWN refusals on the
        machine-payment operations — 400/404/409/410/429/502. The 402 is NOT
        one of these: that response is produced by the payment protocol itself
        and has a different shape on each rail, described on each operation. `type` is the machine-readable discriminator — read its
        trailing slug rather than matching on `title` or `detail`, which are
        prose and may be reworded.
      properties:
        type:
          type: string
          format: uri
          description: >-
            `https://cosim.io/problems/machine-payment/{slug}`. Slugs:
            `invalid-token`, `not-found`, `already-processed`, `expired`,
            `free-order`, `amount-under-minimum`, `amount-over-limit`,
            `coupon-method-restricted`, `rail-disabled`, `rate-limited`,
            `already-paid`, `processing-failed`.
            `amount-over-limit` and `coupon-method-restricted` remain payable at
            the human checkoutUrl; `amount-under-minimum` and `free-order` do
            NOT, because every payment method enforces the same platform minimum
            and the checkout page has no $0 branch.
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
      required: [type, title, status, detail]

    PaymentChallengeProblem:
      type: object
      description: |
        Body of the MPP 402. Emitted by the protocol layer, not by us, so its
        `type` is a paymentauth.org URI rather than one of our
        machine-payment slugs. The payable content is NOT in this body — it is
        in the `WWW-Authenticate` header.
      properties:
        type:
          type: string
          format: uri
          example: https://paymentauth.org/problems/payment-required
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        hint:
          type: string
        challengeId:
          type: string
          description: Identifies this challenge; also present in the WWW-Authenticate header.

    Meta:
      type: object
      properties:
        requestId:
          type: string
        timestamp:
          type: string
          format: date-time

    PackageSummary:
      type: object
      properties:
        id:
          type: string
          description: Package ID — use this in calculatePrice and createCheckout.
        name:
          type: string
        slug:
          type: string
        dataVolume:
          type: object
          properties:
            mb:
              type: integer
            display:
              type: string
              description: "Human-readable (e.g. '1GB', '500MB/day')."
            type:
              type: string
              enum: [total, daily_throttle, daily_cutoff, daily_unlimited]
              description: "total = fixed pool; daily_* = resets each day."
        duration:
          type: object
          properties:
            days:
              type: integer
            display:
              type: string
        price:
          type: object
          properties:
            amount:
              type: number
            currency:
              type: string
        networkType:
          type: string
        speed:
          type: string
          description: "Network speed tier (e.g. '4G', '3G/4G/5G')."
        supportTopup:
          type: boolean
        billingStart:
          type: string
          enum: [first_connection, first_install, purchase]
          description: >-
            When the validity timer starts. 'purchase' bills immediately, is
            non-refundable, and has NO activation deadline — its validity runs from
            payment, so `preInstallDays` below does not apply and no install-by date
            should be quoted for it.
        isFavorite:
          type: boolean
          description: Recommended/popular plan.
        preInstallDays:
          type: integer
          nullable: true
          description: >
            Number of days from purchase within which the eSIM must be installed
            (QR code scanned) AND activated (connected to a network). If this deadline
            passes while the eSIM is still unactivated it expires permanently — having
            installed it does not prevent that. Typical values range from 30 to 180 days
            depending on the plan. Always surface this to users at checkout.
            NOT applicable when billingStart is 'purchase': those plans have no
            activation deadline at all, and this field is null for them. A null here
            always means "no deadline to quote" — never treat it as zero days.
        voice:
          type: object
          nullable: true
          description: >-
            Calling / messaging entitlements included on top of data. null for an
            ordinary data-only plan, which is almost all of them. A plan that carries
            this typically costs several times a data-only plan of the same size, so
            compare it before presenting them as alternatives. Do not state that a
            plan has no voice or SMS without checking this field, and do not confuse
            it with `smsStatus` elsewhere in the API, which is a supplier-side
            capability and says nothing about what the customer gets.
          properties:
            localCalls:
              type: string
              nullable: true
              description: "'unlimited' when calls within the destination are included."
            intlMinutes:
              type: integer
              nullable: true
            intlDestinations:
              type: array
              description: ISO-3166 alpha-2 codes the international minutes cover.
              items:
                type: string
            sms:
              type: string
              nullable: true
              description: "'unlimited' when SMS/MMS are included."
