openapi: 3.1.0

info:
  title: Robolist REST API
  version: "1.0"
  description: |
    Programmatic access to the Robolist.ai robotics market.
    Requires an API key (Verified plan or above).

    **Authentication**
    Pass your API key as a Bearer token:
    ```
    Authorization: Bearer rk_live_<your-key>
    ```

    **Watermarking**
    Every response includes a per-request `X-Robolist-Token` header.
    The JSON body is un-watermarked so edge caches can serve it generically.
    Our Terms of Use state that responses are watermarked for leak tracing.

    **Caching**
    List and detail responses are cacheable: `s-maxage=60, stale-while-revalidate=600`.
    The watermark token is per-request (in the header, not the body) so caching
    does not reduce traceability.

    **Rate limits**

    Plans are billed annually, so quotas are annual. Enforcement is a
    per-second token bucket sized to the daily average. Burst headroom
    ≈ 10% of the daily cap.

    | Plan       | Annual quota                | Avg / day |
    |------------|-----------------------------|-----------|
    | Verified   | 30,000                      | ~82       |
    | Pro        | 1,000,000                   | ~2,740    |
    | Enterprise | Custom                      | —         |

    **Field scope:** Verified and Pro responses include headline specs only.
    Category-specific deep specs (humanoid DOFs, AMR navigation type, etc.)
    are gated to Enterprise and the Institutional Data License.

    Every 2xx response carries `X-RateLimit-Limit` (daily cap) and
    `X-RateLimit-Period: 1d`. On rate limit: HTTP 429 with `Retry-After`.

servers:
  - url: https://robolist.ai/api/v1
    description: Production

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key (rk_live_* / rk_test_*)

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - rate_limited
                - not_found
                - invalid_request
                - internal
              description: Machine-readable error code.
            message:
              type: string
              description: Human-readable description. Safe to display to end users.
            request_id:
              type: string
              format: uuid
              description: Unique per-request identifier. Include when contacting support.

    RobotSummary:
      type: object
      description: Lightweight robot entry as returned by the search endpoint.
      required: [id, slug, name, category, companyName, companySlug, url]
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          example: ur5e
        name:
          type: string
          example: UR5e
        category:
          type: string
          example: cobot
        companyName:
          type: string
          example: Universal Robots
        companySlug:
          type: string
          example: universal-robots
        hqCountry:
          type: string
          nullable: true
          example: DK
          description: ISO-2 country code of the manufacturer's HQ.
        launchYear:
          type: integer
          nullable: true
          example: 2018
        priceUsd:
          type: number
          nullable: true
          example: 34990
        payloadKg:
          type: number
          nullable: true
          example: 5
        reachMm:
          type: number
          nullable: true
          example: 850
        ipRating:
          type: string
          nullable: true
          example: IP54
        productScore:
          type: number
          nullable: true
          example: 78.4
          description: Robo Index (skip-and-normalize). Higher is better.
        url:
          type: string
          example: /companies/universal-robots/robots/ur5e
          description: Canonical Robolist.ai browser URL for this robot.

    RobotSpecs:
      type: object
      properties:
        payloadKg:   { type: number, nullable: true }
        reachMm:     { type: number, nullable: true }
        speedMs:     { type: number, nullable: true }
        batteryHours:{ type: number, nullable: true }
        weightKg:    { type: number, nullable: true }
        powerW:      { type: number, nullable: true }
        ipRating:    { type: string, nullable: true }
        autonomyLevel:{ type: string, nullable: true }
        certList:
          type: array
          items: { type: string }

    RobotDetail:
      description: Full robot record with specs and company.
      allOf:
        - $ref: '#/components/schemas/RobotSummary'
      properties:
        description:
          type: string
          nullable: true
        availability:
          type: string
          nullable: true
          example: shipping
        countryOfOrigin:
          type: string
          nullable: true
          example: DK
        specs:
          $ref: '#/components/schemas/RobotSpecs'
        specsExtra:
          type: object
          nullable: true
          additionalProperties: true
          description: |
            Category-specific deep specs (humanoid DOFs, AMR navigation type, etc.).
            **Gated to Enterprise.** Verified and Pro responses always set this to `null`.
        heroImageUrl:
          type: string
          nullable: true
        company:
          type: object
          required: [id, slug, name, isClaimed]
          properties:
            id:       { type: string, format: uuid }
            slug:     { type: string }
            name:     { type: string }
            hqCountry:{ type: string, nullable: true }
            website:  { type: string, nullable: true }
            isClaimed:
              type: boolean
              description: True when an owner has claimed the company page on Robolist.

    RobotCreate:
      type: object
      description: Request body for `POST /v1/robots`.
      required: [name, category]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 120
          example: My Robot
        slug:
          type: string
          minLength: 2
          maxLength: 80
          pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
          description: Optional. Lowercase letters, digits, and hyphens. Derived from `name` if omitted.
        category:
          type: string
          description: Canonical category slug. See the categories endpoint for the canonical list.
          example: amr-warehouse
        description:
          type: string
          maxLength: 2000
          nullable: true
        launch_year:
          type: integer
          minimum: 1990
          maximum: 2100
          nullable: true

    RobotPatch:
      type: object
      description: Request body for `PATCH /v1/robots/{id}`. Any subset of the writable fields.
      properties:
        name:        { type: string, minLength: 2, maxLength: 120 }
        category:    { type: string }
        description: { type: string, maxLength: 2000, nullable: true }
        launch_year: { type: integer, minimum: 1990, maximum: 2100, nullable: true }

    SearchMeta:
      type: object
      required: [total, limit, offset, has_more]
      properties:
        total:    { type: integer }
        limit:    { type: integer }
        offset:   { type: integer }
        has_more: { type: boolean }

    CompareMeta:
      type: object
      required: [found, not_found]
      properties:
        found:     { type: integer }
        not_found: { type: integer }

  responses:
    BadRequest:
      description: Invalid query parameters.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Valid key but insufficient scope or plan.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until the next request will be accepted.
        X-RateLimit-Remaining:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

