openapi: 3.1.0
info:
  title: kiwiSMS API
  version: 1.0.0
  description: |-
    REST API for SMS OTP number rental.

    ## Quick start
    1. Create an API key in the dashboard (`/dashboard/api-keys`).
    2. `GET /api/v1/services` — list services with `kind`, `serviceNo`, and display `code` (e.g. `s1-5`).
    3. `POST /api/v1/rentals` with `{ "kind": "s1", "serviceNo": 5 }`.
    4. Poll `GET /api/v1/rentals/{id}` or configure webhooks for OTP delivery.

    ## Service codes
    Each catalog row has `kind` (s1–s4, tm1), `serviceNo` (integer within kind), and `code` (`kind-serviceNo`, e.g. `s1-5`).
    Long-term plans use `kind` (l1–l3), `planNo`, and `code` (e.g. `l2-3`).
    Use `kind` + `serviceNo` / `planNo` when renting — not upstream provider codes.

    ## Docs & spec
    Human-readable: `/docs`. Machine-readable: `/openapi.json` or `/openapi.yaml`.
servers:
  - url: https://kiwisms.com
    description: Current deployment
tags:
  - name: Account
    description: Wallet balance
  - name: Catalog
    description: Services and pricing
  - name: Rentals
    description: Rent, poll, cancel, complete, renew
  - name: Long-term
    description: Long-term number plans and rentals
  - name: Webhooks
    description: Outbound OTP notifications to your server
security:
  - ApiKeyHeader: []
  - BearerAuth: []
