openapi: 3.1.0
info:
  title: NumberOTP API
  version: 1.0.0
  description: >
    Official API for NumberOTP. Integrate direct, high-quality temporary phone numbers into your applications for seamless SMS OTP verification.

    ## Base URL
    ```
    https://api.numberotp.com/v1
    ```

    ---

    ## Authentication
    All authenticated endpoints require a `Bearer` token in the `Authorization` header. You can use either:

    - **API Key** (recommended for server-side use): Generate one in your Developer Dashboard. Keys are prefixed with `notp_`.
    - **Session token**: Obtained via the NextAuth sign-in flow.

    ```
    Authorization: Bearer notp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    ---

    ## Service & Country Identifiers
    NumberOTP uses short, provider-level identifiers — **not** human-readable names.

    - **Service**: a short alphabetic code (e.g. `wa` for WhatsApp, `tg` for Telegram, `go` for Google). Retrieve the full list from `GET /public/services`.
    - **Country**: a numeric string ID (e.g. `"0"` for Russia, `"67"` for United States). Retrieve the full list from `GET /public/countries`.

    Always pass these identifiers — never pass display names like `"whatsapp"` or `"US"`.

    ---

    ## Webhooks (Zero-Latency Delivery)
    Configure a global **Webhook URL** in your Developer Dashboard. When an OTP SMS is received on a rented number, we instantly POST the payload to your endpoint.

    ### Verifying the Payload Signature
    We sign every webhook body with HMAC SHA-256 using your **Webhook Secret** (available in your dashboard) and include it in the request header:

    ```
    x-numberotp-signature: sha256=<hmac_hex>
    ```

    Compute the expected signature server-side and compare it to reject tampered requests.

    **Webhook Payload Example:**
    ```json
    {
      "id": "act_8f7d98x",
      "phone_number": "14155552671",
      "service": "wa",
      "country": "67",
      "otp": "482910",
      "full_sms": "Your WhatsApp code is 482910. Do not share it with anyone.",
      "status": "received"
    }
    ```

    ---

    ## Rate Limits
    Public endpoints: **100 requests/minute** per IP.
    Authenticated endpoints: subject to your account's API limit (configurable in the dashboard; `0` = unlimited).

servers:
  - url: https://api.numberotp.com/v1
    description: Production

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        API Key (prefixed `notp_`) or NextAuth session token. Example: `Authorization: Bearer notp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          example: "Service not available in this country"

    ActivationData:
      type: object
      properties:
        id:
          type: string
          description: Activation ID — use this to poll or wait for the OTP.
          example: "act_8f7d98x"
        phone_number:
          type: string
          description: The provisioned temporary phone number (digits only, no `+`).
          example: "14155552671"
        service:
          type: string
          description: Short service code.
          example: "wa"
        country:
          type: string
          description: Numeric country ID.
          example: "67"
        otp:
          type: string
          nullable: true
          description: Extracted OTP code. `null` if not yet received.
          example: "482910"
        full_sms:
          type: string
          nullable: true
          description: Full text of the received SMS. `null` if not yet received.
          example: "Your WhatsApp code is 482910."
        status:
          type: string
          enum: [waiting, received, canceled, expired]
          example: "waiting"
        expires_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the 20-minute activation window closes.
          example: "2026-05-02T15:30:00.000Z"

security:
  - bearerAuth: []

