{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/difficat-com/json-schema/difficat-com-start-job-request-schema.json",
  "title": "StartJobRequest",
  "x-generated": "2026-11-05",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/difficat-com-openapi.yml#/components/schemas/StartJobRequest",
  "type": "object",
  "anyOf": [
    {
      "required": [
        "sourceUrl"
      ]
    },
    {
      "required": [
        "urls"
      ]
    }
  ],
  "properties": {
    "sourceUrl": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048,
      "description": "Site A, the reference site (full URL). Required unless urls is given."
    },
    "targetUrl": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048
    },
    "sourceAuth": {
      "$ref": "#/$defs/BasicAuth"
    },
    "targetAuth": {
      "$ref": "#/$defs/BasicAuth"
    },
    "sourceLogin": {
      "type": "string",
      "enum": [
        "none"
      ],
      "description": "\"none\" to compare Site A's sign-in page as it is. Without it or sourceCookie, a site behind a sign-in form is refused unpaid."
    },
    "targetLogin": {
      "type": "string",
      "enum": [
        "none"
      ],
      "description": "\"none\" to compare Site B's sign-in page as it is. Without it or targetCookie, a site behind a sign-in form is refused unpaid."
    },
    "sourceCookie": {
      "type": "string",
      "maxLength": 8192,
      "description": "The value of the Cookie request header from a browser signed in to Site A, sent only to Site A's host, its www. twin and their subdomains. At most 8192 characters."
    },
    "targetCookie": {
      "type": "string",
      "maxLength": 8192,
      "description": "The value of the Cookie request header from a browser signed in to Site B, sent only to Site B's host, its www. twin and their subdomains. At most 8192 characters."
    },
    "urls": {
      "type": "array",
      "minItems": 1,
      "maxItems": 50000,
      "description": "Explicit page pairs to compare (at most 50000); skips discovery and is priced per pair. One pair is a single-page comparison and needs no sourceUrl.",
      "items": {
        "type": "object",
        "required": [
          "source",
          "target"
        ],
        "properties": {
          "source": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Page URL on Site A."
          },
          "target": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The same page's URL on Site B."
          }
        }
      }
    },
    "maxPages": {
      "type": "integer",
      "minimum": 1,
      "description": "Optional ceiling on the pages compared and charged for. The job is priced on the pages it compares: the urls list, or the pages discovery finds, at most this many. There is no page limit. maxPages: 1 without sitemapUrl compares exactly the given sourceUrl and targetUrl, with no discovery."
    },
    "threshold": {
      "type": "number",
      "minimum": 0,
      "maximum": 1,
      "description": "Visual diff tolerance as a fraction from 0 (exact) to 1: 0.05 means 5%. The web app takes a percentage; the API does not."
    },
    "sitemapUrl": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048
    },
    "viewportWidth": {
      "type": "integer",
      "minimum": 320,
      "maximum": 2560
    },
    "checkMode": {
      "type": "string",
      "enum": [
        "full",
        "text-only",
        "existence-only"
      ],
      "description": "What each page is checked on: full (existence, content and visual, the default), text-only (existence and content, no screenshots) or existence-only (only that every page exists and answers, as for a site translated into another language; content and visual are reported as skipped with reason not-requested)."
    },
    "maxPriceUsd": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "Optional spend cap in USD. A job priced above it is refused before any challenge, so an auto-paying client is never asked for more."
    },
    "dryRun": {
      "type": "boolean",
      "description": "Quote only: runs every check and the pricing, then answers the page count and exact amount. Nothing is charged, the free allowance is not used and no job starts; the same body without dryRun is priced from the retained result."
    },
    "callbackUrl": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048,
      "description": "Optional https URL (port 443) that receives one JSON POST when the job ends (jobId, status, summary, links; never the jobToken), a nudge to read the job. Public hosts only, no redirects followed, retried twice; delivery never affects the job."
    },
    "visual": {
      "$ref": "#/$defs/VisualRules"
    }
  },
  "$defs": {
    "BasicAuth": {
      "type": "object",
      "required": [
        "username",
        "password"
      ],
      "properties": {
        "username": {
          "type": "string",
          "description": "Username for HTTP basic auth."
        },
        "password": {
          "type": "string",
          "description": "Password for HTTP basic auth."
        }
      }
    },
    "VisualRules": {
      "type": "object",
      "description": "Capture rules applied to both sides.",
      "additionalProperties": false,
      "properties": {
        "masks": {
          "type": "array",
          "maxItems": 50,
          "description": "CSS selectors of elements hidden on both sites before the screenshot (at most 50, each up to 200 characters). The layout keeps its place.",
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          }
        },
        "freeze": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "animations": {
              "type": "boolean",
              "description": "Stop CSS animations and transitions before the screenshot."
            },
            "clock": {
              "type": "boolean",
              "description": "Pause timers and the clock after load; a page that needs a timer to render may stay empty."
            },
            "clockSeconds": {
              "type": "integer"
            }
          }
        }
      }
    }
  }
}