paths:
  /robots/search:
    get:
      operationId: searchRobots
      summary: Search the robot market
      description: |
        Filter-based robot search. Results are ordered by Robo Index
        (descending) — you cannot change the sort order. The Robo Index is
        entirely independent of any subscription tier or business relationship.
      parameters:
        - name: category
          in: query
          schema:
            type: string
          description: Canonical category slug (e.g. `cobot`, `humanoid`, `amr`). Use the categories endpoint if unsure.
        - name: priceMaxUsd
          in: query
          schema:
            type: number
          description: Maximum retail price in USD (inclusive).
        - name: priceMinUsd
          in: query
          schema:
            type: number
          description: Minimum retail price in USD (inclusive).
        - name: payloadMinKg
          in: query
          schema:
            type: number
          description: Minimum payload capacity in kg.
        - name: reachMinMm
          in: query
          schema:
            type: number
          description: Minimum reach in mm.
        - name: hqCountry
          in: query
          schema:
            type: string
            minLength: 2
            maxLength: 2
          description: ISO-2 country code for the manufacturer's HQ (e.g. `DE`, `JP`, `CN`).
        - name: claimedOnly
          in: query
          schema:
            type: boolean
          description: When true, only return robots whose company has been claimed on Robolist.
        - name: mode
          in: query
          schema:
            type: string
            enum: [commercial, all, research]
            default: commercial
          description: Market slice. `commercial` (default) is the buyer-facing view.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Success
          headers:
            X-Robolist-Token:
              schema: { type: string }
              description: Per-request watermark token. Internal format.
            Cache-Control:
              schema: { type: string }
              example: public, s-maxage=60, stale-while-revalidate=600
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, request_id]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/RobotSummary' }
                  meta: { $ref: '#/components/schemas/SearchMeta' }
                  request_id: { type: string, format: uuid }
        "400": { $ref: '#/components/responses/BadRequest' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "429": { $ref: '#/components/responses/RateLimited' }
        "500": { $ref: '#/components/responses/InternalError' }

  /robots:
    post:
      operationId: createRobot
      summary: Create a robot
      description: |
        Create a robot under the API key's owning company. Requires the
        `write:robots` scope. Body is the same validation surface used by the
        dashboard create form. `slug` is derived server-side from `name` if
        omitted.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RobotCreate' }
      responses:
        "201":
          description: Created
          headers:
            X-Robolist-Token:
              schema: { type: string }
          content:
            application/json:
              schema:
                type: object
                required: [robot, request_id]
                properties:
                  robot:
                    type: object
                    required: [id, slug, name, category, created_at]
                    properties:
                      id: { type: string, format: uuid }
                      slug: { type: string }
                      name: { type: string }
                      category: { type: string }
                      created_at: { type: string, format: date-time }
                  request_id: { type: string, format: uuid }
        "400": { $ref: '#/components/responses/BadRequest' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402":
          description: Plan limit reached — upgrade to add more robots.
        "403":
          description: Free plan or missing `write:robots` scope.
        "429": { $ref: '#/components/responses/RateLimited' }
        "500": { $ref: '#/components/responses/InternalError' }

  /robots/{id}:
    get:
      operationId: getRobot
      summary: Get full robot details
      description: |
        Returns the full spec card for a single robot. `:id` may be a UUID or a
        slug — both are accepted. On a miss, returns 404.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Robot UUID or slug.
      responses:
        "200":
          description: Success
          headers:
            X-Robolist-Token:
              schema: { type: string }
            Cache-Control:
              schema: { type: string }
              example: public, s-maxage=60, stale-while-revalidate=600
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: '#/components/schemas/RobotDetail' }
                  request_id: { type: string, format: uuid }
        "400": { $ref: '#/components/responses/BadRequest' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "404": { $ref: '#/components/responses/NotFound' }
        "429": { $ref: '#/components/responses/RateLimited' }
        "500": { $ref: '#/components/responses/InternalError' }
    patch:
      operationId: updateRobot
      summary: Update a robot
      description: |
        Partial-update fields on a robot owned by the API key's company.
        Requires the `write:robots` scope. Cross-company access returns 404 —
        the API will not confirm whether someone else's robot exists. `slug` is
        immutable after creation.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: Robot UUID or slug.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RobotPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                required: [robot, request_id]
                properties:
                  robot:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      slug: { type: string }
                      name: { type: string }
                      category: { type: string }
                      description: { type: string, nullable: true }
                      launch_year: { type: integer, nullable: true }
                      updated_at: { type: string, format: date-time }
                  request_id: { type: string, format: uuid }
        "400": { $ref: '#/components/responses/BadRequest' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403":
          description: Free plan or missing `write:robots` scope.
        "404": { $ref: '#/components/responses/NotFound' }
        "429": { $ref: '#/components/responses/RateLimited' }
        "500": { $ref: '#/components/responses/InternalError' }

  /compare:
    get:
      operationId: compareRobots
      summary: Side-by-side spec comparison
      description: |
        Compare 2–4 robots by spec. Returns the full spec card for each robot
        in the order requested. Robots that cannot be resolved are listed in
        `data.notFound`.

        Does **not** generate an AI narrative — that surface is buyer-UI only.
        This endpoint returns the raw structured spec table.
      parameters:
        - name: ids
          in: query
          required: true
          schema:
            type: string
          description: |
            Comma-separated list of 2–4 robot UUIDs or slugs.
            Example: `ids=uuid1,uuid2` or `ids=ur5e,ur10e`
      responses:
        "200":
          description: Success
          headers:
            X-Robolist-Token:
              schema: { type: string }
            Cache-Control:
              schema: { type: string }
              example: public, s-maxage=60, stale-while-revalidate=600
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, request_id]
                properties:
                  data:
                    type: object
                    required: [robots, notFound]
                    properties:
                      robots:
                        type: array
                        items: { $ref: '#/components/schemas/RobotDetail' }
                      notFound:
                        type: array
                        description: Refs that could not be resolved.
                        items:
                          type: object
                          properties:
                            id:   { type: string, format: uuid }
                            slug: { type: string }
                  meta: { $ref: '#/components/schemas/CompareMeta' }
                  request_id: { type: string, format: uuid }
        "400": { $ref: '#/components/responses/BadRequest' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "429": { $ref: '#/components/responses/RateLimited' }
        "500": { $ref: '#/components/responses/InternalError' }
