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
}
| Field | Meaning |
|---|---|
decision | ALLOW / REVIEW / BLOCK |
completeness | complete / 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) |
credits | Which quota bucket this check billed, and what remains in it (see §4) |
report_available | Whether a PDF can be issued for this check (false only for a free check) |
data_as_of / cache_hit | When the underlying decision was actually computed, and whether this response was served from cache |
basis_stale | True if a cached decision predates newer sanctions data for this same subject |
recommendation / recommendation_text | Machine 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.
| Status | Code | When |
|---|---|---|
| 402 | insufficient_credit | No 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. |
| 429 | force_limit | force: 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. |
| 503 | screening_unavailable | The 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.