A2 Route Check an address — free

API docs

This page describes the Business API key: authentication, the screening endpoint, error codes, limits and the read-only endpoints (balance, check history, report verification, address monitoring). Every path and header below is the exact one the service accepts today.

1. Authentication

Every Business API request carries the key in the standard bearer header:

Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE

A key has the shape a2r_<16 hex chars>_<secret>. The 16 hex characters are the lookup prefix; everything after the second underscore is the secret and is only ever shown once, at issuance.

An operator issues a Business key with the CLI:

python -m app.cli issue-key --kind business --name "<client name>" --account-id <account-uuid>

The command prints the raw key to stdout exactly once and never writes it to a file or a log. The key does not expire for 90 days by default (--expires-at overrides this) and is revoked with python -m app.cli revoke-key <key_id>. A revoked or expired key, a malformed key, and an unknown key all answer the same way: 401 invalid_api_key — which one it was is never observable from the response.

A Business key already names its own account; requests authenticated with it must never also send telegram_user_id (that field is for the Telegram bot principal only) — doing so is 422 telegram_user_id_forbidden.

2. Screen an address or a transaction

POST /kyt/screen takes either an address (check_kind: "address", the default) or a transaction (check_kind: "transaction", with txid and direction). Supported networks: tron-mainnet, ethereum-mainnet. Every call also requires an Idempotency-Key header (any client-generated non-empty string up to 128 characters, e.g. a UUID) — repeating the same key with the same account returns the original result instead of billing a second time. An empty or over-length key is rejected with 400 invalid_idempotency_key.

TRON, address:

curl -X POST https://a2route.com/kyt/screen \
  -H "Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE" \
  -H "Idempotency-Key: 5b1f7e2a-8f2b-4c7b-9a1e-2b6a7e9c1234" \
  -H "Content-Type: application/json" \
  -d '{
    "check_kind": "address",
    "network": "tron-mainnet",
    "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
  }'

Ethereum, transaction:

curl -X POST https://a2route.com/kyt/screen \
  -H "Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE" \
  -H "Idempotency-Key: 8b2a1f3e-4c5d-4a6b-9c7d-1e2f3a4b5c6d" \
  -H "Content-Type: application/json" \
  -d '{
    "check_kind": "transaction",
    "network": "ethereum-mainnet",
    "txid": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "direction": "incoming"
  }'

Response envelope:

{
  "check_id": "b6b1e2b0-2e9b-4a0a-9d0a-2f6b2e9b0a11",
  "kind": "address",
  "network": "tron-mainnet",
  "direction": null,
  "decision": "ALLOW",
  "completeness": "complete",
  "risk_labels": [],
  "reason": "no elevated risk signals",
  "checked_at": "2026-09-07T10:15:00Z",
  "served_from": "provider",
  "credits": {"bucket": "monthly", "remaining_checks": 1187, "remaining_cents": null},
  "report_available": true,
  "data_as_of": "2026-09-07T10:15:00Z",
  "cache_hit": false,
  "token": null,
  "transfer": null,
  "sanctions_snapshot_at": "2026-09-07T08:00:00Z",
  "basis_stale": false,
  "recommendation": "accept",
  "recommendation_text": "Accept. No risk signals were found at the time of checking. This is not a guarantee; keep this report with the transaction record.",
  "recommendation_version": 1
}
FieldMeaning
decisionALLOW / REVIEW / BLOCK
completenesscomplete / partial — whether every data source answered. A third case (no source could answer at all) never reaches this field: the request fails closed as 503 screening_unavailable instead (see §3)
creditsWhich quota bucket this check billed, and what remains in it (see §4)
report_availableWhether a PDF can be issued for this check (false only for a free check)
data_as_of / cache_hitWhen the underlying decision was actually computed, and whether this response was served from cache
basis_staleTrue if a cached decision predates newer sanctions data for this same subject
recommendation / recommendation_textMachine code plus the exact human-readable next step, versioned by recommendation_version

To get a PDF report for a completed, billable check: POST /v1/checks/{check_id}/report (also needs an Idempotency-Key header), then download it with GET /v1/reports/{report_id}/file.

3. Error codes

