---
title: "Create Scan"
method: POST
path: "/api/v1/scans"
tags: ["scans"]
---

# Create Scan

`POST /api/v1/scans`

## Request

- Base URL: `https://app.kaminari.ad`
- URL: `https://app.kaminari.ad/api/v1/scans`
- Auth: HTTP bearer

Create a new scan for a single URL + country.

## Request body

- CreateScanRequest: Request body to create a single scan.
  - `url` string, uri, nullable
  - `ad_tag` string, nullable: Ad tag to check: an HTML snippet OR an http(s) URL of a page with the creative already rendered. Mutually exclusive with url/vast_tag.
  - `vast_tag` string, nullable: VAST video tag to check: an http(s) URL of a VAST endpoint OR raw VAST XML. Mutually exclusive with url/ad_tag.
  - `referrer` string, uri, nullable: Optional http(s) page URL the check is performed from. With ad_tag or vast_tag it is the page the tag is embedded in: the browser commits the harness document on this URL without fetching the publisher, so the creative is embedded exactly as it would be on that page. With url or ad_discovery it is where the visitor came from and travels as the Referer of the page request. Cross-origin subrequests receive the origin only (https://publisher.example/, no path) under Chromium's default strict-origin-when-cross-origin policy — same as on a real publisher. Must be a publicly routable host, carry no credentials, and stay within 2048 characters once normalised.
  - `ignore_first_n_domains` integer: Skips detection and tagging on the first N unique domains of the redirect chain, counted from the entry point. Use it to exclude your own click/tracking domains so they don't trigger tags or alerts. Domains fold to their registrable form, so www.example.com and example.com count as one. Any other request to one of those domains is excluded too, wherever in the page it was made from. Default 0 (nothing skipped). Maximum 5.
  - `emulator_id` string, required
  - `proxy` ProxyTargetRequest: Proxy targeting parameters for a scan.
    - `proxy_type` string
    - `region` string
    - `city` string
    - `isp` string
  - `labels` object
  - `campaign_id` string, uuid, nullable: Campaign to attribute the scan to. Alerts are raised only for scans that carry a campaign whose policy set has rules.
  - `run_id` string, uuid, nullable
  - `ad_discovery` boolean: Publisher ad discovery: detect ad blocks on the page and spawn one child scan per detected ad. Only valid with url (not ad_tag/vast_tag).
  - `max_discovered_ads` integer, nullable: How many ad blocks to look for on the publisher page before stopping. Each detected ad becomes its own scan with its own report, and is billed as a separate check. Only valid together with ad_discovery. Omit to use the platform default. Maximum 25.
  - `ad_formats` AdKind[], nullable: Which ad formats the discovery run captures: banner, video and/or pop (pop covers both pop-unders and tab-unders), each at most once. A format left out is neither captured nor billed — e.g. ['pop'] checks the page's pops without screenshotting or clicking its banners; a tab-under the page forces on its own is still reported as a pop. Only valid together with ad_discovery. Omit to capture every format.
  - `repeat_count` integer: How many times to scan this combination. Each repeat is a full scan with its own report and its own billing.
  - `repeat_mode` 'isolated' | 'shared': Browser-session isolation mode for a group of repeated scans. ``ISOLATED`` is the historical (and only) crawler behaviour: every scan gets its own Chromium, its own context and its own proxy session, so every repeat sees a new IP and an empty cookie jar. ``SHARED`` makes the crawler run the whole group inside one browser session — one proxy session (one IP) and ``storage_state`` carried from one repeat to the next. Only groups in this mode carry a ``Scan.repeat_session_id``.
  - `retry_max_attempts` integer: Extra crawl attempts after a technical failure (dead proxy, timeout, browser crash). The same scan is reused, so a retry is never billed twice.
  - `country_code` string, required: ISO 3166-1 alpha-2 country code, case-insensitive; stored uppercase.

## Response `201`

Successful Response

- ScanResponse: Full scan detail returned by GET /scans/:id.
  - `id` string, uuid, required
  - `url` string, required
  - `country_code` string, required
  - `emulator_id` string, required
  - `status` 'pending' | 'running' | 'crawled' | 'checking' | 'checking_async' | 'rechecking' | 'completed' | 'partial' | 'failed' | 'cancelled', required: Status of a scan through its lifecycle. Ordered: pending(0) → running(1) → crawled(2) → checking(3) → checking_async(4) → terminal(5). Forward-only transitions enforced by advance_status() in the DB (see status_order() Postgres function). ``CHECKING_ASYNC`` means all inline parts (regexp + domain checks) have been applied and we are waiting only on async parts (LLM batch today, SafeBrowsing / VirusTotal tomorrow). UI can show partial tags + alerts at this stage; only classification is still pending. ``RECHECKING`` is a **transient** state used **only** by the recheck flow. The recheck use case atomically moves a scan from a terminal state into ``RECHECKING``, wipes prior tags (CH tombstones), wipes scan_part_state, then atomically transitions to ``CHECKING`` with fresh ``expected_parts``. Both transitions bypass the forward-only ``advance_status()`` guard via dedicated repository methods. Why RECHECKING exists: stale ``check.parts.*`` messages for a now- rechecking scan must NOT write tags (those would survive the wipe without tombstones, corrupting the post-recheck tag set). Consumers check ``WRITE_BLOCKED_STATUSES`` and skip the message when the scan is in ``RECHECKING``. Defence in depth — the TagWriterAdapter's UoW also re-reads the scan status before save.
  - `parent_scan_id` string, uuid, nullable
  - `ad_discovery` boolean
  - `max_discovered_ads` integer, nullable
  - `ad_formats` AdKind[], nullable
  - `slot_index` integer, nullable
  - `ad_kind` string, nullable
  - `network` string
  - `network_host` string
  - `offer_url` string, required
  - `redirect_chain` RedirectHopResponse[], required
    - `url` string, required
    - `status_code` integer, required
    - `content_type` string, required
    - `body_size` integer, required
    - `timestamp_ms` integer, required
    - `redirected_from` string, required
    - `sub_requests` SubRequestResponse[], required
      - `url` string, required
      - `resource_type` string, required
      - `status_code` integer, required
      - `content_type` string, required
      - `body_size` integer, required
      - `timestamp_ms` integer, required
      - `children` SubRequestResponse[]
  - `screenshot_url` string
  - `report_url` string
  - `public_report_url` string
  - `ad_tag` string, nullable
  - `vast_tag` string, nullable
  - `referrer` string, nullable
  - `ignore_first_n_domains` integer
  - `creative_kind` 'banner' | 'video'
  - `creative_screenshot_url` string
  - `creative_video_url` string
  - `vast_xml_url` string
  - `creative_html_url` string
  - `creative_width` integer
  - `creative_height` integer
  - `creative_redirect_chain` RedirectHopResponse[]
    - `url` string, required
    - `status_code` integer, required
    - `content_type` string, required
    - `body_size` integer, required
    - `timestamp_ms` integer, required
    - `redirected_from` string, required
    - `sub_requests` SubRequestResponse[], required
      - `url` string, required
      - `resource_type` string, required
      - `status_code` integer, required
      - `content_type` string, required
      - `body_size` integer, required
      - `timestamp_ms` integer, required
      - `children` SubRequestResponse[]
  - `video` VideoMetaResponse: Slim VAST video metadata surfaced on the scan report. Present only for VAST scans. The rendered video frame is served via the existing creative-screenshot URL; this block carries the video-specific facts the report shows alongside it.
    - `duration_ms` integer
    - `mediafile_url` string
    - `vast_version` string
    - `ad_system` string
    - `is_vpaid` boolean
    - `wrapper_depth` integer
    - `click_through` string
  - `proxy` ProxyTargetResponse: Proxy targeting info in a scan response.
    - `proxy_type` string, required
    - `region` string
    - `city` string
    - `isp` string
  - `page_title` string, required
  - `elapsed_ms` integer, required
  - `error` string, required
  - `labels` object
  - `classification` ScanClassificationResponse: Classification result for a scan — AI category + IAB V3 + brand + custom taxonomies.
    - `brand` string, nullable
    - `ai_category` AiCategoryResponse: Freeform AI category tiers (legacy ``iab_v3`` semantics).
      - `tier1` string, required
      - `tier2` string, nullable
      - `tier3` string, nullable
      - `tier4` string, nullable
    - `iab_v3` IabV3CategoryResponse: Canonical IAB Content Taxonomy V3 category tiers (validated).
      - `tier1` string, required
      - `tier2` string, nullable
      - `tier3` string, nullable
      - `tier4` string, nullable
    - `custom_taxonomies` ScanTaxonomyClassificationResponse[]
      - `taxonomy_id` string, uuid, required
      - `taxonomy_name` string, required
      - `taxonomy_slug` string, required
      - `taxonomy_version` integer, required
      - `leaf_node_id` string, uuid, nullable, required
      - `tier1` string, nullable, required
      - `tier2` string, nullable
      - `tier3` string, nullable
      - `tier4` string, nullable
      - `used_default` boolean
  - `campaign_id` string, uuid, nullable
  - `campaign_name` string, nullable
  - `campaign_group_id` string, uuid, nullable
  - `campaign_group_name` string, nullable
  - `created_at` string, date-time, required
  - `completed_at` string, date-time, nullable, required
  - `landings` LandingResponse[]
    - `ord` integer, required
    - `opener_url` string
    - `final_url` string
    - `offer_url` string
    - `page_title` string
    - `screenshot_url` string
    - `redirect_chain` RedirectHopResponse[]
      - `url` string, required
      - `status_code` integer, required
      - `content_type` string, required
      - `body_size` integer, required
      - `timestamp_ms` integer, required
      - `redirected_from` string, required
      - `sub_requests` SubRequestResponse[], required
        - `url` string, required
        - `resource_type` string, required
        - `status_code` integer, required
        - `content_type` string, required
        - `body_size` integer, required
        - `timestamp_ms` integer, required
        - `children` SubRequestResponse[]
    - `elapsed_ms` integer
    - `created_at` string, date-time, required
  - `repeat_index` integer
  - `repeat_total` integer
  - `repeat_session_id` string, uuid, nullable
  - `repeat_scan_ids` string[]: 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` integer
  - `retry_max_attempts` integer

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/kaminari/apis/kaminari-ad-api.md) · [All operations](https://skmtc.dev/kaminari/apis/kaminari-ad-api/llms.txt) · [OpenAPI document](https://skmtc.dev/kaminari/apis/kaminari-ad-api/revisions/013f86e2fd32?raw)
