openapi: 3.1.0
info:
  title: ProxyStream Reseller API
  version: 1.0.0
  description: |
    Buy and manage ProxyStream mobile proxies from your own systems.

    **Authentication.** Every request needs `Authorization: Bearer <api key>`.
    Keys are issued by ProxyStream support and are bound to one account; its
    balance pays for purchases and it owns every proxy you buy.

    **Money.** All amounts are USD numbers with two decimals.

    **Rate limit.** 120 requests per minute per key. Over the limit you get
    `429` with a `Retry-After` header (seconds).

    **Errors.** Every error is an RFC 7807 `application/problem+json` body.
    Switch on `type`; `detail` is a human-readable explanation.

    **Versioning.** `/v1` only gains new optional fields and new endpoints.
    Removing or renaming anything ships as `/v2`, with `/v1` kept running for
    at least 6 months after `/v2` is announced.
  contact:
    email: support@proxystream.net
servers:
  - url: https://proxystream.net/api/reseller/v1
security:
  - ApiKey: []
tags:
  - name: Catalog
    description: What you can buy and what it costs.
  - name: Account
    description: Your reseller account.
  - name: Proxies
    description: Buy proxies and manage the ones you own.

paths:
  /servers:
    get:
      tags: [Catalog]
      operationId: listServers
      summary: List servers you can buy on
      description: |
        Servers that are online and have free capacity right now, best quality
        first. A server that fills up disappears from this list; buying on it
        returns `server_unavailable`. The list is small, so it is not paginated.
      responses:
        "200":
          description: Available servers with your prices
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ServerListing" }
              example:
                data:
                  - id: 12
                    country: US
                    region: Texas
                    isp: T-Mobile
                    type: Mobile 5G
                    rotation_minutes: 15
                    prices:
                      - { duration: 4h, price: 0.4 }
                      - { duration: 1d, price: 1 }
                      - { duration: 3d, price: 2.5 }
                      - { duration: 1w, price: 5 }
                      - { duration: 1mo, price: 15 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /balance:
    get:
      tags: [Account]
      operationId: getBalance
      summary: Get your account balance
      responses:
        "200":
          description: Current balance
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Balance" }
              example: { balance: 42.5, currency: USD }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /proxies:
    get:
      tags: [Proxies]
      operationId: listProxies
      summary: List your proxies
      description: Newest first. Follow `next_cursor` until it is `null`.
      parameters:
        - name: status
          in: query
          description: Only return proxies in this state. Omit for all.
          schema: { $ref: "#/components/schemas/ProxyStatus" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: cursor
          in: query
          description: The `next_cursor` from the previous page, unchanged.
          schema: { type: string }
      responses:
        "200":
          description: One page of proxies
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Proxy" }
                  next_cursor:
                    type: [string, "null"]
                    description: Pass as `cursor` to get the next page. `null` on the last page.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [Proxies]
      operationId: createProxy
      summary: Buy a proxy
      description: |
        Charges your balance the price listed in `GET /servers` and returns the
        new proxy, ready to use. Credentials are generated for you.

        Not idempotent: retrying after a timeout can buy a second proxy. After
        a timeout, check `GET /proxies?status=active` before retrying.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [server_id, duration]
              properties:
                server_id: { type: integer, minimum: 1, example: 12 }
                duration: { $ref: "#/components/schemas/Duration" }
            example: { server_id: 12, duration: 1w }
      responses:
        "201":
          description: Proxy bought
          headers:
            Location:
              description: URL of the new proxy
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Proxy" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientBalance" }
        "409": { $ref: "#/components/responses/ServerUnavailable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /proxies/{id}:
    parameters:
      - $ref: "#/components/parameters/ProxyId"
    get:
      tags: [Proxies]
      operationId: getProxy
      summary: Get a proxy
      responses:
        "200":
          description: The proxy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Proxy" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /proxies/{id}/whitelist-ip:
    parameters:
      - $ref: "#/components/parameters/ProxyId"
    put:
      tags: [Proxies]
      operationId: setProxyWhitelistIp
      summary: Set or clear the whitelist IP
      description: |
        Requests from the whitelisted IP are accepted without credentials.
        Username/password auth keeps working either way. Takes effect within
        about a minute.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [whitelist_ip]
              properties:
                whitelist_ip:
                  type: [string, "null"]
                  format: ipv4
                  description: IPv4 address, or `null` to clear.
            example: { whitelist_ip: 203.0.113.7 }
      responses:
        "200":
          description: Updated proxy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Proxy" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ProxyExpired" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /proxies/{id}/password:
    parameters:
      - $ref: "#/components/parameters/ProxyId"
    put:
      tags: [Proxies]
      operationId: setProxyPassword
      summary: Change the proxy password
      description: Takes effect within about a minute. The username does not change.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password:
                  type: string
                  pattern: "^[A-Za-z0-9._-]{6,64}$"
                  description: 6-64 characters. Letters, digits, `.`, `_` and `-`.
            example: { password: k3y-Rotated_2 }
      responses:
        "200":
          description: Updated proxy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Proxy" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ProxyExpired" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /proxies/{id}/server:
    parameters:
      - $ref: "#/components/parameters/ProxyId"
    put:
      tags: [Proxies]
      operationId: setProxyServer
      summary: Move the proxy to another server
      description: |
        Free of charge; the expiry date is unchanged. `host` and the ports can
        change, so use the returned proxy. Pick the target from `GET /servers`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [server_id]
              properties:
                server_id: { type: integer, minimum: 1 }
            example: { server_id: 17 }
      responses:
        "200":
          description: Updated proxy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Proxy" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: The proxy has expired (`proxy_expired`) or the target server is full or offline (`server_unavailable`).
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: "Your reseller API key, e.g. `Authorization: Bearer psr_...`"

  parameters:
    ProxyId:
      name: id
      in: path
      required: true
      schema: { type: integer, minimum: 1 }

  schemas:
    Duration:
      type: string
      enum: [4h, 1d, 3d, 1w, 1mo]
      description: "`4h` 4 hours, `1d` 1 day, `3d` 3 days, `1w` 7 days, `1mo` 1 calendar month."

    ProxyStatus:
      type: string
      enum: [active, expired]

    Server:
      type: object
      required: [id, country, region, isp, type, rotation_minutes]
      properties:
        id: { type: integer, example: 12 }
        country: { type: string, description: ISO 3166-1 alpha-2, example: US }
        region: { type: string, example: Texas }
        isp: { type: string, example: T-Mobile }
        type:
          type: string
          enum: [Mobile 5G, Mobile 4G, Residential, Datacenter]
        rotation_minutes:
          type: [integer, "null"]
          description: How often the exit IP rotates.
          example: 15

    ServerListing:
      allOf:
        - $ref: "#/components/schemas/Server"
        - type: object
          required: [prices]
          properties:
            prices:
              type: array
              items:
                type: object
                required: [duration, price]
                properties:
                  duration: { $ref: "#/components/schemas/Duration" }
                  price: { type: number, description: USD, example: 5 }

    Balance:
      type: object
      required: [balance, currency]
      properties:
        balance: { type: number, example: 42.5 }
        currency: { type: string, const: USD }

    Proxy:
      type: object
      required: [id, status, host, http_port, socks_port, username, password, whitelist_ip, server, created_at, expires_at]
      properties:
        id: { type: integer, example: 90412 }
        status: { $ref: "#/components/schemas/ProxyStatus" }
        host: { type: string, example: 168.231.79.20 }
        http_port: { type: [integer, "null"], example: 8010 }
        socks_port: { type: [integer, "null"], example: 9010 }
        username: { type: string, example: a7k2m9x }
        password: { type: string, example: q4w8e1r }
        whitelist_ip: { type: [string, "null"], format: ipv4, example: null }
        server: { $ref: "#/components/schemas/Server" }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
      example:
        id: 90412
        status: active
        host: 168.231.79.20
        http_port: 8010
        socks_port: 9010
        username: a7k2m9x
        password: q4w8e1r
        whitelist_ip: null
        server: { id: 12, country: US, region: Texas, isp: T-Mobile, type: Mobile 5G, rotation_minutes: 15 }
        created_at: "2026-09-27T10:00:00.000Z"
        expires_at: "2026-10-04T10:00:00.000Z"

    Problem:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          enum:
            - https://proxystream.net/docs/reseller-api#invalid_request
            - https://proxystream.net/docs/reseller-api#unauthorized
            - https://proxystream.net/docs/reseller-api#insufficient_balance
            - https://proxystream.net/docs/reseller-api#not_found
            - https://proxystream.net/docs/reseller-api#proxy_expired
            - https://proxystream.net/docs/reseller-api#server_unavailable
            - https://proxystream.net/docs/reseller-api#rate_limited
            - https://proxystream.net/docs/reseller-api#internal_error
        title: { type: string }
        status: { type: integer }
        detail: { type: string }

  responses:
    BadRequest:
      description: "`invalid_request`: a parameter or the body is invalid; `detail` says which."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: https://proxystream.net/docs/reseller-api#invalid_request
            title: Invalid request
            status: 400
            detail: "duration must be one of: 4h, 1d, 3d, 1w, 1mo."
    Unauthorized:
      description: "`unauthorized`: the API key is missing, invalid or revoked."
      headers:
        WWW-Authenticate:
          schema: { type: string }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    InsufficientBalance:
      description: "`insufficient_balance`: top up and retry. Nothing was charged."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: https://proxystream.net/docs/reseller-api#insufficient_balance
            title: Insufficient balance
            status: 402
            detail: This purchase costs 5.00 USD. Top up your balance and retry.
    NotFound:
      description: "`not_found`: no proxy with this id on your account."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    ProxyExpired:
      description: "`proxy_expired`: expired proxies cannot be changed."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    ServerUnavailable:
      description: "`server_unavailable`: the server is full or offline. Nothing was charged; pick another from `GET /servers`."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    TooManyRequests:
      description: "`rate_limited`: wait `Retry-After` seconds."
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    InternalError:
      description: "`internal_error`: unexpected failure; safe to retry reads."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
