openapi: 3.1.0
info:
  title: YonoSIM API
  version: "1.0.0-draft"
  summary: The developer-first eSIM marketplace — self-serve provisioning for travel products.
  description: |
    Public developer API for issuing, managing, and refunding eSIMs across
    200+ countries. Auto-refund on unused data, real-time usage webhooks,
    white-label activation, and multi-currency checkout built in.

    Base URL: `https://api.yonosim.com`

    All requests must include a Bearer token from the developer dashboard.
    Test keys (`sk_test_...`) never issue real eSIMs and never bill.
    Live keys (`sk_live_...`) issue real eSIMs and debit prepaid credit.
  contact:
    name: YonoSIM API team
    email: api@yonosim.com
    url: https://yonosim.com/developers
  license:
    name: YonoSIM API Terms
    url: https://yonosim.com/developers/terms
servers:
  - url: https://api.yonosim.com
    description: Production
  - url: http://localhost:8080
    description: Local dev (mock mode)

security:
  - bearerAuth: []

tags:
  - name: Plans
    description: Discover eSIM plans and country coverage.
  - name: Orders
    description: Create, retrieve, and manage eSIM orders.
  - name: Usage
    description: Real-time data consumption per eSIM.
  - name: Refunds
    description: Trigger refunds for unused or defective eSIMs.
  - name: Top-ups
    description: Add data to an existing active eSIM.
  - name: Branding
    description: Configure white-label activation pages.
  - name: Webhooks
    description: Subscribe to eSIM lifecycle events.
  - name: Account
    description: Balance, keys, and account state.

