Integrate ChainAnalyzer via REST API & ScamDB API
REST API requests require a tfk_ prefixed API key. Create one from Dashboard > Settings > API Keys (up to 5 per account; the full key is shown once at creation).
Send via X-API-Key header:
curl -H "X-API-Key: tfk_your_key_here" \ https://chain-analyzer.com/api/v1/public/scanhttps://chain-analyzer.com/api/v1Machine-readable definition of every endpoint (OpenAPI 3.1). Load it straight into Postman, Insomnia, or a client generator.
curl https://chain-analyzer.com/openapi-enterprise.jsonrisk_score runs 0 (safe) to 100 (critical) — higher means riskier. The same convention applies to single scans, batch results, scan history, and webhook payloads. History and batch responses also carry score_semantics: "risk_0to100" so clients can assert it.
Chain identifiers are bitcoin / ethereum / polygon / bsc / base / arbitrum / optimism / avalanche / kaia / solana / tron / xrp. BNB Smart Chain is bsc (bnb returns 400). Omit the hint to auto-detect from the address format.
PATCH is supported (partial updates for webhook configs, Follow Mode, cases, and case addresses).
The production core loop in four steps, from deciding on a deposit to keeping the address under watch. Every example is copy-pasteable curl — swap tfk_your_key_here for your own key.
Use two lanes. On the hot path, GET /public/presign/check returns a verdict immediately; fall through to the full POST /public/scan only when the verdict is not ok. The example is an incoming USDT deposit on TRON (token_contract = TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t).
1. Fast lane — safe to call on every transaction (0 quota, its own rate bucket, ~120s cache)
curl -H "X-API-Key: tfk_your_key_here" \ "https://chain-analyzer.com/api/v1/public/presign/check?chain=tron&to=TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX"{
"level": "low",
"verdict": "ok",
"score": 5,
"reasons": [],
"signals": { "sanctioned": false, "scamdb_hit": false },
"confidence": 0.9,
"chain": "tron",
"to": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"latency_ms": 41,
"cached": false
}verdict collapses to ok / caution / danger and maps straight onto a banner state. level is critical / high / medium / low / unknown (an unsupported chain also returns unknown), and score runs 0-100 with higher meaning riskier.
2. Full scan — only when the verdict is not ok (costs 1 scan)
curl -X POST https://chain-analyzer.com/api/v1/public/scan \ -H "X-API-Key: tfk_your_key_here" -H "Content-Type: application/json" \ -d '{"address":"TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX","chain":"tron"}'{
"success": true,
"address": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"chain": "tron",
"address_type": "wallet",
"risk_level": "LOW",
"risk_score": 8,
"detection_count": 0,
"detections": [],
"metadata": { "is_sanctioned": false, "total_transactions": 1204 },
"ml_anomaly_score": 0.1147,
"scan_duration_ms": 6321,
"cached": false
}Bitcoin addresses with large UTXO sets and very high-frequency EVM accounts can outlast the synchronous HTTP deadline. Take the 202 and its job id, poll status_url until status is completed, then read results_url. It costs the same 1 scan as the synchronous call.
curl -X POST https://chain-analyzer.com/api/v1/public/scan/async \ -H "X-API-Key: tfk_your_key_here" -H "Content-Type: application/json" \ -d '{"address":"347QFbejDBdMZFTxpmn6evvvqyXiqZTCd7","chain":"bitcoin"}'{
"success": true,
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"address": "347QFbejDBdMZFTxpmn6evvvqyXiqZTCd7",
"chain": "bitcoin",
"status_url": "/api/v1/public/scan/550e8400-e29b-41d4-a716-446655440000",
"results_url": "/api/v1/public/scan/550e8400-e29b-41d4-a716-446655440000/results",
"message": "Scan queued. Poll status_url until status is 'completed'."
}Poll, then fetch the results
curl -H "X-API-Key: tfk_your_key_here" \ https://chain-analyzer.com/api/v1/public/scan/550e8400-e29b-41d4-a716-446655440000 curl -H "X-API-Key: tfk_your_key_here" \ https://chain-analyzer.com/api/v1/public/scan/550e8400-e29b-41d4-a716-446655440000/resultsAdding a screened address to the watchlist enrolls it in periodic rescans and fires a webhook the moment its risk level moves. The initial scan runs asynchronously, so the response comes back with initial_scan.status: "pending" and the row is updated once it finishes.
curl -X POST https://chain-analyzer.com/api/v1/watchlist \ -H "X-API-Key: tfk_your_key_here" -H "Content-Type: application/json" \ -d '{"address":"TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX","chain":"tron","label":"deposit-12345"}'{
"success": true,
"item": {
"id": "8f14e45f-ceea-467a-9a1c-1f0d3b6a2f77",
"token_address": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"chain": "tron",
"label": "deposit-12345",
"monitor_type": "address",
"follow_mode_enabled": false,
"follow_depth": 1,
"last_risk_level": null,
"last_scanned_at": null
},
"initial_scan": {
"status": "pending",
"risk_level": "UNKNOWN",
"risk_score": 0,
"detections": []
}
}Register a webhook endpoint
curl -X POST https://chain-analyzer.com/api/v1/webhooks/configs \ -H "X-API-Key: tfk_your_key_here" -H "Content-Type: application/json" \ -d '{"webhook_url":"https://ops.example.com/hooks/chainanalyzer", "description":"AML alerts", "events":["risk.changed","risk.critical_detected"]}'{
"config": {
"id": "3d1f0c6e-5a2b-4f19-8f7a-0c9d2e6b41aa",
"webhook_url": "https://ops.example.com/hooks/chainanalyzer",
"description": "AML alerts",
"events": ["risk.changed", "risk.critical_detected"],
"is_active": true,
"created_at": "2026-08-14T02:11:44Z"
},
"secret": "b7c19a2f…8e04d1",
"message": "Save this secret securely. It won't be shown again."
}The secret is returned exactly once. Deliveries carry X-ChainAnalyzer-Signature: sha256=<HMAC-SHA256(secret, raw request body)> — verify against the raw body bytes, not a re-serialized object.
The actual delivery payload for risk.critical_detected:
{
"event": "risk.critical_detected",
"timestamp": "2026-08-14T03:27:19.482913",
"data": {
"address": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"chain": "tron",
"previous_risk_level": "LOW",
"risk_level": "CRITICAL",
"risk_score": 100,
"detections": [
{
"detector_id": "B2",
"detector_name": "OFAC_SANCTIONED",
"severity": "CRITICAL",
"description": "Address is on OFAC SDN sanctions list"
}
],
"detail_url": "https://chain-analyzer.com/scan?address=TAUN6Fwrnwwm…&chain=tron",
"score_semantics": "risk_0to100"
}
}risk.critical_detected and risk.high_detected fire as separate events from risk.changed, at the moment a watched address newly crosses that level (limited to watchlist items so you are not notified twice). The data body is identical across all three. Only when webhook_url points at hooks.slack.com is the payload converted to Slack blocks instead of this shape.
Read this instead of hard-coding plan limits in client code. It is read-only and costs no quota. A value of -1 means unlimited.
curl -H "X-API-Key: tfk_your_key_here" \ https://chain-analyzer.com/api/v1/public/usage{
"plan_id": "enterprise",
"scans_per_month": -1,
"scans_used": 8412,
"scans_remaining": -1,
"topup_remaining": 0,
"period": "2026-08",
"period_reset_at": "2026-09-01T00:00:00Z",
"api_rate_limit_per_min": 300,
"presign_rate_limit_per_min": 1200,
"batch_limit": 500,
"watchlist_limit": -1,
"webhook_limit": -1
}A minimal screen-then-decide loop with requests: fast lane first, full scan only when needed, then the decision.
import requests
BASE = "https://chain-analyzer.com/api/v1"
HEADERS = {"X-API-Key": "tfk_your_key_here"}
def screen(address: str, chain: str) -> dict:
"""Fast lane first; escalate to a full scan only when it is not clean."""
r = requests.get(
f"{BASE}/public/presign/check",
params={"chain": chain, "to": address},
headers=HEADERS,
timeout=10,
)
r.raise_for_status()
verdict = r.json()
# "ok" means registry + scam-database lookups found nothing. Accept and stop:
# this call costs no monthly quota, so it is safe on every transaction.
if verdict["verdict"] == "ok":
return {"decision": "accept", "risk_score": verdict["score"]}
# Not clean -> spend 1 scan on the full detector pipeline for the audit trail.
r = requests.post(
f"{BASE}/public/scan",
json={"address": address, "chain": chain},
headers=HEADERS,
timeout=120,
)
r.raise_for_status()
scan = r.json()
# risk_score is 0-100, higher = riskier. CRITICAL always blocks.
decision = "block" if scan["risk_level"] in ("CRITICAL", "HIGH") else "review"
return {
"decision": decision,
"risk_score": scan["risk_score"],
"detections": [d["detector_name"] for d in scan["detections"]],
}
print(screen("TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX", "tron"))A low-latency risk verdict for deciding whether to accept a deposit, release a withdrawal, or warn before a wallet signs. Built for the VASP hot path.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /public/presign/check | tfk_ | Pre-signing risk verdict for a destination / spender (API-key form) |
Main parameters: chain (required), to (required — destination or spender), value, data / calldata, spender, kind, signer, asset.
curl -H "X-API-Key: tfk_your_key_here" \ "https://chain-analyzer.com/api/v1/public/presign/check?chain=ethereum&to=0x1234..."{
"level": "critical",
"verdict": "danger",
"score": 100,
"reasons": ["Address is on the OFAC SDN sanctions list"],
"signals": { "sanctioned": true },
"confidence": 1.0,
"chain": "ethereum",
"to": "0x1234...",
"latency_ms": 12,
"cached": false
}| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /public/scan | tfk_ | Scan an address (all 12 chains: Bitcoin / Ethereum / Polygon / BNB / Base / Arbitrum / Optimism / Avalanche / Solana / Kaia / TRON / XRP). Results cached for 30 minutes. |
| GET | /public/scan | tfk_ | Query-parameter form of the same scan |
| POST | /public/scan/async | tfk_ | Scan one address asynchronously (202 + job id) |
| POST | /public/scan/batch | tfk_ | Submit a batch scan job (Pro: 100 / Business: 200 / Enterprise: 500 addresses per job) |
| GET | /public/scan/{job_id} | tfk_ | Check job progress (batch and async scans share this) |
| GET | /public/scan/{job_id}/results | tfk_ | Retrieve job results (includes score_semantics) |
| GET | /public/scans | tfk_ | List recently recorded scans (newest first, up to 100 per page) |
| GET | /public/scans/{scan_id} | tfk_ | Fetch one recorded scan including its detections |
| GET | /public/usage | tfk_ | Current-month usage and plan limits |
| GET | /public/health | None | API health check (no auth required) |
/public/scan{
"address": "347QFbejDBdMZFTxpmn6evvvqyXiqZTCd7",
"chain": "bitcoin"
}{
"success": true,
"address": "347QFbejDBdMZFTxpmn6evvvqyXiqZTCd7",
"chain": "bitcoin",
"address_type": "wallet",
"risk_level": "CRITICAL",
"risk_score": 100,
"detection_count": 4,
"detections": [
{
"detector_id": "B2",
"detector_name": "OFAC_SANCTIONED",
"severity": "CRITICAL",
"description": "Address is on OFAC SDN sanctions list",
"details": { "category": "OFAC_SANCTIONED" }
}
],
"metadata": {
"btc_balance": 0.0,
"total_transactions": 69,
"is_sanctioned": true
},
"ml_anomaly_score": 0.3431,
"scan_duration_ms": 19045,
"cached": false
}risk_score ranges from 0 (safe) to 100 (critical). risk_level is one of CRITICAL / HIGH / MEDIUM / LOW / UNKNOWN.
Bitcoin addresses with large UTXO sets and very high-frequency EVM accounts can outlast the synchronous HTTP deadline. Send those here: you get 202 and a job id immediately, and the job is persisted so it survives a restart.
/public/scan/async{
"success": true,
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"address": "0x1234...",
"chain": "ethereum",
"status_url": "/api/v1/public/scan/550e8400-e29b-41d4-a716-446655440000",
"results_url": "/api/v1/public/scan/550e8400-e29b-41d4-a716-446655440000/results",
"message": "Scan queued. Poll status_url until status is 'completed'."
}Poll status_url until status is completed, then read the results from results_url.
curl -H "X-API-Key: tfk_your_key_here" \ https://chain-analyzer.com/api/v1/public/scan/550e8400-.../results{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"score_semantics": "risk_0to100",
"results": [
{
"address": "0x1234...",
"chain": "ethereum",
"status": "completed",
"risk_level": "HIGH",
"risk_score": 78,
"detection_count": 3,
"detections": [],
"metadata": {},
"error_message": null,
"completed_at": "2026-08-13T04:21:07Z"
}
]
}Costs the same 1 scan as the synchronous call, charged once on success. Available even on plans without batch scanning.
/public/scan/batchSubmit a batch scan job (Pro: 100 / Business: 200 / Enterprise: 500 addresses per job)
{
"addresses": [
"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
"0x1234567890123456789012345678901234567890"
],
"include_ai_analysis": true,
"notify_email": "user@example.com"
}{
"success": true,
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"total_addresses": 2,
"message": "Batch scan queued for 2 addresses."
}Reads back recorded scans for your tenant. Read-only, so it does not consume monthly quota.
curl -H "X-API-Key: tfk_your_key_here" \ "https://chain-analyzer.com/api/v1/public/scans?limit=20&offset=0"Read this instead of hard-coding plan limits in client code. Read-only, so it does not consume monthly quota. A value of -1 means unlimited.
/public/usage{
"plan_id": "business",
"scans_per_month": 3000,
"scans_used": 412,
"scans_remaining": 2588,
"topup_remaining": 0,
"period": "2026-08",
"period_reset_at": "2026-09-01T00:00:00Z",
"api_rate_limit_per_min": 120,
"presign_rate_limit_per_min": 480,
"batch_limit": 200,
"watchlist_limit": 100,
"webhook_limit": 5
}Keep addresses under continuous watch and receive risk changes over webhooks. Watchlist capacity and the number of webhook endpoints are enforced server-side per plan.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /watchlist | tfk_ | List watchlist items |
| POST | /watchlist | tfk_ | Add an address to the watchlist |
| DELETE | /watchlist/{item_id} | tfk_ | Remove a watchlist item |
| POST | /watchlist/{item_id}/rescan | tfk_ | Rescan now (change detection and notifications included) |
| PATCH | /watchlist/{item_id}/follow | tfk_ | Enable Follow Mode / change depth (Pro+, depth clamped to the plan maximum) |
| GET | /watchlist/{item_id}/discoveries | tfk_ | List related addresses discovered by Follow Mode |
| GET | /webhooks/configs | tfk_ | List webhook endpoints |
| POST | /webhooks/configs | tfk_ | Register a webhook endpoint (the signing secret is returned once) |
| PATCH | /webhooks/configs/{config_id} | tfk_ | Partially update a webhook endpoint (URL, events, active state) |
| DELETE | /webhooks/configs/{config_id} | tfk_ | Delete a webhook endpoint |
| POST | /webhooks/test | tfk_ | Send a test delivery |
| GET | /webhooks/deliveries | tfk_ | Fetch the delivery log (for debugging failures) |
| GET | /webhooks/events | None | List subscribable event types (no auth required) |
| Event | Description |
|---|---|
risk.changed | A watched address changes risk level |
risk.high_detected | A HIGH risk is detected |
risk.critical_detected | A CRITICAL risk is detected |
scan.completed | A scan completes |
batch.completed | A batch job completes |
watchlist.alert | General watchlist alerts |
Everything the investigation UI does is available over the API: build a case, group addresses, bulk-scan it, export a unified PDF, analyze exposure, and trace fund flow. Long-running work (bulk scan, report package generation) returns 202 and is polled for status.
| Method | Path | Auth | Description |
|---|---|---|---|
| CRUD | /cases | tfk_ | Create / fetch / update / delete cases, and manage their addresses and notes |
| POST | /cases/{case_id}/bulk-scan | tfk_ | Start a bulk scan of the case addresses (202) and poll its progress |
| GET | /cases/{case_id}/report.pdf | tfk_ | Unified PDF report covering every address in the case |
| POST / GET | /cases/{case_id}/str-package | tfk_ | Generate and download the suspicious-transaction-report support package (evidence PDF / draft .docx / 43-column CSV) Enterprise |
| GET | /exposure/{address} | tfk_ | Multi-hop exposure analysis (direct and indirect, grouped by risk tier) |
| GET | /exposure/{address}/terminals | tfk_ | List classified endpoints reachable from one origin, with the path to each Business+ |
| POST | /graph/flow-path | tfk_ | Directed fund-flow path between two addresses Pro+ |
| POST | /graph/relationship-check | tfk_ | Relationship and shared counterparties between two addresses Enterprise |
| POST | /scan/tx | tfk_ | Scan both counterparties of a transaction by hash (all 12 chains) Enterprise |
| GET | /risk-report/{address} | tfk_ | Risk assessment report for a single address (PDF, with exposure and an AI summary) |
POST /scan/tx resolves on all 12 chains — the 8 EVM chains plus Bitcoin, Solana, TRON, and XRP. The tx_hash format is chain-dependent: 0x + 64 hex on EVM, bare 64 hex on Bitcoin / TRON / XRP, and 87-88 character base58 on Solana. A TRC-20 transfer on TRON resolves to the token Transfer event participants rather than the contract, and an XRPL Payment reports the delivered amount rather than the requested one (non-Payment types return the accounts involved plus a note). Enterprise plans only, including the integration trial tier.
Feature details: One-shot investigation ・ Contract monitoring ・ Batch scanning
Two endpoints for the reporting question an address scan cannot answer — what did this customer do with this token last quarter. Both require an explicit UTC window.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /tx-query | tfk_ | Every transfer of one token touching one address in a date range, with a counterparty roll-up |
| POST | /contract-alert | tfk_ | Sweep a whole token contract and list the addresses matching a malicious pattern |
start_date and end_date are required. end_date must be later than start_date, and the span must not exceed 90 days — a longer window returns 400 rather than being silently truncated. At most the 500 most recent transfers are returned and analyzed, so split longer periods into several calls.
Both are read-only and cost no monthly scan quota (they do count against your plan's per-minute request budget).
/tx-queryReturns every transfer of the given token that touched the given address inside the window, plus a per-counterparty aggregate (counts, amounts, net) labelled from the known registry.
{
"address": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"chain": "tron",
"start_date": "2026-05-01T00:00:00Z",
"end_date": "2026-07-30T00:00:00Z",
"lang": "en"
}{
"success": true,
"chain": "tron",
"address": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_symbol": "USDT",
"start_date": "2026-05-01T00:00:00Z",
"end_date": "2026-07-30T00:00:00Z",
"total_transfers": 128,
"transfers": [
{
"tx_hash": "5f2c…",
"block_number": 74210553,
"timestamp": "2026-07-29T11:04:12Z",
"from_address": "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
"to_address": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"amount": 24500.0,
"token_symbol": "USDT",
"token_decimals": 6,
"direction": "sent",
"counterparty": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj"
}
],
"counterparties": [
{
"address": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"label": "Unknown",
"category": "unknown",
"sent_count": 12,
"received_count": 3,
"sent_amount": 291400.0,
"received_amount": 18000.0,
"net_amount": -273400.0
}
],
"query_duration_ms": 4187,
"error": null
}A failed query is surfaced as 400 with detail, so a 200 response always carries success: true and a null error.
/contract-alertThe inverse of /tx-query: no address, sweep every transfer of the token and return only the addresses matching a malicious pattern. Built for issuers watching their own token continuously.
{
"token_contract": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"chain": "ethereum",
"start_date": "2026-07-01T00:00:00Z",
"end_date": "2026-07-31T00:00:00Z",
"lang": "en"
}{
"success": true,
"chain": "ethereum",
"token_contract": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"token_symbol": "USDT",
"start_date": "2026-07-01T00:00:00Z",
"end_date": "2026-07-31T00:00:00Z",
"total_transfers_analyzed": 500,
"flagged_addresses": [
{
"address": "0x1234567890123456789012345678901234567890",
"pattern_type": "address_poisoning",
"severity": "HIGH",
"reason": "Zero-value transfers from an address matching the first and last 4 characters of a real counterparty",
"tx_count": 7,
"total_amount": 0.0,
"sample_tx_hash": "0xabc…"
}
],
"query_duration_ms": 9042,
"error": null
}| pattern_type | Description |
|---|---|
known_malicious | Address already in the malicious registry |
address_poisoning | Address poisoning (a lookalike address baiting a misdirected send) |
relay_chain | Relay chain (nearly the full amount forwarded on immediately after receipt) |
rapid_distribution | Rapid distribution to many addresses in a short window |
large_transfer | Anomalously large transfer |
Public OSINT API. No authentication required for read endpoints. IP-based rate limiting.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /scamdb/lookup/{address} | None | Check if an address is in ScamDB (free, no auth) |
| GET | /scamdb/entries | None | List verified ScamDB entries (paginated) |
| GET | /scamdb/entries/{scam_id} | None | Get a single ScamDB entry |
| GET | /scamdb/stats | None | ScamDB statistics |
| GET | /scamdb/search?q=... | tfk_ | Full-text search ScamDB (tfk_ key required) |
| POST | /scamdb/report | tfk_ | Submit a scam report (tfk_ key required) |
/scamdb/lookup/{address}curl https://chain-analyzer.com/api/v1/scamdb/lookup/7kMpieh2THdaC5eUvxFJDL3TdsQWVQCwdhsEjLj1eL26{
"found": true,
"entry": {
"id": "SCAM-001",
"address": "7kMpieh2THdaC5eUvxFJDL3TdsQWVQCwdhsEjLj1eL26",
"type": "drainer",
"severity": "danger",
"domains": ["solland.cc", "hibit.app"],
"method": "FCFS airdrop phishing",
"total_stolen_usd": 3700,
"verified": true
},
"match_type": "exact"
}| Plan | Requests/min | Description |
|---|---|---|
| Pro | 60 | REST API |
| Business | 120 | REST API |
| Enterprise | 300 | REST API |
| Pro / Business / Enterprise | 240 / 480 / 1200 | Screening (separate bucket) |
| Any (IP-based) | 30 | ScamDB reads |
ScamDB reads: IP-based 30 req/min. A 429 error is returned when exceeded.
| Plan | Scans/month |
|---|---|
| Free | 10 |
| Starter | 200 |
| Pro | 1,000 |
| Business | 3,000 |
| Enterprise | Unlimited |
Only execution charges quota. A synchronous or asynchronous scan costs 1; batch and case bulk scans cost 1 per address, charged only for addresses that complete. Screening, usage, history, and report downloads cost nothing.
| Code | Description |
|---|---|
400 | Bad request (invalid parameters) |
401 | Authentication error (API key is invalid or missing) |
403 | Access denied (plan feature restriction) |
404 | Resource not found |
409 | Conflict (e.g. a job is already running for this case) |
410 | The artifact cannot be rebuilt — regenerate it |
422 | Validation error (missing required field, wrong type) |
429 | Rate limit exceeded, or monthly quota exhausted |
500 | Server error |
Errors use the standard FastAPI {"detail": "..."} envelope. What your client should do differs by status.
A missing X-API-Key (or Authorization: Bearer) returns detail: "API key required"; a revoked or unknown key returns detail: "Invalid API key". Retrying will not help — suspect the wrong key or a revocation. Key and plan changes propagate within 5 minutes.
{ "detail": "Invalid API key" }The key is valid but the feature is not on its plan. The messages are fixed per feature (for example "Transaction-hash scan is an Enterprise feature.", "Batch scanning not available on your plan", "Watchlist limit reached (100 items)"). Retrying is pointless; this needs an upgrade or fewer registered items.
{ "detail": "Transaction-hash scan is an Enterprise feature." }The resource does not exist, or belongs to another tenant — the two are deliberately indistinguishable so existence does not leak. Suspect a wrong job or case id, or an artifact past its retention window.
A body field or path parameter has the wrong shape (a missing required field, a scan_id that is not a UUID). This is FastAPI's structured validation error, so detail is an array naming the location and reason. Unlike a 400, the request never reached the handler.
{
"detail": [
{
"type": "missing",
"loc": ["body", "token_contract"],
"msg": "Field required"
}
]
}Two different conditions share this status, and the headers tell them apart mechanically.
(a) Per-minute rate limit — carries Retry-After
detail reads "Rate limit exceeded. Max 120 requests per minute." and the response carries Retry-After (seconds), X-RateLimit-Limit, and X-RateLimit-Remaining: 0. Waiting the stated interval always clears it, so back off and retry.
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
{ "detail": "Rate limit exceeded. Max 120 requests per minute." }(b) Monthly scan quota exhausted — no Retry-After
detail is a fixed Japanese message including the counts, returned in Japanese regardless of Accept-Language or the lang parameter. There is no Retry-After, and no amount of backoff clears it before the next UTC month — this needs a plan upgrade or a scan pack. Endpoints that take an explicit list of addresses report the shortfall separately ("N scans remaining but M requested") rather than silently truncating your list.
HTTP/1.1 429 Too Many Requests
{
"detail": "月間スキャン上限に達しています(1000 / 1000 件)。プランのアップグレード、またはスキャンパックの購入をご検討ください。"
}The other thing integrators get backwards as often as error handling is the score direction. Both risk_score and score run 0 (safe) to 100 (critical) — higher is riskier. History, batch, and webhook payloads carry score_semantics: "risk_0to100" so your client can assert the convention rather than assume it.
SDKs for Python, JavaScript, and Go are in development.
© 2026 ChainAnalyzer. All rights reserved.