components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key format: `lsms_{prefix}_{secret}`"
    BearerAuth:
      type: http
      scheme: bearer
      description: Same API key as Bearer token
  parameters:
    CountryQuery:
      name: country
      in: query
      description: Numeric country code
      schema:
        type: string
        enum: &a1
          - "187"
          - "16"
          - "36"
          - "43"
          - "78"
          - "48"
        default: "187"
    CountryCodeQuery:
      name: country_code
      in: query
      description: Alias for `country`
      schema:
        type: string
        enum: *a1
    SyncQuery:
      name: sync
      in: query
      description: Set to `0` to skip SMS sync (faster polling)
      schema:
        type: string
        enum:
          - "0"
          - "1"
        default: "1"
    RentalId:
      name: id
      in: path
      required: true
      description: KiwiSMS rental id (sequential integer)
      schema:
        type: integer
        minimum: 1
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Optional UUID for safe retries on POST (max 256 chars, 24h TTL)
      schema:
        type: string
        maxLength: 256
    PageQuery:
      name: page
      in: query
      description: Page number (1-based)
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSizeQuery:
      name: pageSize
      in: query
      description: Items per page (max 100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 100
    SearchQuery:
      name: q
      in: query
      description: Search by service name, code, kind, or service number
      schema:
        type: string
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
        code:
          type: string
    Balance:
      type: object
      required:
        - balance
        - currency
      properties:
        balance:
          type: number
          description: USD wallet balance
        currency:
          type: string
          const: USD
    ServiceItem:
      type: object
      required:
        - kind
        - serviceNo
        - code
        - name
        - price
        - count
        - multiSms
        - durationMinutes
      properties:
        kind:
          type: string
          enum:
            - s1
            - s2
            - s3
            - s4
            - tm1
        serviceNo:
          type: integer
          minimum: 1
          description: Service number within the kind (s1-1 → kind s1, serviceNo 1)
        code:
          type: string
          description: "Display reference: `{kind}-{serviceNo}` (e.g. s1-5)"
          example: s1-5
        name:
          type: string
        price:
          type: string
          description: USD price as string
        count:
          type: integer
          description: Available stock
        multiSms:
          type: boolean
        durationMinutes:
          type: integer
          description: Rental window in minutes for this service
    ServicesResponse:
      type: object
      required:
        - countryCode
        - catalogMode
        - count
        - services
      properties:
        countryCode:
          type: string
        catalogMode:
          type: boolean
        count:
          type: integer
        services:
          type: array
          items:
            $ref: "#/components/schemas/ServiceItem"
    Rental:
      type: object
      required:
        - id
        - kind
        - serviceNo
        - phone
        - serviceName
        - status
        - price
        - codes
        - otpCode
        - expiresAt
        - createdAt
        - active
        - secondsRemaining
      properties:
        id:
          type: integer
          description: KiwiSMS rental id (sequential integer)
        kind:
          type: string
          enum:
            - s1
            - s2
            - s3
            - s4
            - tm1
        serviceNo:
          type:
            - integer
            - "null"
          description: Catalog service number within kind
        phone:
          type: string
        serviceName:
          type: string
        status:
          type: string
          enum:
            - WAITING
            - RECEIVED
            - COMPLETED
            - CANCELLED
            - EXPIRED
        price:
          type: number
        codes:
          type: array
          items:
            type: string
        otpCode:
          type:
            - string
            - "null"
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        active:
          type: boolean
        secondsRemaining:
          type: integer
    RentalCreated:
      type: object
      required:
        - id
        - kind
        - phone
        - serviceName
        - price
        - status
        - expiresAt
      properties:
        id:
          type: integer
          description: KiwiSMS rental id (sequential integer)
        kind:
          type: string
          enum:
            - s1
            - s2
            - s3
            - s4
            - tm1
        phone:
          type: string
        serviceName:
          type: string
        price:
          type: number
        status:
          type: string
          const: WAITING
        expiresAt:
          type: string
          format: date-time
        asleep:
          type: boolean
    RentRequest:
      type: object
      required:
        - kind
        - serviceNo
      properties:
        kind:
          type: string
          enum:
            - s1
            - s2
            - s3
            - s4
            - tm1
          description: Service kind (S.1–S.4, T-Mobile line)
        serviceNo:
          type: integer
          minimum: 1
          description: Service number within the kind (from GET /api/v1/services)
        max_price:
          oneOf:
            - type: string
            - type: number
          description: Maximum price willing to pay (USD)
        areas:
          type: string
          description: Comma-separated area codes
        carrier:
          type: string
          enum:
            - tmo
            - vz
            - att
        country:
          type: string
          enum: *a1
        country_code:
          type: string
          enum: *a1
        phone:
          type: string
          minLength: 10
          description: K.2 only — reuse an existing phone (digits or E.164)
        reuse_phone:
          type: string
          minLength: 10
          description: Alias for phone (K.2 reuse)
    RentalListResponse:
      type: object
      required:
        - rentals
        - count
      properties:
        rentals:
          type: array
          items:
            $ref: "#/components/schemas/Rental"
        count:
          type: integer
    RentalResponse:
      type: object
      required:
        - rental
      properties:
        rental:
          $ref: "#/components/schemas/Rental"
    RentResponse:
      type: object
      required:
        - rental
      properties:
        rental:
          $ref: "#/components/schemas/RentalCreated"
    RentalActionResponse:
      type: object
      required:
        - success
        - rentalId
        - status
      properties:
        success:
          type: boolean
          const: true
        rentalId:
          type: string
          description: KiwiSMS rental id (numeric string)
        status:
          type: string
          enum:
            - CANCELLED
            - COMPLETED
    PublicPricingItem:
      type: object
      required:
        - kind
        - serviceNo
        - code
        - name
        - price
      properties:
        kind:
          type: string
          enum:
            - s1
            - s2
            - s3
            - s4
            - tm1
        serviceNo:
          type: integer
          minimum: 1
        code:
          type: string
          example: s1-5
        name:
          type: string
        price:
          type: string
    PublicPricingResponse:
      type: object
      required:
        - verification
      properties:
        verification:
          type: array
          items:
            $ref: "#/components/schemas/PublicPricingItem"
    PublicServicesResponse:
      type: object
      required:
        - countryCode
        - services
        - total
        - page
        - pageSize
        - totalPages
      properties:
        countryCode:
          type: string
        services:
          type: array
          items:
            $ref: "#/components/schemas/ServiceItem"
        total:
          type: integer
        page:
          type: integer
          minimum: 1
        pageSize:
          type: integer
          maximum: 100
        totalPages:
          type: integer
          minimum: 1
        q:
          type: string
          description: Echo of search query when filtered
    LongTermPlanItem:
      type: object
      required:
        - kind
        - planNo
        - code
        - serviceName
        - countryName
        - durationLabel
        - durationKey
        - price
        - renewPrice
      properties:
        kind:
          type: string
          enum:
            - l1
            - l2
            - l3
            - ltm
        planNo:
          type: integer
          minimum: 1
        code:
          type: string
          example: l2-3
        serviceName:
          type: string
        countryName:
          type:
            - string
            - "null"
        durationLabel:
          type: string
        durationKey:
          type: string
        price:
          type: string
        renewPrice:
          type: string
    LongTermPlansResponse:
      type: object
      required:
        - available
        - plans
        - total
        - page
        - pageSize
        - totalPages
      properties:
        available:
          type: boolean
        count:
          type: integer
        plans:
          type: array
          items:
            $ref: "#/components/schemas/LongTermPlanItem"
        total:
          type: integer
        page:
          type: integer
          minimum: 1
        pageSize:
          type: integer
          maximum: 100
        totalPages:
          type: integer
          minimum: 1
        q:
          type: string
    LongTermRentRequest:
      type: object
      required:
        - kind
        - planNo
      properties:
        kind:
          type: string
          enum:
            - l1
            - l2
            - l3
            - ltm
        planNo:
          type: integer
          minimum: 1
          description: Plan number within kind (from GET /api/v1/long-term/plans)
        area_code:
          type: string
          description: Optional US area code (L.1)
        quantity:
          type: integer
          minimum: 1
          description: Bulk rent count (L.1 only; requires Idempotency-Key when > 1)
    LongTermRental:
      type: object
      required:
        - id
        - kind
        - serviceName
        - phone
        - sellPrice
        - status
      properties:
        id:
          type: string
          description: Long-term rental id
        kind:
          type: string
          enum:
            - l1
            - l2
            - l3
            - ltm
        planNo:
          type:
            - integer
            - "null"
        code:
          type:
            - string
            - "null"
          example: l2-3
        serviceName:
          type: string
        phone:
          type: string
        sellPrice:
          type: number
        status:
          type: string
        activeFrom:
          type:
            - string
            - "null"
          format: date-time
        activeTill:
          type:
            - string
            - "null"
          format: date-time
        autoRenew:
          type: boolean
        active:
          type: boolean
        lineStatus:
          type: string
          enum:
            - active
            - sleep
            - expired
    LongTermRentalResponse:
      type: object
      required:
        - rental
      properties:
        rental:
          $ref: "#/components/schemas/LongTermRental"
    LongTermRentalListResponse:
      type: object
      required:
        - rentals
        - count
      properties:
        rentals:
          type: array
          items:
            $ref: "#/components/schemas/LongTermRental"
        count:
          type: integer
    OtpWebhookPayload:
      type: object
      required:
        - event
        - rentalId
        - kind
        - serviceNo
        - code
        - serviceName
        - phone
        - otpCode
        - smsText
        - receivedAt
      properties:
        event:
          type: string
          const: sms.received
        rentalId:
          type: string
          description: KiwiSMS rental id (numeric string)
        kind:
          type: string
          enum:
            - s1
            - s2
            - s3
            - s4
            - tm1
        serviceNo:
          type:
            - integer
            - "null"
        code:
          type:
            - string
            - "null"
          description: Display reference `{kind}-{serviceNo}` when known
          example: s1-5
        serviceName:
          type: string
        phone:
          type: string
        otpCode:
          type: string
        smsText:
          type: string
        receivedAt:
          type: string
          format: date-time
      description: Outbound webhook POST (configure URL on account or API key). Signed with `X-Webhook-Signature` (HMAC-SHA256 hex of raw JSON body).
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Account suspended
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
paths:
  /api/v1/balance:
    get:
      operationId: getBalance
      tags:
        - Account
      summary: Wallet balance
      responses:
        "200":
          description: USD balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Balance"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/services:
    get:
      operationId: listServices
      tags:
        - Catalog
      summary: Services with stock and prices
      parameters:
        - $ref: "#/components/parameters/CountryQuery"
        - $ref: "#/components/parameters/CountryCodeQuery"
      responses:
        "200":
          description: Service catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServicesResponse"
        "400":
          description: Unsupported country
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Unsupported country code
                code: INVALID_COUNTRY
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Service error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Failed to fetch services
                code: PROVIDER_ERROR
  /api/v1/rentals:
    get:
      operationId: listRentals
      tags:
        - Rentals
      summary: List active rentals
      parameters:
        - $ref: "#/components/parameters/SyncQuery"
      responses:
        "200":
          description: Active rentals (WAITING / RECEIVED)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentalListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: createRental
      tags:
        - Rentals
      summary: Rent a number
      description: Charges wallet for a ~20 minute session. Send `Idempotency-Key` for safe retries.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RentRequest"
      responses:
        "201":
          description: Rental created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentResponse"
        "400":
          description: Invalid JSON or validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Insufficient balance
                code: INSUFFICIENT_BALANCE
        "404":
          description: Service unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Service unavailable
                code: SERVICE_UNAVAILABLE
        "409":
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Idempotency key reused with different body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Idempotency-Key was already used with a different request body
                code: IDEMPOTENCY_KEY_REUSED
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/rentals/{id}:
    get:
      operationId: getRental
      tags:
        - Rentals
      summary: Poll rental status and OTP codes
      parameters:
        - $ref: "#/components/parameters/RentalId"
        - $ref: "#/components/parameters/SyncQuery"
      responses:
        "200":
          description: Rental details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentalResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Rental not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Rental not found
                code: RENTAL_NOT_FOUND
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/rentals/{id}/cancel:
    post:
      operationId: cancelRental
      tags:
        - Rentals
      summary: Cancel WAITING rental
      description: Refunds wallet when cancellation succeeds.
      parameters:
        - $ref: "#/components/parameters/RentalId"
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentalActionResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Not found or not cancellable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Rental not found or cannot be cancelled (only WAITING rentals)
                code: CANCEL_NOT_ALLOWED
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Provider cancellation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Could not cancel rental. Try again shortly.
                code: PROVIDER_ERROR
  /api/v1/rentals/{id}/done:
    post:
      operationId: completeRental
      tags:
        - Rentals
      summary: Mark rental completed
      description: Releases the number when you are finished.
      parameters:
        - $ref: "#/components/parameters/RentalId"
      responses:
        "200":
          description: Completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentalActionResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Rental not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Rental not found
                code: RENTAL_NOT_FOUND
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/rentals/{id}/renew:
    post:
      operationId: renewRental
      tags:
        - Rentals
      summary: Renew same phone number
      description: Starts a new ~20 minute session for the same number. Supports `Idempotency-Key`.
      parameters:
        - $ref: "#/components/parameters/RentalId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Renewed (or idempotent replay)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentResponse"
        "201":
          description: New rental session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Insufficient balance
                code: INSUFFICIENT_BALANCE
        "404":
          description: Rental not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Rental not found
                code: NOT_FOUND
        "409":
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Idempotency key reused with different body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/long-term/plans:
    get:
      operationId: listLongTermPlans
      tags:
        - Long-term
      summary: Long-term plans with prices
      parameters:
        - $ref: "#/components/parameters/PageQuery"
        - $ref: "#/components/parameters/PageSizeQuery"
        - $ref: "#/components/parameters/SearchQuery"
      responses:
        "200":
          description: Paginated long-term plan catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongTermPlansResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Catalog fetch failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/long-term/rentals:
    get:
      operationId: listLongTermRentals
      tags:
        - Long-term
      summary: List long-term rentals
      parameters:
        - $ref: "#/components/parameters/SyncQuery"
      responses:
        "200":
          description: Long-term rentals
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongTermRentalListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: createLongTermRental
      tags:
        - Long-term
      summary: Rent a long-term number
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LongTermRentRequest"
      responses:
        "200":
          description: Rental created (or idempotent replay)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongTermRentalResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Insufficient balance
                code: INSUFFICIENT_BALANCE
        "404":
          description: Plan not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Plan not found
                code: PLAN_NOT_FOUND
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/long-term/rentals/{id}:
    get:
      operationId: getLongTermRental
      tags:
        - Long-term
      summary: Poll one long-term rental
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/SyncQuery"
      responses:
        "200":
          description: Long-term rental details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongTermRentalResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Rental not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Rental not found
                code: RENTAL_NOT_FOUND
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/public/long-term-plans:
    get:
      operationId: listPublicLongTermPlans
      tags:
        - Long-term
        - Catalog
      summary: Paginated public long-term catalog (no auth)
      security: []
      parameters:
        - $ref: "#/components/parameters/PageQuery"
        - $ref: "#/components/parameters/PageSizeQuery"
        - $ref: "#/components/parameters/SearchQuery"
      responses:
        "200":
          description: Paginated long-term plans
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongTermPlansResponse"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Catalog fetch failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/public/pricing:
    get:
      operationId: getPublicPricing
      tags:
        - Catalog
      summary: Public pricing snapshot (no auth)
      security: []
      responses:
        "200":
          description: Top services for landing page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicPricingResponse"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/public/services:
    get:
      operationId: listPublicServices
      tags:
        - Catalog
      summary: Paginated public service catalog (no auth)
      description: Browse pricing without authentication. Supports search and pagination (max 100 per page).
      security: []
      parameters:
        - $ref: "#/components/parameters/CountryQuery"
        - $ref: "#/components/parameters/PageQuery"
        - $ref: "#/components/parameters/PageSizeQuery"
        - $ref: "#/components/parameters/SearchQuery"
      responses:
        "200":
          description: Paginated catalog page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicServicesResponse"
        "400":
          description: Unsupported country
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Catalog fetch failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
x-webhooks:
  smsReceived:
    post:
      tags:
        - Webhooks
      summary: OTP received (outbound to your URL)
      description: |-
        Configure webhook URL on your account or API key. When an OTP arrives, kiwiSMS POSTs JSON to your URL.

        Verify `X-Webhook-Signature`: HMAC-SHA256 hex digest of the **raw request body** using your webhook secret.

        Respond with HTTP 2xx to acknowledge. Non-2xx responses are logged but not retried automatically.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OtpWebhookPayload"
            example:
              event: sms.received
              rentalId: "1042"
              kind: s1
              serviceNo: 5
              code: s1-5
              serviceName: Google
              phone: "+12025551234"
              otpCode: "123456"
              smsText: Your Google verification code is 123456
              receivedAt: 2026-05-29T12:00:00.000Z
      responses:
        2XX:
          description: Your server acknowledged the webhook