paths:
  # ─── Discovery ────────────────────────────────────────────────────────────
  /v1/countries:
    get:
      tags: [Plans]
      summary: List supported countries and regions.
      description: |
        Returns every country with at least one purchasable plan, plus
        multi-country regions (Europe, Asia, Global).
      responses:
        "200":
          description: List of countries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Country"
              example:
                data:
                  - id: jp
                    name: Japan
                    iso: JP
                    kind: country
                    planCount: 42
                  - id: eu-35
                    name: Europe (35 countries)
                    iso: null
                    kind: region
                    planCount: 18

  /v1/plans/search:
    post:
      tags: [Plans]
      summary: Search plans by country, data volume, or duration.
      description: |
        The primary discovery endpoint. Filter by country/region, minimum data,
        and validity days. Prices returned are your effective activation cost at
        your current tier — not retail.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                country:
                  type: string
                  description: ISO-3166-1 alpha-2 country code, or region id from /v1/countries.
                  example: JP
                region:
                  type: string
                  description: Region id (e.g. "eu-35", "asia-15", "global-100"). Mutually exclusive with country.
                minDataGb:
                  type: number
                  minimum: 0
                  example: 5
                minDays:
                  type: integer
                  minimum: 1
                  example: 30
                includeUnlimited:
                  type: boolean
                  default: true
      responses:
        "200":
          description: Matching plans.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Plan"

  /v1/plans/{planId}:
    get:
      tags: [Plans]
      summary: Retrieve a single plan.
      parameters:
        - $ref: "#/components/parameters/PlanId"
      responses:
        "200":
          description: Plan detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Plan"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Orders ───────────────────────────────────────────────────────────────
  /v1/orders:
    post:
      tags: [Orders]
      summary: Create an eSIM order.
      description: |
        Provisions an eSIM immediately. On success, returns the activation QR
        (as a URL) and LPA string. Debits your prepaid credit at your tier rate.

        Idempotent when `Idempotency-Key` header is set — safe to retry.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [planId]
              properties:
                planId:
                  type: string
                  example: jp_5gb_30d
                customerRef:
                  type: string
                  description: Your own reference (user id, booking id, etc.). Echoed back on webhooks.
                  maxLength: 200
                customerEmail:
                  type: string
                  format: email
                  description: If present, we send the activation email on your behalf using your branding.
                brandingId:
                  type: string
                  description: Branding profile id from POST /v1/branding.
                metadata:
                  type: object
                  additionalProperties:
                    type: string
                  description: Arbitrary key/value pairs (max 20 keys, 500 chars each).
      responses:
        "201":
          description: Order created and eSIM provisioned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          $ref: "#/components/responses/BadRequest"
        "402":
          $ref: "#/components/responses/InsufficientCredit"
    get:
      tags: [Orders]
      summary: List orders (paginated).
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/OrderStatus"
        - name: createdAfter
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Paginated order list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Order"
                  nextCursor:
                    type: string
                    nullable: true

  /v1/orders/{orderId}:
    get:
      tags: [Orders]
      summary: Retrieve an order.
      parameters:
        - $ref: "#/components/parameters/OrderId"
      responses:
        "200":
          description: Order detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/orders/{orderId}/qr:
    get:
      tags: [Orders]
      summary: Get the activation QR code as an image.
      parameters:
        - $ref: "#/components/parameters/OrderId"
        - name: format
          in: query
          schema:
            type: string
            enum: [png, svg]
            default: png
      responses:
        "200":
          description: QR image bytes.
          content:
            image/png: {}
            image/svg+xml: {}

  /v1/orders/{orderId}/lpa:
    get:
      tags: [Orders]
      summary: Get the raw LPA activation string.
      parameters:
        - $ref: "#/components/parameters/OrderId"
      responses:
        "200":
          description: LPA string for manual entry.
          content:
            application/json:
              schema:
                type: object
                properties:
                  lpa:
                    type: string
                    example: "LPA:1$rsp.truphone.com$XYZ123..."

  /v1/orders/{orderId}/usage:
    get:
      tags: [Usage]
      summary: Real-time data usage.
      description: |
        Returns bytes used / remaining as of the last reconciliation with the
        upstream network (typically within 60 seconds). For push-based
        updates, subscribe to `order.data.low` and `order.data.exhausted`
        webhooks instead of polling.
      parameters:
        - $ref: "#/components/parameters/OrderId"
      responses:
        "200":
          description: Usage snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Usage"

  /v1/orders/{orderId}/topup:
    post:
      tags: [Top-ups]
      summary: Add data to an existing eSIM.
      description: |
        Extends the existing eSIM with an additional plan. The customer's LPA
        stays the same — no re-install required.
      parameters:
        - $ref: "#/components/parameters/OrderId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [planId]
              properties:
                planId:
                  type: string
      responses:
        "201":
          description: Top-up successful.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"

  /v1/orders/{orderId}/refund:
    post:
      tags: [Refunds]
      summary: Refund an order.
      description: |
        Manual refund. Auto-refunds for unused data (< 5% used at validity end)
        happen automatically without calling this endpoint.
      parameters:
        - $ref: "#/components/parameters/OrderId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  enum: [customer_request, defective, coverage_failure, fraud, other]
                partial:
                  type: number
                  description: Partial refund amount in USD. Omit for full refund.
                notes:
                  type: string
                  maxLength: 500
      responses:
        "200":
          description: Refund processed. Amount credited back to your balance.
          content:
            application/json:
              schema:
                type: object
                properties:
                  refundId:
                    type: string
                  amountUsd:
                    type: number
                  balanceUsd:
                    type: number

  /v1/orders/bulk:
    post:
      tags: [Orders]
      summary: Provision up to 500 eSIMs in one call.
      description: |
        Ideal for tour operators pre-provisioning by tour departure. All-or-nothing:
        if any single order fails validation, the whole batch is rejected.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [orders]
              properties:
                orders:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    type: object
                    required: [planId]
                    properties:
                      planId: { type: string }
                      customerRef: { type: string }
                      customerEmail: { type: string, format: email }
                      brandingId: { type: string }
                brandingId:
                  type: string
                  description: Default branding for the batch.
      responses:
        "201":
          description: All orders provisioned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Order"

  # ─── Branding ─────────────────────────────────────────────────────────────
  /v1/branding:
    post:
      tags: [Branding]
      summary: Create or update a branding profile.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                logoUrl:
                  type: string
                  format: uri
                primaryColor:
                  type: string
                  pattern: "^#[0-9A-Fa-f]{6}$"
                  example: "#0EA5E9"
                customDomain:
                  type: string
                  description: e.g. esim.yourbrand.com (requires DNS CNAME to yonosim-cname.com)
                supportEmail:
                  type: string
                  format: email
      responses:
        "201":
          description: Branding profile created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Branding"

  # ─── Webhooks ─────────────────────────────────────────────────────────────
  /v1/webhooks:
    post:
      tags: [Webhooks]
      summary: Subscribe to lifecycle events.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                description:
                  type: string
                  maxLength: 200
      responses:
        "201":
          description: Subscription created. Returns a signing secret used to verify future deliveries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  secret:
                    type: string
                    description: Shown once. Use with HMAC-SHA256 to verify the `X-YonoSIM-Signature` header on delivered payloads.

  # ─── Account ──────────────────────────────────────────────────────────────
  /v1/balance:
    get:
      tags: [Account]
      summary: Prepaid credit balance and current tier.
      responses:
        "200":
          description: Balance snapshot.
          content:
            application/json:
              schema:
                type: object
                properties:
                  balanceUsd:
                    type: number
                  tier:
                    type: string
                    enum: [starter, growth, scale, enterprise]
                  discountOffRetail:
                    type: number
                    description: 0.05, 0.15, 0.25, or custom.
                  rolling30dSpendUsd:
                    type: number
                  autoRechargeUsd:
                    type: number
                    nullable: true