paths:
  /public/services:
    get:
      summary: List Services
      description: >
        Returns all services available for activation. Use the `code` field from each item as the `service` parameter when creating activations.

        Optionally filter by country numeric ID to see only services available in that country.
      security: []
      tags: [Public]
      parameters:
        - in: query
          name: country
          schema:
            type: string
          description: Numeric country ID to filter by (e.g. `"67"` for United States). Omit for all countries.
          example: "67"
      responses:
        '200':
          description: List of available services
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      services:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                              description: Short service identifier — pass this as `service` in activation requests.
                              example: "wa"
                            name:
                              type: string
                              description: Human-readable name.
                              example: "WhatsApp"
                            count:
                              type: integer
                              description: Number of available numbers for this service.
                              example: 1842
                  source:
                    type: string
                    enum: [cache, db-cache, api]
                    example: "cache"
        '429':
          description: Rate limit exceeded (100 req/min per IP)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /public/countries:
    get:
      summary: List Countries
      description: >
        Returns all countries available for number provisioning. Use the `id` field from each item as the `country` parameter when creating activations.

        Optionally filter by service code to see only countries that have numbers available for that specific service.
      security: []
      tags: [Public]
      parameters:
        - in: query
          name: service
          schema:
            type: string
          description: Short service code to filter by (e.g. `"wa"`). Omit or pass `"any"` for all countries.
          example: "wa"
      responses:
        '200':
          description: List of available countries
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    description: >
                      Object map keyed by the numeric country ID string. Pass the key as `country` in activation requests.
                    additionalProperties:
                      type: object
                      properties:
                        name:
                          type: string
                          description: Country display name.
                          example: "United States"
                        code:
                          type: string
                          description: Numeric ID (same as the key).
                          example: "67"
                    example:
                      "67":
                        name: "United States"
                        code: "67"
                      "0":
                        name: "Russia"
                        code: "0"
                  source:
                    type: string
                    enum: [cache, db-cache, api]
                    example: "cache"
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /public/prices:
    get:
      summary: Get Prices
      description: >
        Returns real-time pricing for all service/country combinations. Results are cached for up to 5 minutes.

        Use the `service` short code and numeric `country` ID to look up the price for a specific combination.
      security: []
      tags: [Public]
      parameters:
        - in: query
          name: service
          schema:
            type: string
          description: Short service code to filter (e.g. `"wa"`).
          example: "wa"
        - in: query
          name: country
          schema:
            type: string
          description: Numeric country ID to filter (e.g. `"67"`).
          example: "67"
      responses:
        '200':
          description: Pricing data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    description: >
                      Nested map: `{ [countryId]: { [serviceCode]: { cost: number, count: number } } }`.
                    example:
                      "67":
                        "wa":
                          cost: 0.05
                          count: 1842
                  source:
                    type: string
                    enum: [cache, api]
                    example: "cache"
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /activations:
    post:
      summary: Purchase a Number
      description: >
        Deducts balance from your account and provisions a temporary phone number for the requested service.

        Once an OTP SMS arrives (within 20 minutes), it is delivered via:
        1. **Webhook** — instantly POSTed to your configured global webhook URL, or the per-request `webhook_url` override.
        2. **Polling** — call `GET /activations/{id}` at your preferred interval.
        3. **Long-poll** — call `GET /activations/{id}/wait` to block until the OTP arrives or the window expires.

        If no OTP is received within 20 minutes, the charged amount is automatically refunded to your wallet.
      tags: [Activations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - service
                - country
              properties:
                service:
                  type: string
                  description: >
                    Short service code from `GET /public/services`. **Do not** pass the display name.
                  example: "wa"
                country:
                  type: string
                  description: >
                    Numeric country ID from `GET /public/countries`. **Do not** pass the country name or ISO code.
                  example: "67"
                webhook_url:
                  type: string
                  format: uri
                  description: >
                    Optional. Override your global webhook URL for this specific activation only.
                  example: "https://your-server.com/api/otp-callback"
                pool:
                  type: string
                  enum: [auto, pool1, pool2, pool3]
                  description: >
                    Optional (default "auto"). "auto" buys from the pool with the best measured delivery
                    rate for this service and country (weighed against price and stock, and your own recent
                    results unless you turned personalization off in Dashboard → Settings), switching to the
                    next pool automatically if one cannot supply a number. A specific pool is only ever that pool.
                  example: "auto"
                max_price:
                  type: number
                  description: >
                    Optional. Highest price (USD) you accept for this number. Pools above it are skipped;
                    if none remains the request fails instead of charging more.
                  example: 0.5
            example:
              service: "wa"
              country: "67"
              webhook_url: "https://your-server.com/api/otp-callback"
      responses:
        '200':
          description: Number provisioned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        description: Activation ID — use this for polling or waiting.
                        example: "act_8f7d98x"
                      phoneNumber:
                        type: string
                        description: Provisioned phone number (digits only, no `+`).
                        example: "14155552671"
                      expiresAt:
                        type: string
                        format: date-time
                        description: When the 20-minute window expires.
                        example: "2026-05-02T15:30:00.000Z"
                  balance:
                    type: number
                    description: Your remaining wallet balance after this purchase.
                    example: 4.75
        '401':
          description: Unauthorized — missing or invalid API key / session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Service not available in this country
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: API limit reached for this billing period
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /activations/{id}:
    get:
      summary: Poll Activation Status
      description: >
        Returns the current status of an activation. Safe to call repeatedly at any interval.

        If the OTP has already been received, it is returned immediately from the database without an upstream call.
        If the activation is still `waiting`, a live upstream check is made and the result is cached in the database.
      tags: [Activations]
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Activation ID returned by `POST /activations`.
          example: "act_8f7d98x"
      responses:
        '200':
          description: Activation status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/ActivationData'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden — activation belongs to a different account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Activation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /activations/{id}/wait:
    get:
      summary: Wait for OTP (Long-Poll)
      description: >
        Blocks the HTTP connection until an OTP is received or the activation expires (up to 20 minutes).

        Useful for simple integrations that want a single blocking call rather than a polling loop.
        For production workloads with multiple concurrent activations, the **Webhook** approach is strongly preferred.
      tags: [Activations]
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Activation ID returned by `POST /activations`.
          example: "act_8f7d98x"
      responses:
        '200':
          description: OTP received
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      otp:
                        type: string
                        example: "482910"
                      full_sms:
                        type: string
                        example: "Your WhatsApp code is 482910."
                      status:
                        type: string
                        example: "received"
        '400':
          description: Activation already canceled or expired
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: string
                  status:
                    type: string
                    enum: [canceled, expired]
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '408':
          description: Timeout — no OTP received within the polling window. Retry or wait for the webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: string
                    example: "Timeout waiting for OTP. Please poll again."
                  status:
                    type: string
                    example: "waiting"

  /rentals:
    post:
      summary: Rent a Number
      description: >
        Purchase a number for a fixed duration (2h, 4h, 12h, 24h, 48h, 72h, 96h, 120h, 144h, 168h, 336h, 504h, 720h).
        The number receives unlimited SMS during the rental window. Extend at any time via PATCH /rentals/{id}.


        **Reseller sub-users:** fully supported — authenticate with the sub-user's Bearer token.
        The reseller's markup is applied automatically; the reseller's buying balance is debited at platform cost.
        The reseller earns the markup profit immediately.


        **⚠ No refunds** for rentals or cancellations once a number is issued.
      tags: [Activations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [service, country, duration]
              properties:
                service: { type: string, example: "wa", description: "Service code (e.g. wa, tg, go)" }
                country: { type: string, example: "12", description: "Country ID from /public/countries" }
                duration: { type: integer, example: 168, description: "Duration in hours: 2, 4, 12, or multiples of 24 up to 720 (30 days)" }
            example:
              service: "wa"
              country: "12"
              duration: 168
      responses:
        '200':
          description: Number rented successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      id: { type: string, example: "8675309" }
                      phoneNumber: { type: string, example: "12025551234" }
                      expiresAt: { type: string, format: date-time }
                      durationHours: { type: integer, example: 168 }
                  balance: { type: number, example: 14.38 }
        '400':
          description: Invalid duration or missing fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "Invalid duration. Must be 2, 4, 12, or a multiple of 24 (up to 720 hours)."
        '402':
          description: Insufficient balance
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          description: Service not available for rental in this country
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: Reseller has insufficient buying credits (sub-user purchases only)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /rentals/{id}:
    get:
      summary: Get Rental Status & SMS History
      tags: [Activations]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: Activation ID returned by POST /rentals
      responses:
        '200':
          description: Rental details with SMS history
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      phoneNumber: { type: string }
                      status: { type: string, enum: [active, expired, finished, canceled] }
                      expiresAt: { type: string, format: date-time }
                      durationHours: { type: integer }
                      smsHistory:
                        type: array
                        items:
                          type: object
                          properties:
                            text: { type: string }
                            sender: { type: string }
                            receivedAt: { type: string, format: date-time }
    patch:
      summary: Extend a Rental
      description: Add hours to an active rental. Same duration constraints as POST /rentals. **⚠ No refunds** once extended.
      tags: [Activations]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hours]
              properties:
                hours: { type: integer, example: 24, description: "Additional hours: 2, 4, 12, or multiples of 24" }
            example:
              hours: 24
      responses:
        '200':
          description: Rental extended
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  newExpiresAt: { type: string, format: date-time }
                  cost: { type: number }
                  balance: { type: number }
        '402':
          description: Insufficient balance
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /reseller/profile:
    get:
      summary: Get Reseller Profile
      description: >
        Returns account info including buying balance, earned balance, reseller public ID,
        brand settings (name, logo, color), webhook URL, sub-user defaults, and markup version.
      tags: [Reseller]
      responses:
        '200':
          description: Reseller profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id: { type: string, example: "64f3a..." }
                      email: { type: string, example: "you@company.com" }
                      name: { type: string, example: "Jane Smith" }
                      buyingBalance: { type: number, example: 42.50 }
                      earnedBalance: { type: number, example: 18.75 }
                      resellerPublicId: { type: string, example: "a26e2be6" }
                      brand:
                        type: object
                        properties:
                          name: { type: string, example: "Acme OTP" }
                          logoUrl: { type: string, example: "https://acme.com/logo.png" }
                          color: { type: string, example: "#6366f1" }
                      webhookUrl: { type: string, nullable: true, example: "https://acme.com/webhook" }
                      minSubUserTopup: { type: number, example: 10 }
                      allowSelfFundDefault: { type: boolean, example: true }
                      markupVersion: { type: integer, example: 3 }
                      publicSignupUrl: { type: string, example: "https://www.numberotp.com/join/a26e2be6" }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Not a reseller account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    patch:
      summary: Update Brand, Webhook & Sub-User Settings
      description: Update brand fields, webhook URL/secret, and sub-user defaults. All fields optional — only provided keys are updated.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                brandName: { type: string, example: "Acme OTP" }
                logoUrl: { type: string, example: "https://acme.com/logo.png" }
                color: { type: string, example: "#6366f1" }
                webhookUrl: { type: string, example: "https://acme.com/webhook" }
                webhookSecret: { type: string, example: "my-hmac-secret" }
                allowSelfFundDefault: { type: boolean, example: true }
                minSubUserTopup: { type: number, example: 10 }
            example:
              brandName: "Acme OTP"
              color: "#6366f1"
              webhookUrl: "https://acme.com/webhook"
              minSubUserTopup: 10
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller/markup:
    get:
      summary: Get Markup Configuration
      description: Returns the global price multiplier, per-service overrides, per-country overrides, and autoAdjustLoyalty flag.
      tags: [Reseller]
      responses:
        '200':
          description: Current markup config
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      global: { type: number, example: 2.0, description: "Multiplier applied to all platform prices." }
                      overrides:
                        type: object
                        description: Per-service multiplier overrides keyed by service code.
                        example: { wa: 1.8, tg: 2.5 }
                      countryOverrides:
                        type: object
                        description: Per-country multiplier overrides keyed by numeric country ID.
                        example: { "67": 1.5 }
                      autoAdjustLoyalty: { type: boolean, example: false }
                      version: { type: integer, example: 3 }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    patch:
      summary: Update Markup
      description: >
        Set global multiplier and optional per-service/country overrides.
        Sub-user price = platform_price × countryOverride × serviceOverride × global.
        Busts the markup cache immediately on save.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                global:
                  type: number
                  description: Multiplier applied to all prices. 1.0 = no markup.
                  example: 2.0
                overrides:
                  type: object
                  description: Per-service multipliers (keyed by service code).
                  example: { wa: 1.8 }
                countryOverrides:
                  type: object
                  description: Per-country multipliers (keyed by numeric country ID string).
                  example: { "67": 1.5 }
                autoAdjustLoyalty:
                  type: boolean
                  description: If true, loyalty discounts are passed through to sub-users.
                  example: false
            example:
              global: 2.0
              overrides: { wa: 1.8 }
              autoAdjustLoyalty: false
      responses:
        '200':
          description: Markup updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller/users:
    get:
      summary: List Sub-Users
      description: Returns all sub-users linked to this reseller account.
      tags: [Reseller]
      responses:
        '200':
          description: List of sub-users
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, example: "64f3b..." }
                        email: { type: string, example: "user@example.com" }
                        name: { type: string, example: "John Doe" }
                        balance: { type: number, example: 12.50 }
                        suspended: { type: boolean, example: false }
                        allowSelfFund: { type: boolean, example: true }
                        createdAt: { type: string, format: date-time }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      summary: Create Sub-User
      description: Manually create a sub-user. Optionally deduct initialBalance from your buying credits and allocate it to the new account.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, example: "user@example.com" }
                password: { type: string, example: "securepassword" }
                name: { type: string, example: "John Doe" }
                initialBalance: { type: number, example: 5.00, description: "Deducted from reseller buying credits." }
                allowSelfFund: { type: boolean, example: true }
            example:
              email: "user@example.com"
              password: "securepassword"
              name: "John Doe"
              initialBalance: 5.00
      responses:
        '200':
          description: Sub-user created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      email: { type: string }
        '409':
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient buying credits for initialBalance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller/users/{subUserId}:
    get:
      summary: Get Sub-User Details
      description: Returns full profile for one sub-user.
      tags: [Reseller]
      parameters:
        - in: path
          name: subUserId
          required: true
          schema: { type: string }
          example: "64f3b..."
      responses:
        '200':
          description: Sub-user details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      email: { type: string }
                      name: { type: string }
                      balance: { type: number }
                      suspended: { type: boolean }
                      allowSelfFund: { type: boolean }
                      createdAt: { type: string, format: date-time }
        '404':
          description: Sub-user not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    patch:
      summary: Update Sub-User
      description: Adjust balance delta, toggle suspension, or change allowSelfFund. balanceDelta is atomic — positive adds funds from reseller buying credits, negative deducts from sub-user.
      tags: [Reseller]
      parameters:
        - in: path
          name: subUserId
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                balanceDelta: { type: number, example: 10.00 }
                suspended: { type: boolean, example: false }
                allowSelfFund: { type: boolean, example: true }
            example:
              balanceDelta: 10.00
              suspended: false
      responses:
        '200':
          description: Sub-user updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
        '402':
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Delete Sub-User
      description: Permanently deletes the sub-user. Remaining balance is forfeited.
      tags: [Reseller]
      parameters:
        - in: path
          name: subUserId
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }

  /reseller/payout/convert:
    post:
      summary: Convert Earned Balance → Buying Credits
      description: Instantly move funds from earned balance (sub-user revenue) to buying credits wallet. Zero fee. Guarded atomically against race conditions.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, example: 15.00 }
            example:
              amount: 15.00
      responses:
        '200':
          description: Converted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      newEarnedBalance: { type: number, example: 3.75 }
                      newBuyingBalance: { type: number, example: 57.50 }
        '402':
          description: Insufficient earned balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller/payout/withdraw:
    post:
      summary: Withdraw Earned Balance as Crypto
      description: Submit a crypto withdrawal request. Minimum $10. Processed within 24 hours.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency, cryptoAddress]
              properties:
                amount: { type: number, example: 25.00 }
                currency: { type: string, example: "USDT", description: "Crypto ticker: USDT, BTC, ETH, SOL, etc." }
                cryptoAddress: { type: string, example: "TYour...WalletAddress" }
            example:
              amount: 25.00
              currency: "USDT"
              cryptoAddress: "TYour...WalletAddress"
      responses:
        '200':
          description: Withdrawal queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  message: { type: string, example: "Withdrawal request submitted. You will receive $24.63 after a $0.37 processing fee. Processed within 24h." }
                  gross: { type: number, example: 25.00, description: "Amount requested (deducted from earned balance)" }
                  fee: { type: number, example: 0.38, description: "Processing fee (1.5%, min $0.50)" }
                  netAmount: { type: number, example: 24.62, description: "Amount you will actually receive" }
                  earnedBalance: { type: number, example: 74.38, description: "Remaining earned balance after deduction" }
                  transactionId: { type: string, example: "60d21b4667d0d8992e610c85" }
        '400':
          description: Insufficient earned balance or below minimum
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller/payments/crypto:
    post:
      summary: Top Up Buying Credits via Crypto
      description: Create a Cryptomus invoice to add buying credits. Minimum $20. Returns a checkout URL to redirect to.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, example: 50.00 }
            example:
              amount: 50.00
      responses:
        '200':
          description: Invoice created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  url: { type: string, example: "https://pay.cryptomus.com/pay/8b03432e-385b-4670-8d06-064591096795" }
                  invoiceId: { type: string, example: "8b03432e-385b-4670-8d06-064591096795" }
                  transactionId: { type: string, example: "65bbe87b4098c17a31cff3e7" }

  /reseller/payments/card:
    post:
      summary: Top Up Buying Credits via Card
      description: Create a card checkout link (processed via FCE) to add buying credits. Minimum $20. Returns a checkout URL to redirect to.
      tags: [Reseller]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, example: 50.00 }
            example:
              amount: 50.00
      responses:
        '200':
          description: Checkout link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  url: { type: string, example: "https://pay.freecustom.email/..." }
                  transactionId: { type: string, example: "60d21b4667d0d8992e610c85" }
        '503':
          description: Card payments not configured or temporarily disabled on this platform
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /reseller-public/{resellerPublicId}/brand:
    get:
      summary: Get Reseller Brand Info
      description: Returns white-label brand data (name, logo URL, color) for the signup page. No authentication required.
      security: []
      tags: [Reseller Public]
      parameters:
        - in: path
          name: resellerPublicId
          required: true
          schema: { type: string }
          example: "a26e2be6"
      responses:
        '200':
          description: Brand info
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      brandName: { type: string, nullable: true, example: "Acme OTP" }
                      logoUrl: { type: string, nullable: true, example: "https://acme.com/logo.png" }
                      color: { type: string, nullable: true, example: "#6366f1" }
        '404':
          description: Reseller not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller-public/{resellerPublicId}/signup:
    post:
      summary: Register as Sub-User
      description: >
        Creates a new account linked to this reseller. Requires a Cloudflare Turnstile token.
        Sends an email OTP for verification. Sub-users use `POST /api/auth/verify-email` next.
      security: []
      tags: [Reseller Public]
      parameters:
        - in: path
          name: resellerPublicId
          required: true
          schema: { type: string }
          example: "a26e2be6"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, turnstileToken]
              properties:
                email: { type: string, example: "newuser@example.com" }
                password: { type: string, example: "securepassword" }
                name: { type: string, example: "Jane Doe" }
                turnstileToken: { type: string, example: "0.xxxxx..." }
            example:
              email: "newuser@example.com"
              password: "securepassword"
              name: "Jane Doe"
              turnstileToken: "0.xxxxx..."
      responses:
        '200':
          description: Registration successful — OTP email sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  message: { type: string, example: "Account created. Check your email for the verification code." }
        '409':
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Reseller public ID not found or reseller disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /reseller-public/{resellerPublicId}/topup/crypto:
    post:
      summary: Sub-User Crypto Top-Up
      description: >
        Creates a Cryptomus invoice for a sub-user to fund their own balance.
        Authenticate as the sub-user (Bearer notp_... API key or session cookie) — NOT as the reseller.
        Minimum = reseller's configured minSubUserTopup. Self-funding must be enabled by the reseller.
      tags: [Reseller Public]
      parameters:
        - in: path
          name: resellerPublicId
          required: true
          schema: { type: string }
          example: "a26e2be6"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, example: 20.00 }
            example:
              amount: 20.00
      responses:
        '200':
          description: Invoice created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  url: { type: string, example: "https://pay.cryptomus.com/pay/8b03432e-385b-4670-8d06-064591096795" }
                  invoiceId: { type: string, example: "8b03432e-385b-4670-8d06-064591096795" }
                  transactionId: { type: string, example: "60d21b4667d0d8992e610c85" }
        '401':
          description: Unauthorized — must authenticate as sub-user, not reseller
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '403':
          description: >
            Access denied. Either you authenticated as the reseller (use sub-user credentials instead),
            or self-funding is disabled. Contact your provider to enable it.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /reseller-public/{resellerPublicId}/topup/card:
    post:
      summary: Sub-User Card Top-Up (Card via FCE)
      description: >
        Creates a card checkout link (processed via FCE) for a sub-user to fund their own balance.
        Authenticate as the sub-user (Bearer notp_... API key or session cookie) — NOT as the reseller.
        Minimum = reseller's configured minSubUserTopup. Self-funding must be enabled by the reseller.
        Cards must be enabled on the platform.
      tags: [Reseller Public]
      parameters:
        - in: path
          name: resellerPublicId
          required: true
          schema: { type: string }
          example: "a26e2be6"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, example: 20.00 }
            example:
              amount: 20.00
      responses:
        '200':
          description: Checkout session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  url: { type: string, example: "https://pay.freecustom.email/..." }
                  transactionId: { type: string, example: "60d21b4667d0d8992e610c85" }
        '401':
          description: Unauthorized — must authenticate as sub-user
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '403':
          description: >
            Access denied. Either you authenticated as the reseller (use sub-user credentials instead),
            or self-funding is disabled. Contact your provider to enable it.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: Card payments not configured on this platform
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

tags:
  - name: Public
    description: No authentication required. Rate-limited to 100 requests/minute per IP.
  - name: Activations
    description: Purchase numbers and retrieve OTP codes. All endpoints require authentication.
  - name: Reseller
    description: >
      Reseller management endpoints. Require Bearer authentication from a reseller-enabled account.
      Base URL: `https://api.numberotp.com/v1`
  - name: Reseller Public
    description: No authentication required. Called by sub-users on the reseller's white-label signup page.
