openapi: 3.0.3
info:
  title: xhr.dev on-prem API
  version: 2.0.0
  description: |
    xhr.dev solves Akamai Bot Manager and DataDome challenges. This spec
    describes the HTTP API exposed by the on-prem Docker container you run
    inside your own infrastructure — there is no hosted/cloud server.

    Akamai additionally exposes a stateful WebSocket session at
    `/akamai/session`, documented separately (see docs.xhr.dev/api-akamai)
    since OpenAPI 3.0 doesn't model WebSocket protocols.
  contact:
    name: xhr.dev support
    email: info@xhr.dev

servers:
  - url: http://localhost:3000
    description: Your self-hosted xhrdev container

tags:
  - name: health
    description: Container liveness
  - name: akamai
    description: |
      Solves Akamai Bot Manager sensor challenges (_abck / bm-sz) and SBSD.
      /solve fetches and drives the challenge itself; /mitm-solve accepts an
      already-captured challenge from your own MITM proxy.
  - name: datadome
    description: |
      Solves DataDome captcha and interstitial challenges. Single-shot HTTP
      — no WebSocket session (unlike Akamai). /solve takes an
      already-extracted challenge payload; /mitm-solve parses the challenge
      out of a raw captured response itself.

paths:
  /hc:
    get:
      operationId: getHealthCheck
      summary: Health check
      tags: [health]
      responses:
        '200':
          description: Container is up
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    type: string
                    enum: ['ok']

  /akamai/queue-metrics:
    get:
      operationId: getAkamaiQueueMetrics
      summary: Solve queue metrics
      tags: [akamai]
      responses:
        '200':
          description: Current queue state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueMetrics'

  /akamai/solve:
    post:
      operationId: postAkamaiSolve
      summary: Solve an Akamai challenge starting from a URL
      tags: [akamai]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AkamaiSolveRequest'
      responses:
        '200':
          description: Solve result (or capture result if submit=false)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AkamaiSolveResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Solve failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AkamaiSolveResponse'

  /akamai/mitm-solve:
    post:
      operationId: postAkamaiMitmSolve
      summary: Solve an Akamai challenge from an already-captured response
      tags: [akamai]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AkamaiMitmSolveRequest'
      responses:
        '200':
          description: Solve result (or capture result if submit=false)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AkamaiSolveResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Solve failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AkamaiSolveResponse'

  /dd/solve:
    post:
      operationId: postDdSolve
      summary: Solve a DataDome challenge given an extracted payload
      tags: [datadome]
      parameters:
        - name: submit
          in: query
          required: false
          description: Set false to return the built payload without submitting it
          schema:
            type: boolean
            default: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DatadomeSolveRequest'
      responses:
        '200':
          description: Solved cookie, or built payload if submit=false
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DatadomeSolveResponse'
                  - $ref: '#/components/schemas/DatadomeCaptureResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Submit rejected by DataDome
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Solve failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /dd/mitm-solve:
    post:
      operationId: postDdMitmSolve
      summary: Solve a DataDome challenge from an already-captured response
      tags: [datadome]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DatadomeMitmSolveRequest'
      responses:
        '200':
          description: Merged cookie header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatadomeMitmSolveResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatadomeMitmSolveResponse'
        '422':
          description: Submit rejected by DataDome
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatadomeMitmSolveResponse'
        '500':
          description: Solve failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatadomeMitmSolveResponse'

components:
  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string

    QueueMetrics:
      type: object
      required: [active, queued, totalAdmitted, totalCompleted, totalRejected, totalTimedOut]
      properties:
        active:
          type: integer
        queued:
          type: integer
        totalAdmitted:
          type: integer
        totalCompleted:
          type: integer
        totalRejected:
          type: integer
        totalTimedOut:
          type: integer

    ProfileSnapshot:
      type: object
      required: [id, chromeFullVersion, os, timezone, timezoneOffsetMinutes, tlsClientHello, userAgent]
      properties:
        id:
          type: string
          description: 'Format: chrome-<version>-<os>'
          example: chrome-146-macos
        chromeFullVersion:
          type: string
          example: 146.0.7680.81
        os:
          type: string
          enum: [linux, macos, windows, windows10, windows11]
        timezone:
          type: string
          example: America/New_York
        timezoneOffsetMinutes:
          type: integer
          example: -300
        tlsClientHello:
          type: string
          example: chrome_146
        userAgent:
          type: string
        httpHeaderTemplates:
          $ref: '#/components/schemas/HeaderTemplates'

    HeaderTemplates:
      type: object
      properties:
        form:
          type: array
          items:
            type: array
            items:
              type: string
        iframe:
          type: array
          items:
            type: array
            items:
              type: string
        image:
          type: array
          items:
            type: array
            items:
              type: string
        xhr:
          type: array
          items:
            type: array
            items:
              type: string

    JsProfilePayload:
      type: object
      description: >-
        Browser fingerprint payload. Partial objects are accepted; unset
        fields fall back to solver defaults. See the Akamai/DataDome API
        reference docs for the full field list.
      properties:
        os:
          type: string
          enum: [macos, ubuntu, windows10, windows11]
        chromeVersion:
          type: string
        hardwareConcurrency:
          type: integer
        deviceMemory:
          type: integer
        timezone:
          type: string
        hardware:
          type: object
          properties:
            canvas:
              type: string
            emptyCanvas:
              type: string
            fonts:
              type: array
              items:
                type: string
            webgl:
              type: object
              properties:
                image:
                  type: string
                renderer:
                  type: string
                vendor:
                  type: string
                params:
                  type: object
        screen:
          type: object
          properties:
            outerHeight:
              type: number
            innerHeight:
              type: number
            width:
              type: number
            height:
              type: number
      additionalProperties: true

    AkamaiSolveRequest:
      type: object
      required: [url, profile, js_profile]
      properties:
        url:
          type: string
          format: uri
        profile:
          $ref: '#/components/schemas/ProfileSnapshot'
        js_profile:
          $ref: '#/components/schemas/JsProfilePayload'
        script:
          type: object
          properties:
            html:
              type: string
            js:
              type: string
            url:
              type: string
              format: uri
        cookies:
          type: object
          additionalProperties:
            type: string
        proxy:
          type: string
        mode:
          type: string
          enum: [abck, sbsd]
        maxSensors:
          type: integer
        submit:
          type: boolean
          default: true
        timeout:
          type: integer
          default: 20000
        acceptCookieName:
          type: string
          default: _abck
        request_id:
          type: string
        attempt_id:
          type: string
        correlation_id:
          type: string
        readiness_state:
          type: string
          enum: [created, ready, solving, completed, aborted]
        spider:
          type: string

    AkamaiMitmSolveRequest:
      allOf:
        - $ref: '#/components/schemas/AkamaiSolveRequest'
        - type: object
          required: [original_url, profile_id, challenge_body, js_profile]
          properties:
            original_url:
              type: string
              format: uri
            profile_id:
              type: string
            challenge_body:
              type: string
            challenge_headers:
              type: array
              items:
                type: array
                items:
                  type: string
            challenge_status:
              type: integer
            original_cookies:
              type: string
            original_headers:
              type: array
              items:
                type: array
                items:
                  type: string
            original_method:
              type: string
            proxy_url:
              type: string
            upstream_proxy:
              type: string
            script_id:
              type: string
            user_agent:
              type: string
            http_header_templates:
              $ref: '#/components/schemas/HeaderTemplates'

    AkamaiSolveResponse:
      type: object
      properties:
        success:
          type: boolean
        accepted:
          type: boolean
        acceptance_cookie:
          type: string
        cookie_header:
          type: string
        cookies:
          type: object
          additionalProperties:
            type: string
        mode:
          type: string
          enum: [abck, sbsd]
        outcome:
          type: string
        outcome_reason:
          type: string
        sensors_sent:
          type: integer
        max_sensors_reached:
          type: boolean
        last_response_status:
          type: integer
        error:
          type: string

    DDChallenge:
      type: object
      required: [cid, hsh, rt, s]
      properties:
        cid:
          type: string
        hsh:
          type: string
        rt:
          type: string
          enum: [c, i]
        s:
          type: number
        ir:
          type: integer
        t:
          type: string
        b:
          type: integer
        e:
          type: string

    DatadomeSolveRequest:
      type: object
      required: [url, dd, ddCookie, profile, js_profile]
      properties:
        url:
          type: string
          format: uri
        dd:
          $ref: '#/components/schemas/DDChallenge'
        ddCookie:
          type: string
        profile:
          $ref: '#/components/schemas/ProfileSnapshot'
        js_profile:
          $ref: '#/components/schemas/JsProfilePayload'
        proxy:
          type: string
        os:
          type: string
          enum: [ubuntu, windows, windows10, windows11, macos]
        script_id:
          type: string
        interstitialUrl:
          type: string
          format: uri
        timeout:
          type: integer
          minimum: 1
          maximum: 120000
        spider:
          type: string
        iframeData:
          type: object
          properties:
            html:
              type: string
            url:
              type: string
              format: uri
            captchaLayout:
              type: string
            finalNavigationResponseBodySizes:
              type: array
              items:
                type: integer

    DatadomeSolveResponse:
      type: object
      required: [cookie]
      properties:
        cookie:
          type: string

    DatadomeCaptureResponse:
      type: object
      required: [body, origin, referer, url]
      properties:
        body:
          type: string
        origin:
          type: string
        referer:
          type: string
        url:
          type: string

    DatadomeMitmSolveRequest:
      type: object
      required: [original_url, profile_id, challenge_body, challenge_headers, js_profile]
      properties:
        original_url:
          type: string
          format: uri
        profile_id:
          type: string
        challenge_body:
          type: string
        challenge_headers:
          type: array
          items:
            type: array
            items:
              type: string
        js_profile:
          $ref: '#/components/schemas/JsProfilePayload'
        interstitial_url:
          type: string
          format: uri
        http_header_templates:
          $ref: '#/components/schemas/HeaderTemplates'
        device_memory:
          type: integer
        script_id:
          type: string
        proxy_url:
          type: string
        upstream_proxy:
          type: string
        sec_ch_ua:
          type: string
        sec_ch_ua_arch:
          type: string
        sec_ch_ua_full_version_list:
          type: string
        sec_ch_ua_model:
          type: string
        timezone:
          type: string
        timezone_offset_minutes:
          type: integer
        user_agent:
          type: string

    DatadomeMitmSolveResponse:
      type: object
      required: [success, cookies]
      properties:
        success:
          type: boolean
        cookies:
          type: string
        error:
          type: string