# ══════════════════════════════════════════════════════════════════════════
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_XXX or sk_test_XXX

  parameters:
    OrderId:
      name: orderId
      in: path
      required: true
      schema:
        type: string
        pattern: "^ord_[a-zA-Z0-9]+$"
    PlanId:
      name: planId
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: |
        Unique key (UUID recommended). Same key + same body within 24h returns
        the original response — safe to retry.
      schema:
        type: string
        maxLength: 128

  responses:
    BadRequest:
      description: Request failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InsufficientCredit:
      description: Prepaid credit balance below the plan cost.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Country:
      type: object
      properties:
        id: { type: string, example: jp }
        name: { type: string, example: Japan }
        iso: { type: string, nullable: true, example: JP }
        kind: { type: string, enum: [country, region] }
        planCount: { type: integer }

    Plan:
      type: object
      properties:
        id: { type: string, example: jp_5gb_30d }
        country: { type: string, example: JP }
        region: { type: string, nullable: true }
        dataGb: { type: number, nullable: true, description: "Null for unlimited plans." }
        unlimited: { type: boolean }
        dailyCapMb:
          type: integer
          nullable: true
          description: For unlimited plans, the fair-use daily cap before throttling.
        days: { type: integer, example: 30 }
        retailUsd: { type: number, example: 12.00 }
        costUsd:
          type: number
          example: 10.20
          description: Your effective activation cost at your current tier.
        networks:
          type: array
          items: { type: string }
          example: ["NTT DoCoMo", "SoftBank"]
        speedMax: { type: string, example: "5G" }

    Order:
      type: object
      properties:
        id:
          type: string
          example: ord_2X8k9aB7
        planId: { type: string }
        customerRef: { type: string, nullable: true }
        customerEmail: { type: string, format: email, nullable: true }
        brandingId: { type: string, nullable: true }
        status:
          $ref: "#/components/schemas/OrderStatus"
        iccid: { type: string, example: "89014103211118510720" }
        lpa:
          type: string
          example: "LPA:1$rsp.truphone.com$XY..."
        qrPng:
          type: string
          format: uri
          example: "https://esim.yonosim.com/q/2X8k9aB7.png"
        activationPageUrl:
          type: string
          format: uri
          example: "https://esim.yourbrand.com/a/2X8k9aB7"
        costUsd: { type: number }
        createdAt: { type: string, format: date-time }
        activatedAt: { type: string, format: date-time, nullable: true }
        validityEndsAt: { type: string, format: date-time, nullable: true }
        metadata:
          type: object
          additionalProperties: { type: string }

    OrderStatus:
      type: string
      enum:
        - pending      # created, upstream provisioning
        - issued       # eSIM ready, not yet installed by customer
        - active       # installed and consuming data
        - exhausted    # all data used
        - expired      # validity period ended
        - refunded     # activation cost credited back
        - cancelled    # cancelled before install

    Usage:
      type: object
      properties:
        orderId: { type: string }
        bytesUsed: { type: integer }
        bytesLimit: { type: integer, nullable: true, description: "Null for unlimited plans." }
        percentUsed: { type: number, nullable: true, minimum: 0, maximum: 100 }
        daysRemaining: { type: number }
        lastSyncedAt: { type: string, format: date-time }

    Branding:
      type: object
      properties:
        id: { type: string, example: brand_marriott }
        name: { type: string }
        logoUrl: { type: string, format: uri, nullable: true }
        primaryColor: { type: string, nullable: true }
        customDomain: { type: string, nullable: true }
        supportEmail: { type: string, format: email, nullable: true }
        activationPageUrl:
          type: string
          format: uri
          description: The white-label activation URL customers use.

    WebhookEvent:
      type: string
      enum:
        - order.created
        - order.activated
        - order.data.low         # <100 MB or <10% remaining (configurable)
        - order.data.exhausted
        - order.expired
        - order.refunded
        - order.cancelled

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: invalid_plan
            message:
              type: string
              example: "Plan 'jp_5gb_30d' is not available in your account."
            param:
              type: string
              nullable: true
            requestId:
              type: string
              example: req_2X8k9aB7c3D
