---
title: "Scan a website"
method: POST
path: "/v1/website/scan"
tags: ["Scan v1"]
---

# Scan a website

`POST /v1/website/scan`

## Request

- Base URL: `https://api.blockaid.io`
- URL: `https://api.blockaid.io/v1/website/scan`
- Auth: one of:
  - API key in header `X-API-Key`
  - API key in header `X-CLIENT-ID`
- TypeScript: <https://skmtc.dev/blockaid-official/apis/blockaid-api/docs/v1/website/scan/post.ts> (a function with typed inputs; its response is checked against the schema)

Replaces `/v0/site/scan`. The v0 `anyOf` hit/miss split is gone — a site with no findings is a 200 with an empty `negative_categories`.

## Request body

- V1WebsiteScanRequest
  - `url` string, uri, required: The website URL to scan.
  - `metadata` V1ScanRequestMetadata: Request context, carried forward from v0 `ScanRequestMetadata`. The v0 `domain` and `non_dapp` fields are dropped in v1 — both are superseded by `connection.origin`.
    - `account` V1Account
      - `account_id` string, required: Unique identifier for the account.
      - `account_creation_timestamp` string, date-time: Timestamp when the account was created.
      - `user_age` integer: Age of the user in years.
      - `user_country_code` string: ISO country code of the user's location.
      - `account_addresses` string[]: List of all account addresses in different chains based on the CAIPs standard (https://github.com/ChainAgnostic/CAIPs). Ethereum mainnet example: eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb
    - `connection` V1Connection
      - `user_agent` string: User agent string from the client's browser or application.
      - `ip_address` string, ipvanyaddress, required: IP address of the customer making the request. Both IPv4 and IPv6 addresses are supported.
      - `origin` string, uri: The full URL of the website that initiated the request — the same direction as the HTTP `Origin` header.
      - `walletconnect_name` string: WalletConnect session name, when the request originates from a WalletConnect session.
      - `walletconnect_description` string: WalletConnect session description, when the request originates from a WalletConnect session.

## Response `200`

Scan completed

- V1WebsiteScanResponse
  - `request_id` string, required
  - `metadata` V1WebsiteMetadata, required
    - `entity_type` 'TOKEN' | 'ADDRESS' | 'TRANSACTION' | 'SIGNATURE' | 'WEBSITE', required: Closed enum. Clients must branch exhaustively. `SIGNATURE` is reserved and has no endpoint in v1.
    - `details` V1WebsiteMetadataDetails, required
      - `url` string, uri, required: The scanned website URL.
  - `risk` V1Risk, required: Three states, and only three: - **Unknown** — both arrays empty. Nothing was found under the active profile and nothing vouches for the entity. `severity` is `{ low, 10 }`. This is the common case, not an error. - **Risk found** — `negative_categories` populated, `positive_categories` empty. - **Trusted** — `positive_categories` populated, `negative_categories` empty. `severity` is `{ none, 0 }`. Both arrays populated at once does not occur. An empty `negative_categories` never means the entity is safe.
    - `severity` V1Severity, required: The verdict. `level` is bucketed from `score`; the two always agree, so a client may branch on either.
      - `level` 'none' | 'low' | 'medium' | 'high' | 'critical', required: Risk severity level. Closed enum, bucketed from `risk.score`. Replaces the three inconsistent v0 `ValidationResultType` enums. `none` = 0, requires a populated `positive_categories`. `low` = 10–30, and covers two states: unknown (score exactly 10, `negative_categories` empty) and a real but minor finding (`negative_categories` populated). `medium` = 31–60. `high` = 61–89. `critical` = 90–100. Nothing lands between 1 and 9. v0's `Error` result type has no equivalent — failures are now the error envelope.
      - `score` integer, required: Note the type change from v0, where `malicious_score` was a string. 0 when `positive_categories` is populated; otherwise `max(10, max(category scores))`. The unknown baseline of 10 is a floor findings push up from, so a weak detection never scores below an entity nothing is known about. Not verifiable by customers: its inputs are internal-only.
    - `profile` V1Profile, required: Resolved from the credential on the request (`X-API-Key` or `X-CLIENT-ID`), which resolve a profile identically. An object rather than a bare string so fields can land without a breaking change. Custom profile ids are not in v1 — every id returned is one of the managed set.
      - `id` string, required
    - `negative_categories` V1NegativeCategory[], required: Security, fraud and compliance findings in one array, so `negative_categories.length === 0` is a safe check. Splitting compliance out would let a sanctions hit slip past the most common integrator branch.
      - `category` string, required: Flat value from the register — replaces v0's `category` + `sub_category` pair and the `interaction_with_*` compound names, whose directness now lives in `proximity`. Open string. To route findings to an owning team, map `category` client-side; the register carries that mapping and it does not vary per scan.
      - `proximity` 'ownership' | 'counterparty' | 'indirect': Closed enum. How far the scanned entity stands from the risk. Same vocabulary as v0 `Proximity`, minus `overall` — that value is a request-side filter on `/v0/address/risk-exposure`, not a response value. `ownership` — it is the thing, or operates it. `counterparty` — direct interaction. `indirect` — traced value, one or more hops removed. `indirect` collapses every depth beyond one hop. Where hop count matters it rides in `signals[].evidence` as a display value and is not branchable. On a transaction this describes the **cited participant**, not the transaction itself. Returned on the `risk_exposure` profile only — see `risk.profile`. Under any other profile the field is absent.
      - `name` string, required: Display name for the category.
      - `description` string, required: Short display text, composed server-side and keyed on (`category`, `proximity`). **Print verbatim.** This is the only field carrying the attacker/victim distinction: at `proximity: "ownership"` a drainer and the wallet it emptied return the same category at the same level.
      - `signals` V1Signal[], required: The signals that produced this category. May be empty.
        - `id` string, required: Stable namespaced key. Open string.
        - `name` string, required: End-user-facing title. Static per signal id.
        - `description` string, required: End-user-facing explanation. Static per signal id; the per-scan specifics are in `evidence`.
        - `evidence` V1Evidence, required: The per-scan specifics this signal cited, as `{ key: value }`. Always present, may be empty. Keys are drawn from a registered vocabulary with a declared type, so the platform renders by type rather than per signal id. A key means the same thing and carries the same type wherever it appears. Keys are additive and never break existing clients. Registered types: `address`, `address[]`, `tx_hash`, `timestamp`, `percentage`, `number`, `label`, `boolean`, `url`. Evidence is not a scoring input and must not be branched on — keys are open strings.
    - `positive_categories` V1PositiveCategory[], required
      - `category` string, required: Flat value from the register. Open string, same vocabulary as `V1NegativeCategory.category`.
      - `name` string, required: Display name for the category.
      - `description` string, required: Short display text for the category.
      - `signals` V1Signal[], required
        - `id` string, required: Stable namespaced key. Open string.
        - `name` string, required: End-user-facing title. Static per signal id.
        - `description` string, required: End-user-facing explanation. Static per signal id; the per-scan specifics are in `evidence`.
        - `evidence` V1Evidence, required: The per-scan specifics this signal cited, as `{ key: value }`. Always present, may be empty. Keys are drawn from a registered vocabulary with a declared type, so the platform renders by type rather than per signal id. A key means the same thing and carries the same type wherever it appears. Keys are additive and never break existing clients. Registered types: `address`, `address[]`, `tx_hash`, `timestamp`, `percentage`, `number`, `label`, `boolean`, `url`. Evidence is not a scoring input and must not be branched on — keys are open strings.

## Other responses

- `400`: `MALFORMED_REQUEST` — body is not parseable: truncated payload, wrong Content-Type, invalid JSON. Not retryable.
- `401`: `UNAUTHENTICATED` — missing, malformed, or revoked credential. Not retryable.
- `403`: `FORBIDDEN` — key is valid but not entitled to the requested surface, chain, or profile. Not retryable.
- `422`: `VALIDATION_FAILED` — parses cleanly, violates the schema. Carries `error.details[]`. Not retryable.
- `429`: `RATE_LIMITED` — over quota. Retryable; honour `Retry-After`.
- `500`: `INTERNAL_ERROR` — unhandled failure inside the scan path. Retryable with backoff.
- `502`: `UPSTREAM_FAILED` — a dependency required to complete the scan returned an error. Retryable with backoff.
- `504`: `UPSTREAM_TIMEOUT` — the scan did not complete within the time limit. Retryable with backoff.

## Changes

> 120 revisions in range; 1 not diffed.

- **2026-10-29** `6a9a1f3974d3`: 2 breaking
  - removed the required property `metadata/id` from the response with the `200` status
  - removed the required property `policy` from the response with the `200` status
- **2026-10-29** `53277726e822`: 3 warning, 1 info
  - removed the optional property `metadata/first_scanned` from the response with the `200` status
  - removed the optional property `metadata/last_updated` from the response with the `200` status
  - added the new `approve` enum value to the `policy/action/anyOf[subschema #1]/` response property for the response status `200`
  - removed the `allow` enum value from the `policy/action/anyOf[subschema #1]/` response property for the response status `200`
- **2026-10-08** `cec32ae8b816`: 1 info
  - endpoint added
- **2026-10-08** `5f40f1064bf0`: 1 breaking
  - api path removed without deprecation
- **2026-10-08** `7742f7c5af0e`: 3 warning, 1 info
  - removed the optional property `metadata/first_scanned` from the response with the `200` status
  - removed the optional property `metadata/last_updated` from the response with the `200` status
  - added the new `approve` enum value to the `policy/action/anyOf[subschema #1]/` response property for the response status `200`
  - removed the `allow` enum value from the `policy/action/anyOf[subschema #1]/` response property for the response status `200`

[Operation history](https://skmtc.dev/blockaid-official/apis/blockaid-api/changes/v1/website/scan/post.md)

---

[API](https://skmtc.dev/blockaid-official/apis/blockaid-api.md) - [All operations](https://skmtc.dev/blockaid-official/apis/blockaid-api/llms.txt) - [OpenAPI document](https://skmtc.dev/blockaid-official/apis/blockaid-api/revisions/104550c9d3f1?raw)
