{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/kaminari-ad/json-schema/kaminari-ad-scan-response-schema.json",
  "title": "ScanResponse",
  "description": "Full scan detail returned by GET /scans/:id.",
  "x-generated": "2026-11-13",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/kaminari-ad-openapi.json#/components/schemas/ScanResponse",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "title": "Id"
    },
    "url": {
      "type": "string",
      "title": "Url"
    },
    "country_code": {
      "type": "string",
      "title": "Country Code"
    },
    "emulator_id": {
      "type": "string",
      "title": "Emulator Id"
    },
    "status": {
      "$ref": "#/$defs/ScanStatus"
    },
    "parent_scan_id": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ],
      "title": "Parent Scan Id"
    },
    "ad_discovery": {
      "type": "boolean",
      "title": "Ad Discovery",
      "default": false
    },
    "max_discovered_ads": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "title": "Max Discovered Ads"
    },
    "ad_formats": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/AdKind"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "title": "Ad Formats"
    },
    "slot_index": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "title": "Slot Index"
    },
    "ad_kind": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Ad Kind"
    },
    "network": {
      "type": "string",
      "title": "Network",
      "default": ""
    },
    "network_host": {
      "type": "string",
      "title": "Network Host",
      "default": ""
    },
    "offer_url": {
      "type": "string",
      "title": "Offer Url"
    },
    "redirect_chain": {
      "items": {
        "$ref": "#/$defs/RedirectHopResponse"
      },
      "type": "array",
      "title": "Redirect Chain"
    },
    "screenshot_url": {
      "type": "string",
      "title": "Screenshot Url",
      "default": ""
    },
    "report_url": {
      "type": "string",
      "title": "Report Url",
      "default": ""
    },
    "public_report_url": {
      "type": "string",
      "title": "Public Report Url",
      "default": ""
    },
    "ad_tag": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Ad Tag"
    },
    "vast_tag": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Vast Tag"
    },
    "referrer": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Referrer"
    },
    "ignore_first_n_domains": {
      "type": "integer",
      "title": "Ignore First N Domains",
      "default": 0
    },
    "creative_kind": {
      "type": "string",
      "enum": [
        "banner",
        "video"
      ],
      "title": "Creative Kind",
      "default": "banner"
    },
    "creative_screenshot_url": {
      "type": "string",
      "title": "Creative Screenshot Url",
      "default": ""
    },
    "creative_video_url": {
      "type": "string",
      "title": "Creative Video Url",
      "default": ""
    },
    "vast_xml_url": {
      "type": "string",
      "title": "Vast Xml Url",
      "default": ""
    },
    "creative_html_url": {
      "type": "string",
      "title": "Creative Html Url",
      "default": ""
    },
    "creative_width": {
      "type": "integer",
      "title": "Creative Width",
      "default": 0
    },
    "creative_height": {
      "type": "integer",
      "title": "Creative Height",
      "default": 0
    },
    "creative_redirect_chain": {
      "items": {
        "$ref": "#/$defs/RedirectHopResponse"
      },
      "type": "array",
      "title": "Creative Redirect Chain"
    },
    "video": {
      "anyOf": [
        {
          "$ref": "#/$defs/VideoMetaResponse"
        },
        {
          "type": "null"
        }
      ]
    },
    "proxy": {
      "anyOf": [
        {
          "$ref": "#/$defs/ProxyTargetResponse"
        },
        {
          "type": "null"
        }
      ]
    },
    "page_title": {
      "type": "string",
      "title": "Page Title"
    },
    "elapsed_ms": {
      "type": "integer",
      "title": "Elapsed Ms"
    },
    "error": {
      "type": "string",
      "title": "Error"
    },
    "labels": {
      "additionalProperties": {
        "type": "string"
      },
      "type": "object",
      "title": "Labels"
    },
    "classification": {
      "anyOf": [
        {
          "$ref": "#/$defs/ScanClassificationResponse"
        },
        {
          "type": "null"
        }
      ]
    },
    "campaign_id": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ],
      "title": "Campaign Id"
    },
    "campaign_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Campaign Name"
    },
    "campaign_group_id": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ],
      "title": "Campaign Group Id"
    },
    "campaign_group_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Campaign Group Name"
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "title": "Created At"
    },
    "completed_at": {
      "anyOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "type": "null"
        }
      ],
      "title": "Completed At"
    },
    "landings": {
      "items": {
        "$ref": "#/$defs/LandingResponse"
      },
      "type": "array",
      "title": "Landings"
    },
    "repeat_index": {
      "type": "integer",
      "title": "Repeat Index",
      "default": 0
    },
    "repeat_total": {
      "type": "integer",
      "title": "Repeat Total",
      "default": 1
    },
    "repeat_session_id": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ],
      "title": "Repeat Session Id"
    },
    "repeat_scan_ids": {
      "items": {
        "type": "string",
        "format": "uuid"
      },
      "type": "array",
      "title": "Repeat Scan Ids",
      "description": "The sibling scans this request also created, excluding this one. Populated only by the create endpoints — list and detail responses leave it empty."
    },
    "retry_attempt": {
      "type": "integer",
      "title": "Retry Attempt",
      "default": 0
    },
    "retry_max_attempts": {
      "type": "integer",
      "title": "Retry Max Attempts",
      "default": 0
    }
  },
  "type": "object",
  "required": [
    "id",
    "url",
    "country_code",
    "emulator_id",
    "status",
    "offer_url",
    "redirect_chain",
    "page_title",
    "elapsed_ms",
    "error",
    "created_at",
    "completed_at"
  ],
  "$defs": {
    "AdKind": {
      "type": "string",
      "enum": [
        "banner",
        "pop",
        "video"
      ],
      "title": "AdKind",
      "description": "Closed vocabulary for a discovered ad block.\n\n``banner`` and ``video`` carry a creative (screenshot/HTML) plus an\noptional click-through landing — ``video`` is a VAST creative playing\nin a video element (in-player or outstream); ``pop`` is a behaviour\n(pop-under or tab-under) with a landing but no creative."
    },
    "AiCategoryResponse": {
      "properties": {
        "tier1": {
          "type": "string",
          "title": "Tier1"
        },
        "tier2": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier2"
        },
        "tier3": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier3"
        },
        "tier4": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier4"
        }
      },
      "type": "object",
      "required": [
        "tier1"
      ],
      "title": "AiCategoryResponse",
      "description": "Freeform AI category tiers (legacy ``iab_v3`` semantics)."
    },
    "IabV3CategoryResponse": {
      "properties": {
        "tier1": {
          "type": "string",
          "title": "Tier1"
        },
        "tier2": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier2"
        },
        "tier3": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier3"
        },
        "tier4": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier4"
        }
      },
      "type": "object",
      "required": [
        "tier1"
      ],
      "title": "IabV3CategoryResponse",
      "description": "Canonical IAB Content Taxonomy V3 category tiers (validated)."
    },
    "LandingResponse": {
      "properties": {
        "ord": {
          "type": "integer",
          "title": "Ord"
        },
        "opener_url": {
          "type": "string",
          "title": "Opener Url",
          "default": ""
        },
        "final_url": {
          "type": "string",
          "title": "Final Url",
          "default": ""
        },
        "offer_url": {
          "type": "string",
          "title": "Offer Url",
          "default": ""
        },
        "page_title": {
          "type": "string",
          "title": "Page Title",
          "default": ""
        },
        "screenshot_url": {
          "type": "string",
          "title": "Screenshot Url",
          "default": ""
        },
        "redirect_chain": {
          "items": {
            "$ref": "#/$defs/RedirectHopResponse"
          },
          "type": "array",
          "title": "Redirect Chain"
        },
        "elapsed_ms": {
          "type": "integer",
          "title": "Elapsed Ms",
          "default": 0
        },
        "created_at": {
          "type": "string",
          "format": "date-time",
          "title": "Created At"
        }
      },
      "type": "object",
      "required": [
        "ord",
        "created_at"
      ],
      "title": "LandingResponse",
      "description": "One landing tab of an ad-tag scan, exposed via ``ScanResponse.landings``.\n\nMulti-tab landing capture: a single ad creative click can open\nmultiple browser tabs (offer + tracking pixels + secondary popups);\neach becomes its own ``LandingResponse``. ``ord=0`` is the primary\nlanding whose URL/screenshot are also mirrored into the legacy\nscalar fields on ``ScanResponse`` for backward compatibility."
    },
    "ProxyTargetResponse": {
      "properties": {
        "proxy_type": {
          "type": "string",
          "title": "Proxy Type"
        },
        "region": {
          "type": "string",
          "title": "Region",
          "default": ""
        },
        "city": {
          "type": "string",
          "title": "City",
          "default": ""
        },
        "isp": {
          "type": "string",
          "title": "Isp",
          "default": ""
        }
      },
      "type": "object",
      "required": [
        "proxy_type"
      ],
      "title": "ProxyTargetResponse",
      "description": "Proxy targeting info in a scan response."
    },
    "RedirectHopResponse": {
      "properties": {
        "url": {
          "type": "string",
          "title": "Url"
        },
        "status_code": {
          "type": "integer",
          "title": "Status Code"
        },
        "content_type": {
          "type": "string",
          "title": "Content Type"
        },
        "body_size": {
          "type": "integer",
          "title": "Body Size"
        },
        "timestamp_ms": {
          "type": "integer",
          "title": "Timestamp Ms"
        },
        "redirected_from": {
          "type": "string",
          "title": "Redirected From"
        },
        "sub_requests": {
          "items": {
            "$ref": "#/$defs/SubRequestResponse"
          },
          "type": "array",
          "title": "Sub Requests"
        }
      },
      "type": "object",
      "required": [
        "url",
        "status_code",
        "content_type",
        "body_size",
        "timestamp_ms",
        "redirected_from",
        "sub_requests"
      ],
      "title": "RedirectHopResponse",
      "description": "One main-frame redirect step with nested sub-request tree."
    },
    "ScanClassificationResponse": {
      "properties": {
        "brand": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Brand"
        },
        "ai_category": {
          "anyOf": [
            {
              "$ref": "#/$defs/AiCategoryResponse"
            },
            {
              "type": "null"
            }
          ]
        },
        "iab_v3": {
          "anyOf": [
            {
              "$ref": "#/$defs/IabV3CategoryResponse"
            },
            {
              "type": "null"
            }
          ]
        },
        "custom_taxonomies": {
          "items": {
            "$ref": "#/$defs/ScanTaxonomyClassificationResponse"
          },
          "type": "array",
          "title": "Custom Taxonomies",
          "default": []
        }
      },
      "type": "object",
      "title": "ScanClassificationResponse",
      "description": "Classification result for a scan — AI category + IAB V3 + brand + custom taxonomies."
    },
    "ScanStatus": {
      "type": "string",
      "enum": [
        "pending",
        "running",
        "crawled",
        "checking",
        "checking_async",
        "rechecking",
        "completed",
        "partial",
        "failed",
        "cancelled"
      ],
      "title": "ScanStatus",
      "description": "Status of a scan through its lifecycle.\n\nOrdered: pending(0) → running(1) → crawled(2) → checking(3) →\nchecking_async(4) → terminal(5). Forward-only transitions enforced by\nadvance_status() in the DB (see status_order() Postgres function).\n\n``CHECKING_ASYNC`` means all inline parts (regexp + domain checks) have\nbeen applied and we are waiting only on async parts (LLM batch today,\nSafeBrowsing / VirusTotal tomorrow). UI can show partial tags + alerts\nat this stage; only classification is still pending.\n\n``RECHECKING`` is a **transient** state used **only** by the recheck\nflow. The recheck use case atomically moves a scan from a terminal\nstate into ``RECHECKING``, wipes prior tags (CH tombstones), wipes\nscan_part_state, then atomically transitions to ``CHECKING`` with\nfresh ``expected_parts``. Both transitions bypass the forward-only\n``advance_status()`` guard via dedicated repository methods.\n\nWhy RECHECKING exists: stale ``check.parts.*`` messages for a now-\nrechecking scan must NOT write tags (those would survive the wipe\nwithout tombstones, corrupting the post-recheck tag set). Consumers\ncheck ``WRITE_BLOCKED_STATUSES`` and skip the message when the scan\nis in ``RECHECKING``. Defence in depth — the TagWriterAdapter's UoW\nalso re-reads the scan status before save."
    },
    "ScanTaxonomyClassificationResponse": {
      "properties": {
        "taxonomy_id": {
          "type": "string",
          "format": "uuid",
          "title": "Taxonomy Id"
        },
        "taxonomy_name": {
          "type": "string",
          "title": "Taxonomy Name"
        },
        "taxonomy_slug": {
          "type": "string",
          "title": "Taxonomy Slug"
        },
        "taxonomy_version": {
          "type": "integer",
          "title": "Taxonomy Version"
        },
        "leaf_node_id": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ],
          "title": "Leaf Node Id"
        },
        "tier1": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier1"
        },
        "tier2": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier2"
        },
        "tier3": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier3"
        },
        "tier4": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Tier4"
        },
        "used_default": {
          "type": "boolean",
          "title": "Used Default",
          "default": false
        }
      },
      "type": "object",
      "required": [
        "taxonomy_id",
        "taxonomy_name",
        "taxonomy_slug",
        "taxonomy_version",
        "leaf_node_id",
        "tier1"
      ],
      "title": "ScanTaxonomyClassificationResponse",
      "description": "One per-(scan, custom-taxonomy) classification.\n\nMirrors the ``scan_taxonomy_classifications`` row + adds the\ntaxonomy ``name`` and ``slug`` so the UI can render the block\nwithout a second round-trip. ``leaf_node_id`` is included for\nclick-to-filter integrations on the scan list page; ``used_default``\nis a UI hint that the AI fell back to the default leaf rather\nthan picking a specific category."
    },
    "SubRequestResponse": {
      "properties": {
        "url": {
          "type": "string",
          "title": "Url"
        },
        "resource_type": {
          "type": "string",
          "title": "Resource Type"
        },
        "status_code": {
          "type": "integer",
          "title": "Status Code"
        },
        "content_type": {
          "type": "string",
          "title": "Content Type"
        },
        "body_size": {
          "type": "integer",
          "title": "Body Size"
        },
        "timestamp_ms": {
          "type": "integer",
          "title": "Timestamp Ms"
        },
        "children": {
          "items": {
            "$ref": "#/$defs/SubRequestResponse"
          },
          "type": "array",
          "title": "Children"
        }
      },
      "type": "object",
      "required": [
        "url",
        "resource_type",
        "status_code",
        "content_type",
        "body_size",
        "timestamp_ms"
      ],
      "title": "SubRequestResponse",
      "description": "One sub-request node with recursive children (initiator tree)."
    },
    "VideoMetaResponse": {
      "properties": {
        "duration_ms": {
          "type": "integer",
          "title": "Duration Ms",
          "default": 0
        },
        "mediafile_url": {
          "type": "string",
          "title": "Mediafile Url",
          "default": ""
        },
        "vast_version": {
          "type": "string",
          "title": "Vast Version",
          "default": ""
        },
        "ad_system": {
          "type": "string",
          "title": "Ad System",
          "default": ""
        },
        "is_vpaid": {
          "type": "boolean",
          "title": "Is Vpaid",
          "default": false
        },
        "wrapper_depth": {
          "type": "integer",
          "title": "Wrapper Depth",
          "default": 0
        },
        "click_through": {
          "type": "string",
          "title": "Click Through",
          "default": ""
        }
      },
      "type": "object",
      "title": "VideoMetaResponse",
      "description": "Slim VAST video metadata surfaced on the scan report.\n\nPresent only for VAST scans. The rendered video frame is served via\nthe existing creative-screenshot URL; this block carries the\nvideo-specific facts the report shows alongside it."
    }
  }
}