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

# Scan a transaction

`POST /v1/transaction/scan`

## Request

- Base URL: `https://api.blockaid.io`
- URL: `https://api.blockaid.io/v1/transaction/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/transaction/scan/post.ts> (a function with typed inputs; its response is checked against the schema)

Replaces the per-chain `/v0/{chain}/transaction/scan` routes.

A transaction is an event, not an entity. `proximity` on a returned category describes the **cited participant** — carried in `signals[].evidence` — not the transaction itself.

Request and response payloads are discriminated on `format`. v1 supports the two exchange formats; further formats are added as new members of the union.

## Request body

- union: Discriminated on `format`. Two formats are supported: `EXCHANGE_WITHDRAWAL` and `EXCHANGE_DEPOSIT`.
  - V1ExchangeWithdrawalScanRequest
    - `format` 'EXCHANGE_WITHDRAWAL', required: Identifies this payload as an exchange withdrawal. This format uses `withdrawing_account` (the exchange-side account) and `to` (the external destination address).
    - `chain` string, required: Chain identifier. Open string rather than a closed enum, because supported chains differ per surface and expand without a major version. See the per-surface support matrix in the docs.
    - `withdrawing_account` string, required: Account ID of the exchange-side account the funds are leaving. Distinct from `metadata.account.account_id`, which is end-user profile context — this is a leg of the operation itself.
    - `to` string, required: External destination address. The cited participant for most findings.
    - `asset` string, nullable: Asset identifier or contract address, where the chain distinguishes assets.
    - `amount` string, nullable: Amount in the asset's smallest unit, as a decimal string. String rather than number to avoid precision loss on 256-bit values.
    - `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.
  - V1ExchangeDepositScanRequest
    - `format` 'EXCHANGE_DEPOSIT', required: Identifies this payload as an exchange deposit. This format uses `depositor_address` (the external origin address) and `to` (the exchange-side account).
    - `chain` string, required: Chain identifier. Open string rather than a closed enum, because supported chains differ per surface and expand without a major version. See the per-surface support matrix in the docs.
    - `depositor_address` string, required: External origin address — the address of the depositor sending funds in, not to be confused with where the deposit lands. The cited participant for most findings.
    - `to` string, required: Account ID of the exchange-side account being credited. Distinct from `metadata.account.account_id`, which is end-user profile context — this is a leg of the operation itself.
    - `asset` string, nullable: Asset identifier or contract address, where the chain distinguishes assets.
    - `amount` string, nullable: Amount in the asset's smallest unit, as a decimal string. String rather than number to avoid precision loss on 256-bit values.
    - `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

- V1TransactionScanResponse
  - `request_id` string, required
  - `metadata` V1TransactionMetadata, 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` union, required: Echoes the request payload, discriminated on `format`. `entity_type` stays `TRANSACTION` for every member — the payload format is not an entity type.
      - V1ExchangeWithdrawalDetails: Funds leaving a custodial platform to an external address. `withdrawing_account` is the exchange-side leg, `to` the onchain leg; the risk subject is the destination.
        - `format` 'EXCHANGE_WITHDRAWAL', required: Identifies this payload as an exchange withdrawal. This format uses `withdrawing_account` (the exchange-side account) and `to` (the external destination address).
        - `chain` string, required: Chain identifier. Open string rather than a closed enum, because supported chains differ per surface and expand without a major version. See the per-surface support matrix in the docs.
        - `withdrawing_account` string, required: Account ID of the exchange-side account the funds are leaving. Distinct from `metadata.account.account_id`, which is end-user profile context — this is a leg of the operation itself.
        - `to` string, required: External destination address. The cited participant for most findings.
        - `asset` string, nullable: Asset identifier or contract address, where the chain distinguishes assets.
        - `amount` string, nullable: Amount in the asset's smallest unit, as a decimal string. String rather than number to avoid precision loss on 256-bit values.
      - V1ExchangeDepositDetails: Funds arriving at a custodial platform from an external address. `depositor_address` is the onchain leg, `to` the exchange-side leg; the risk subject is the origin.
        - `format` 'EXCHANGE_DEPOSIT', required: Identifies this payload as an exchange deposit. This format uses `depositor_address` (the external origin address) and `to` (the exchange-side account).
        - `chain` string, required: Chain identifier. Open string rather than a closed enum, because supported chains differ per surface and expand without a major version. See the per-surface support matrix in the docs.
        - `depositor_address` string, required: External origin address — the address of the depositor sending funds in, not to be confused with where the deposit lands. The cited participant for most findings.
        - `to` string, required: Account ID of the exchange-side account being credited. Distinct from `metadata.account.account_id`, which is end-user profile context — this is a leg of the operation itself.
        - `asset` string, nullable: Asset identifier or contract address, where the chain distinguishes assets.
        - `amount` string, nullable: Amount in the asset's smallest unit, as a decimal string. String rather than number to avoid precision loss on 256-bit values.
  - `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`: 6 breaking, 2 warning, 2 info
  - added the new required request property `oneOf[subschema #1: Exchange withdrawal]/withdrawing_account`
  - added the new required request property `oneOf[subschema #2: Exchange deposit]/depositor_address`
  - removed the required property `metadata/details/oneOf[subschema #1: Exchange withdrawal]/from` from the response with the `200` status
  - removed the required property `metadata/details/oneOf[subschema #2: Exchange deposit]/from` from the response with the `200` status
  - …6 more
- **2026-10-29** `53277726e822`: 8 breaking, 7 warning, 5 info
  - added the new required request property `oneOf[subschema #1: Exchange withdrawal]/from`
  - added the new required request property `oneOf[subschema #1: Exchange withdrawal]/to`
  - added the new required request property `oneOf[subschema #2: Exchange deposit]/from`
  - added the new required request property `oneOf[subschema #2: Exchange deposit]/to`
  - …16 more
- …earlier changes not shown

[Operation history](https://skmtc.dev/blockaid-official/apis/blockaid-api/changes/v1/transaction/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)