Every documented endpoint's error response has the shape {"error": "<code>"}, mirrored in an X-Error-Code response header — except an unmapped route or an unsupported method, which falls back to FastAPI's default {"detail": "..."} shape instead.

StatusCodeWhen
402insufficient_creditNo quota left in any bucket. The body also carries balance_remaining_cents and check_price_cents when a prepaid balance was the last bucket tried.
429force_limitforce: true is capped at 5 forced re-checks per account per UTC day. The body and a Retry-After header carry retry_after_seconds, counted to the next UTC midnight.
503screening_unavailableThe screening engine or an upstream data source (TronGrid, an Ethereum RPC node) could not answer in time. Any debit for this attempt is refunded. The body and a Retry-After header carry retry_after_seconds: 60.

Other codes worth knowing: 401 invalid_api_key (see §1), 403 account_suspended, 422 telegram_user_id_forbidden (a Business key must never send telegram_user_id), 409 idempotency_key_conflict (same Idempotency-Key reused with a different request body).

4. Limits and plans

Business is $299/mo: 1,200 checks included, $0.30 per check above that, drawn from a prepaid balance. There is no hard cap beyond your balance — a check simply fails with 402 insufficient_credit once every bucket (monthly allowance, then balance) is exhausted; the screening engine is never called for an unpaid check.

Your own remaining quota is always visible with GET /v1/balance:

curl https://a2route.com/v1/balance \
  -H "Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE"
[
  {"bucket": "monthly", "remaining_checks": 1187, "remaining_cents": null},
  {"bucket": "balance", "remaining_checks": null, "remaining_cents": 4200}
]

Every POST /kyt/screen response's own credits field (§2) already tells you which bucket that specific check drew from and what is left in it, so you rarely need to poll /v1/balance separately. Full check history is available at GET /v1/checks (paginated) and GET /v1/checks/{check_id} (one check).

5. Verify a report

Drop a PDF into the verification page (or choose it with the file control). Its SHA-256 is calculated in your browser; only GET /verify?fp=… is sent to A2 Route. verified: "file" means the file matches an issued file and report. Entering only a report_id returns verified: "record": it only confirms that a report record exists and does not prove that a presented file has the same bytes; no presented file was verified. An unknown fingerprint or id returns verified: null. No API key is required.

curl "https://a2route.com/verify?fp=<sha256-of-the-pdf-file>"
curl https://a2route.com/verify/<report_id>
{
  "status": "valid",
  "report_id": "RS-20260907-4F2A9B10",
  "issued_at": "2026-09-07T10:16:00Z",
  "decision": "ALLOW",
  "network": "tron-mainnet",
  "subject_hint": "TR7NHq…gjLj6t",
  "completeness": "complete",
  "file_sha256": "0f4c…9a2e",
  "verified": "file",
  "check_kind": "transaction"
}

completeness answers a different question than status: "valid" says the record exists, "complete" or "partial" says how much of the screening behind it actually ran. It is null when the report carries no coverage state.

A fingerprint or id that matches no issued report answers {"status": "unknown"} — every other field is still present, as null, not omitted. This endpoint is rate-limited per client IP, not per API key, since it takes none.

6. Monitor an address

A Pro-plan subscription (not the Business plan itself) unlocks continuous weekly re-screening of up to 20 addresses: POST /v1/monitored adds an address, GET /v1/monitored lists your watchlist, DELETE /v1/monitored/{monitored_id} removes one. A Business key names its account implicitly, the same way it does for /kyt/screen — never pass telegram_user_id or account_id.

curl -X POST https://a2route.com/v1/monitored \
  -H "Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE" \
  -H "Content-Type: application/json" \
  -d '{"network": "tron-mainnet", "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"}'

curl https://a2route.com/v1/monitored \
  -H "Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE"

curl -X DELETE https://a2route.com/v1/monitored/<monitored_id> \
  -H "Authorization: Bearer a2r_1234567890abcdef_REDACTEDSECRETVALUE"

Without an active Pro subscription, POST /v1/monitored answers 403 monitoring_not_included. GET and DELETE on your own rows always work, even after a Pro subscription lapses, so you can still see and clean up an existing watchlist. A 21st address on an account already at the cap of 20 is 409 monitoring_limit; re-adding an address already on the list is 409 monitored_address_exists.