Transaction Safety API
Check a transaction before confirming a payment. Integrate TRON checks, understand the results and handle retries safely.
Transaction Safety API
Check a transaction before recording a payment as received. ChainZap combines transaction facts, risk signals and optional expected recipient, token and amount matching into a single response.
What you can check
TRON is available in v1. Ethereum and TON are planned; they are not accepted by the API. Access is invite-only and subject to your organization's enabled networks and limits. Availability of a network is separate from live service status.
Use POST /v1/transaction/check for a minimal KYT check or add the expected_* fields for enhanced verification. Use GET /v1/usage for your organization's quota and recent usage. Authentication uses X-API-Key from your server.
Interpret the whole response
A successful HTTP response does not mean a payment is safe or confirmed. Read transaction_state, overall_state, coverage, matching flags, freshness and reasons together. null means unknown or not requested, depending on the field. Missing evidence must never be treated as a clean result.
Start with the quickstart, then review coverage and idempotency.
Your first transaction check
Get access
Ask for an invitation through ChainZap support. Sign in through your identity provider, open API keys, create a named key and save it in your server's secret store. The full key is shown once. Commercial terms are agreed with your organization; no public price is assumed.
Send your first request
This example uses a fictional key and transaction hash. Replace them on your server. All amounts are decimal strings. Keep your idempotency key with the payment operation, including across process restarts.
curl --include --max-time 15 'https://api.chainzap.io/v1/transaction/check' \
-H 'Content-Type: application/json' \
-H 'X-API-Key: cz_live_000000000000.fictional_example_only' \
-H 'Idempotency-Key: merchant-order-1001-check-1' \
-H 'X-Request-ID: merchant-order-1001-attempt-1' \
--data '{"network":"tron","tx_hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'
Inspect X-Request-ID in the response headers and request_id in the JSON. Preserve this identifier for support. The fictional hash may return not_found; see transaction states.
Verify expected payment details
{
"network": "tron",
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"expected_recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"expected_token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"expected_amount": "1250.000001"
}
The example addresses are public sample addresses, not payment instructions. Match an exact token contract, not a symbol. Read all three match flags; they refer to the same selected transfer. See matching.
Handle uncertainty
If the connection closes before you receive a response, retry the same body and same Idempotency-Key. Never invent a new key to bypass an uncertain result. Only retry a received error when error.retryable is true; honor Retry-After and cap retries. A stored error can be replayed unchanged: retryable does not force a new provider check.
See Python and TypeScript examples for bounded retries and usage for quota inspection.
Authentication
Server API keys
Send X-API-Key over HTTPS on every business API request. Never place a key in a URL, browser storage, analytics, support message or source control. Keys grant access to one organization; requests do not accept a caller-selected tenant ID.
The developer portal sign-in session is separate from API keys. Owner, admin and developer members can create, rotate and revoke keys. Billing and viewer members can inspect usage and key metadata but cannot manage keys or run checks.
Key lifecycle
Give each integration a descriptive label. The full key is shown only on creation or rotation; lists show a prefix. Rotation immediately revokes the old key and creates its replacement atomically. Update your server's secret store after rotation. To avoid an outage during a planned migration, create a separate key, update and verify your integration, then explicitly revoke the previous key.
An invalid or revoked key returns 401 invalid_api_key. A suspended organization returns 403 tenant_suspended. A network outside the organization's entitlements returns 403 network_not_enabled. Do not retry these errors without correcting access.
POST /v1/transaction/check
Request
The request is a strict JSON object: unknown fields are rejected. network is tron. tx_hash is a 64-character hexadecimal transaction identifier. Optional expected_recipient and expected_token_contract are checksum-valid TRON addresses. expected_amount is a nonnegative decimal string; exponent notation and JSON numbers are rejected.
Add Idempotency-Key for a logical check operation and X-Request-ID for correlation. Business request bodies are limited to 4 KiB. Send Content-Type: application/json. See the generated API reference for exact required fields, enums and response schemas.
Response
Read transaction_state independently from overall_state. The response includes confirmations, normalized transfers, the selected transfer, risk level, sanctions status, token authenticity, matching flags, coverage, freshness and 2 to 5 prioritized reasons. Transfer and exposure amounts are decimal strings. Nullable fields must be handled explicitly.
transfers_truncated=true and AMBIGUOUS_TRANSFER mean the response requires attention. Do not infer a full list from a truncated response or combine matching flags from different transfers.
A response with partial coverage is not billed. Complete coverage consumes one unit of monthly quota, including a complete high-risk result. Replaying the same idempotent operation never bills twice.
Match a transfer to a payment
Use the expected_* fields to verify a payment's recipient, token contract and amount. Send all three fields or only the comparisons you need.
One transfer, three comparisons
A transaction can contain several transfers. ChainZap first looks for a transfer that satisfies every supplied condition. If none does, it selects the transfer with the most matches. Ties are resolved consistently using the transfer index.
recipient_match, token_match and amount_match all describe that same transfer. If several transfers satisfy every condition, the response includes AMBIGUOUS_TRANSFER.
| Result | Meaning |
|---|---|
true |
The value matches the one you supplied. |
false |
The value could be checked but does not match. |
null |
You did not request this comparison, or there was not enough data to make it. |
How values are compared
If you provide an expected value but its transfer data is unavailable, the corresponding match field is null, coverage is partial, and the reasons include EXPECTED_DATA_UNAVAILABLE. Missing token decimals, for example, do not prove an amount mismatch. Do not treat null as a successful match.
Addresses are validated with TRON checksums. Tokens are compared by exact contract, never by name or symbol. Amounts are converted from decimal strings to raw integer units using known token decimals and compared exactly; no floating-point tolerance is applied. An amount with more nonzero precision than the asset supports returns 422 invalid_expected_amount.
Mismatch example
If you expect "10.000001" but the selected transfer contains "10", the response includes amount_match=false and EXPECTED_AMOUNT_MISMATCH.
A different recipient produces EXPECTED_RECIPIENT_MISMATCH; a different token contract produces EXPECTED_TOKEN_MISMATCH. A matching amount does not prove that the recipient and token also match. Before accepting a payment, check every required comparison, the transaction state and the overall assessment.
GET /v1/usage
Request
curl --include --max-time 15 'https://api.chainzap.io/v1/usage?days=30' \
-H 'X-API-Key: cz_live_000000000000.fictional_example_only' \
-H 'X-Request-ID: merchant-usage-1001'
days is an integer from 1 to 90, default 30. No idempotency key is needed for this read. Inspect the response X-Request-ID header. A usage request is rate-limited but does not consume transaction quota. Retry only errors marked retryable, respecting Retry-After.
Quota and statistics
period.start and period.end delimit the current UTC calendar month. used counts billable completed checks, reserved counts outstanding reservations, and remaining = max(0, limit - used - reserved). A reservation is released when a nonbillable request is finalized.
recent counts completed logical checks during the requested rolling period. Idempotent HTTP replays are not new check records. It includes billable, error and LIMITED_DATA counts, average latency, networks and overall-state distribution. It contains no transaction hashes or provider details.
The portal adds UTC daily buckets, 1/7/30-day counters and p50/p95 when at least 20 completed latency samples exist in the last 30 days. Successful-response counts include partial responses; limited coverage is a subset, not an additional total.
Integration examples
Python: idempotent retry
This server-side example uses httpx. The key is fictional. In a real integration, load the API key from your server's secret store and persist operation_key and body in your own operation ledger before the first attempt. A process restart must reuse them.
import random
import time
import httpx
api_key = "cz_live_000000000000.fictional_example_only"
operation_key = "merchant-order-1001-check-1"
body = {"network": "tron", "tx_hash": "a" * 64}
def check():
with httpx.Client(timeout=15, follow_redirects=False) as client:
for attempt in range(4):
try:
response = client.post(
"https://api.chainzap.io/v1/transaction/check",
headers={
"X-API-Key": api_key,
"Idempotency-Key": operation_key,
"X-Request-ID": f"order-1001-attempt-{attempt + 1}",
},
json=body,
)
except httpx.TransportError:
if attempt == 3:
raise RuntimeError("unresolved_transport_outcome") from None
time.sleep(2 ** attempt + random.uniform(0, 1))
continue
print("X-Request-ID:", response.headers.get("X-Request-ID"))
try:
result = response.json()
except ValueError:
raise RuntimeError("unresolved_gateway_response") from None
if response.is_success:
return result
error = result.get("error", {})
if error.get("retryable") is not True:
raise RuntimeError(error.get("code", "unresolved_response"))
if response.headers.get("Idempotent-Replayed") == "true":
raise RuntimeError("stored_error_requires_reconciliation")
try:
instructed = int(response.headers.get("Retry-After", "0"))
except ValueError:
raise RuntimeError("invalid_retry_after") from None
delay = max(instructed, 2 ** attempt) + random.uniform(0, 1)
if attempt == 3 or delay > 30:
raise RuntimeError("schedule_retry_with_same_operation_key")
time.sleep(delay)
This handles retryable 409/429/503/504 and network uncertainty without creating another logical operation. Treat returned business states according to your payment workflow. Do not log body or the entire result.
JavaScript / TypeScript: server request
Use this code on your server, never in a browser with a live key. The operation key and body must be persisted before calling the function, just as in the Python example.
type PublicResult = {
request_id?: string;
error?: { code: string; message: string; retryable: boolean };
[field: string]: unknown;
};
const apiKey = "cz_live_000000000000.fictional_example_only";
const operationKey = "merchant-order-1001-check-1";
const body = { network: "tron", tx_hash: "a".repeat(64) };
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function check(): Promise<PublicResult> {
for (let attempt = 0; attempt < 4; attempt++) {
let response: Response;
try {
response = await fetch("https://api.chainzap.io/v1/transaction/check", {
method: "POST", redirect: "error", signal: AbortSignal.timeout(15000),
headers: {
"Content-Type": "application/json", "X-API-Key": apiKey,
"Idempotency-Key": operationKey,
"X-Request-ID": `order-1001-attempt-${attempt + 1}`,
},
body: JSON.stringify(body),
});
} catch {
if (attempt === 3) throw new Error("unresolved_transport_outcome");
await wait((2 ** attempt + Math.random()) * 1000);
continue;
}
console.info("X-Request-ID:", response.headers.get("X-Request-ID"));
const result = await response.json() as PublicResult;
if (response.ok) return result;
if (result.error?.retryable !== true) {
throw new Error(result.error?.code ?? "unresolved_response");
}
if (response.headers.get("Idempotent-Replayed") === "true") {
throw new Error("stored_error_requires_reconciliation");
}
const instructed = Number(response.headers.get("Retry-After") ?? "0");
if (!Number.isFinite(instructed) || instructed < 0) {
throw new Error("invalid_retry_after");
}
const delay = Math.max(instructed, 2 ** attempt) + Math.random();
if (attempt === 3 || delay > 30) {
throw new Error("schedule_retry_with_same_operation_key");
}
await wait(delay * 1000);
}
throw new Error("retry_budget_exhausted");
}
Response scenarios
The following synthetic fixtures illustrate valid response shapes, not live checks. Every sample has a corresponding X-Request-ID header equal to its request_id. Inspect null values and coverage as well as the overall state.
Confirmed transaction
View full JSON response
{
"aml_risk_level": "low",
"amount": "1250.000001",
"amount_match": true,
"confirmations": 27,
"coverage": "complete",
"decimals": 6,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "CLEAR",
"reasons": [
{
"category": "transaction",
"code": "TX_FINALIZED",
"severity": "info"
},
{
"category": "risk",
"code": "AML_LOW",
"severity": "info"
}
],
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"recipient_match": true,
"request_id": "example-confirmed",
"required_confirmations": 19,
"sanctions_hit": false,
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_authenticity": "official",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_match": true,
"token_symbol": "USDT",
"transaction_category": "trc20_transfer",
"transaction_state": "confirmed",
"transaction_type": "TriggerSmartContract",
"transfers": [
{
"amount": "1250.000001",
"asset_type": "trc20",
"decimals": 6,
"index": 0,
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_id": null,
"token_symbol": "USDT"
}
],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Awaiting confirmation
View full JSON response
{
"aml_risk_level": "low",
"amount": "1250.000001",
"amount_match": true,
"confirmations": 3,
"coverage": "complete",
"decimals": 6,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "REVIEW",
"reasons": [
{
"code": "TX_PENDING_CONFIRMATIONS",
"category": "transaction",
"severity": "warning"
},
{
"code": "AML_LOW",
"category": "risk",
"severity": "info"
}
],
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"recipient_match": true,
"request_id": "example-pending",
"required_confirmations": 19,
"sanctions_hit": false,
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_authenticity": "official",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_match": true,
"token_symbol": "USDT",
"transaction_category": "trc20_transfer",
"transaction_state": "pending",
"transaction_type": "TriggerSmartContract",
"transfers": [
{
"amount": "1250.000001",
"asset_type": "trc20",
"decimals": 6,
"index": 0,
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_id": null,
"token_symbol": "USDT"
}
],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Failed transaction
View full JSON response
{
"aml_risk_level": "low",
"amount": null,
"amount_match": null,
"confirmations": 27,
"coverage": "complete",
"decimals": 6,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "REVIEW",
"reasons": [
{
"code": "TX_REVERTED",
"category": "transaction",
"severity": "warning"
},
{
"code": "TX_FACTS_VERIFIED",
"category": "transaction",
"severity": "info"
}
],
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"recipient_match": null,
"request_id": "example-failed",
"required_confirmations": 19,
"sanctions_hit": false,
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_authenticity": "official",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_match": null,
"token_symbol": "USDT",
"transaction_category": "trc20_transfer",
"transaction_state": "failed",
"transaction_type": "TriggerSmartContract",
"transfers": [],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Transaction not found
{
"aml_risk_level": "unknown",
"amount": null,
"amount_match": null,
"confirmations": null,
"coverage": "complete",
"decimals": null,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "REVIEW",
"reasons": [
{
"code": "TX_NOT_FOUND",
"category": "transaction",
"severity": "warning"
},
{
"code": "TX_FACTS_VERIFIED",
"category": "transaction",
"severity": "info"
}
],
"recipient": null,
"recipient_match": null,
"request_id": "example-not-found",
"required_confirmations": 19,
"sanctions_hit": null,
"sender": null,
"token_authenticity": "unknown",
"token_contract": null,
"token_match": null,
"token_symbol": null,
"transaction_category": "unknown",
"transaction_state": "not_found",
"transaction_type": "unknown",
"transfers": [],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Insufficient data
View full JSON response
{
"aml_risk_level": "unknown",
"amount": "1250.000001",
"amount_match": true,
"confirmations": 27,
"coverage": "partial",
"decimals": 6,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "LIMITED_DATA",
"reasons": [
{
"code": "AML_UNAVAILABLE",
"category": "risk",
"severity": "warning"
},
{
"code": "SANCTIONS_UNAVAILABLE",
"category": "sanctions",
"severity": "warning"
}
],
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"recipient_match": true,
"request_id": "example-limited-data",
"required_confirmations": 19,
"sanctions_hit": null,
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_authenticity": "official",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_match": true,
"token_symbol": "USDT",
"transaction_category": "trc20_transfer",
"transaction_state": "confirmed",
"transaction_type": "TriggerSmartContract",
"transfers": [
{
"amount": "1250.000001",
"asset_type": "trc20",
"decimals": 6,
"index": 0,
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_id": null,
"token_symbol": "USDT"
}
],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Sanctions match
View full JSON response
{
"aml_risk_level": "low",
"amount": "1250.000001",
"amount_match": true,
"confirmations": 27,
"coverage": "complete",
"decimals": 6,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "HIGH_RISK",
"reasons": [
{
"code": "SANCTIONS_HIT",
"category": "sanctions",
"severity": "critical"
},
{
"code": "TX_FINALIZED",
"category": "transaction",
"severity": "info"
}
],
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"recipient_match": true,
"request_id": "example-sanctions-hit",
"required_confirmations": 19,
"sanctions_hit": true,
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_authenticity": "official",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_match": true,
"token_symbol": "USDT",
"transaction_category": "trc20_transfer",
"transaction_state": "confirmed",
"transaction_type": "TriggerSmartContract",
"transfers": [
{
"amount": "1250.000001",
"asset_type": "trc20",
"decimals": 6,
"index": 0,
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_id": null,
"token_symbol": "USDT"
}
],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Payment details do not match
View full JSON response
{
"aml_risk_level": "low",
"amount": "1250.000001",
"amount_match": false,
"confirmations": 27,
"coverage": "complete",
"decimals": 6,
"freshness": {
"checked_at": "2026-09-03T12:00:00Z",
"max_source_age_seconds": 4,
"risk_data_at": "2026-09-03T12:00:00Z",
"token_data_at": "2026-09-03T12:00:00Z",
"transaction_at": "2026-09-03T11:59:00Z"
},
"key_exposure": [],
"network": "tron",
"overall_state": "REVIEW",
"reasons": [
{
"code": "EXPECTED_AMOUNT_MISMATCH",
"category": "expected",
"severity": "warning"
},
{
"code": "TX_FINALIZED",
"category": "transaction",
"severity": "info"
}
],
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"recipient_match": true,
"request_id": "example-expected-mismatch",
"required_confirmations": 19,
"sanctions_hit": false,
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_authenticity": "official",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_match": true,
"token_symbol": "USDT",
"transaction_category": "trc20_transfer",
"transaction_state": "confirmed",
"transaction_type": "TriggerSmartContract",
"transfers": [
{
"amount": "1250.000001",
"asset_type": "trc20",
"decimals": 6,
"index": 0,
"recipient": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"sender": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"token_id": null,
"token_symbol": "USDT"
}
],
"transfers_truncated": false,
"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
Transaction states
| State | Meaning | Integration behavior |
|---|---|---|
confirmed |
Chain facts report confirmation. | Also require sufficient confirmations and examine the overall state and coverage. |
pending |
The transaction is visible but finality is incomplete. | Do not record final settlement; schedule a separately identified later observation after resolving the current check. |
failed |
Execution failed or reverted. | Do not treat the expected transfer as received. |
not_found |
The queried transaction was not found in the available evidence. | Check the network and hash; absence is not proof of a completed payment. |
unknown |
The available transaction facts cannot establish a state. | Treat as incomplete evidence. |
TRON v1 uses a 19-confirmation finality policy together with finalized chain evidence. A transaction state alone does not establish risk coverage. Response examples illustrate these states.
Overall states
| State | Meaning |
|---|---|
CLEAR |
Applicable checks completed without an identified issue and finality requirements were met. This is not a guarantee against future risk. |
REVIEW |
The result contains a condition requiring review, such as pending finality, failed execution, elevated risk or an expected-payment mismatch. |
HIGH_RISK |
A high or critical finding exists, such as a sanctions match, high AML risk or a fake token. |
LIMITED_DATA |
Evidence is incomplete, unsupported or contradictory. Never treat this as CLEAR. |
Coverage and overall state are independent: a known high-risk finding may coexist with partial coverage. Conflicting transaction facts take priority as LIMITED_DATA. Always retain and inspect the actual returned reasons; the list is prioritized and bounded, not an exhaustive evidence dump.
Data completeness
Complete and partial
complete means the applicable evidence checks met their requirements for this response. It does not mean the transaction is safe. partial means one or more required sources, asset mappings, token checks or transaction facts are missing, unsupported or inconsistent.
Unavailable sanctions data is sanctions_hit=null, never false. Unknown risk is aml_risk_level="unknown". An unverified token is token_authenticity="unknown"; its ticker does not prove authenticity. LIMITED_DATA is a normal business response with HTTP 200, not necessarily a transport error.
Freshness
Read freshness.checked_at, the evidence timestamps and max_source_age_seconds. Nullable source timestamps remain unknown. An idempotent replay returns the original result and original request ID; it is not a fresh observation. A new observation is a new logical operation after you have resolved the previous response.
Billing
Only complete checks are billable. Partial coverage, transport errors and idempotent replays do not consume an additional billable unit. The playground uses the same rule and the same organization's quota.
Reason codes
Each reason has code, category and severity (info, warning, high, critical). Responses include 2 to 5 prioritized reasons. Consumers must tolerate future codes and must not infer that omitted reasons mean a check was performed.
| Code | Meaning |
|---|---|
TX_FACTS_VERIFIED |
Transaction facts meet verification requirements. |
TX_FACTS_PARTIAL |
Required transaction facts are incomplete. |
TX_FACTS_DISAGREE |
Transaction evidence is contradictory. |
TX_FINALIZED |
Transaction finality requirements are met. |
TX_PENDING_CONFIRMATIONS |
Finality or confirmations are insufficient. |
TX_REVERTED |
Transaction execution failed. |
TX_NOT_FOUND |
The transaction was not found. |
TRANSFERS_TRUNCATED |
Transfer evidence was truncated. |
AMBIGUOUS_TRANSFER |
More than one transfer matches the supplied expectations. |
AML_UNSUPPORTED_ASSET |
The applicable asset lacks supported AML coverage. |
AML_UNAVAILABLE |
Applicable AML evidence is missing. |
AML_LOW |
Available AML assessment reports low risk. |
AML_ELEVATED |
Available AML assessment reports moderate risk. |
AML_HIGH |
Available AML assessment reports high or severe risk. |
SANCTIONS_HIT |
A sanctions match is present. |
SANCTIONS_CLEAR |
Available sanctions screening found no match. |
SANCTIONS_UNAVAILABLE |
Sanctions screening is unavailable; this is not a clear result. |
TOKEN_AUTH_UNAVAILABLE |
Token authentication evidence is unavailable. |
TOKEN_OFFICIAL |
Token contract is recognized as official. |
TOKEN_FAKE |
Token is identified as fake. |
TOKEN_UNKNOWN |
Token authenticity is unknown. |
EXPECTED_RECIPIENT_MISMATCH |
The selected transfer does not match the expected recipient. |
EXPECTED_TOKEN_MISMATCH |
The selected transfer does not match the expected contract. |
EXPECTED_AMOUNT_MISMATCH |
The selected transfer does not match the expected raw amount. |
EXPECTED_DATA_UNAVAILABLE |
A requested comparison could not be completed because transfer data is missing. Its match field is null. |
Idempotency
Persist a key per logical operation
Idempotency-Key accepts 1 to 128 printable ASCII characters. Keys are scoped to your organization and the normalized request body. Preserve the same key and body across retries, including retries from another server or after API key rotation. Never include credentials or personal information in it.
A completed operation is replayed byte-for-byte with Idempotent-Replayed: true and its original X-Request-ID. A different body with the same key returns nonretryable 409 idempotency_conflict. A still-processing operation returns retryable 409 idempotency_in_progress with Retry-After.
Retention and errors
The default retention is 24 hours; confirm any contracted retention change with support. After retention expires, the key can represent a new billable execution. Bound automatic retries to the retention window and keep your own payment-operation ledger.
Stored errors, including quota and provider failures finalized for an operation, are replayed too. retryable=true describes the failure; it does not erase a stored response. Stop automatic retrying when Idempotent-Replayed: true accompanies a stored error. Once you have definitively received and reconciled that result, explicitly create a new logical check if your workflow needs a later observation. Never use a new key merely because the original network outcome is unknown.
Rate limits, quota and concurrency
Organization limits
Limits come from your current versioned plan or a custom entitlement assignment. Inspect /v1/usage and the portal's Plan and limits page. No public numeric entitlement or price is promised by the examples.
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset describe your organization's request capacity. X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset describe monthly billable quota; reset headers use Unix seconds. All API keys and requests made in the portal share the same quota. Monthly periods follow UTC, regardless of your browser's timezone.
Backpressure
Rate exhaustion returns 429 rate_limit_exceeded. Too many in-flight checks returns 429 concurrency_limit_exceeded. A full quota returns 429 quota_exceeded. Honor Retry-After, which may be long for a monthly quota. Replays still pass through access, rate and concurrency controls, but do not bill again.
Use a bounded request queue and per-organization concurrency on your server. Plan changes affect future entitlements and do not rewrite historical usage.
Retries and timeouts
Bounded retries
Use a client deadline longer than the 12-second server hard deadline, for example 15 seconds per attempt. The latency target is p95 at most 5 seconds; it is a release gate, not an unconditional guarantee. Account for network time outside the server budget.
For a received error, retry only when error.retryable=true. Honor Retry-After, use exponential backoff with jitter, limit attempts and cap the overall workflow duration. If the instructed wait exceeds your interactive budget, schedule a retry rather than shorten the wait.
For a network exception, retry the same stored body and Idempotency-Key. Preserve both across restarts. Stop on an idempotent replay of a stored error and reconcile the result. 409 idempotency_conflict requires an integration fix; 409 idempotency_in_progress, 429, 503 and 504 may be retried under these rules.
Record only the request ID and public error code in ordinary logs. Do not log request bodies, API keys, wallet values or provider payloads. Executable examples follow these rules.
Error reference
Public envelope
{"request_id":"merchant-order-1001-attempt-1","error":{"code":"provider_unavailable","message":"Transaction data is temporarily unavailable","retryable":true}}
The response header X-Request-ID identifies the same response. Branch on the stable code and retryable; do not parse the human-readable message. Gateway failures may not have this envelope: treat an invalid or missing envelope as an unresolved transport outcome and preserve the original idempotency key.
| HTTP | Codes | Action |
|---|---|---|
| 401 | invalid_api_key |
Correct server credentials; do not retry blindly. |
| 403 | tenant_suspended, network_not_enabled |
Resolve access with your organization. |
| 409 | idempotency_conflict |
Fix the operation/body association; nonretryable. |
| 409 | idempotency_in_progress |
Wait for Retry-After and retry the same operation. |
| 422 | malformed_tx_hash, invalid_network, invalid_tron_address, invalid_expected_amount, invalid_idempotency_key, payload_too_large, validation_error |
Correct the invalid request. |
| 429 | rate_limit_exceeded, quota_exceeded, concurrency_limit_exceeded |
Honor Retry-After; reduce concurrency or wait for quota reset. |
| 503 | provider_unavailable, rate_limiter_unavailable, api_disabled, api_not_configured |
Retry only when retryable and within your retry budget; retain the same key. |
| 504 | request_timeout |
Retry the same key and body after the instructed delay. |
| 500 | internal_error |
Use the bounded retry policy and contact support with X-Request-ID. |
Check Idempotent-Replayed: a stored error will not refresh itself on replay. See idempotency. Never assume that an HTTP error means no request was accepted.
Security recommendations
Protect credentials
Call the API from your backend. Store API keys in a secret manager, limit access to the integration service and rotate after suspected exposure. Never paste a key into a support ticket, URL, issue tracker, analytics event, browser console or frontend bundle.
Keep your identity provider protected with its available strong authentication policies. Accept invitations only from a trusted organization administrator. Sign out on shared devices; use Organization settings to sign out everywhere.
Handle data deliberately
Validate incoming payment requests on your server. Bind checks to your own payment-operation IDs and reconcile ambiguous outcomes. Use exact token contracts and decimal strings, and handle null and unknown states explicitly. Escape organization names, key labels and response values before displaying them in your own UI.
Retain only data your workflow needs. Restrict access to response details. Send support the response X-Request-ID and public error code; omit keys, balances and raw payloads.
API reference
Download OpenAPI. Field names and types below come directly from the public API contract. A required field may still accept null when the type allows it. Read the guides above for each field’s meaning.
POST /v1/transaction/check
Verify one TRON transaction. Send a JSON body and your server’s X-API-Key. Use Idempotency-Key to retry an operation safely.
| HTTP | Response |
|---|---|
| 200 | Result returned. Inspect the business state and data coverage. |
| 401 | API key is invalid or has been revoked. |
| 403 | Your organization or network does not have access. |
| 409 | The operation is still running, or the key was reused with a different body. |
| 422 | Correct the request fields. |
| 429 | A rate, concurrency or monthly quota limit was reached. |
| 503 | API access or a required service is unavailable. |
| 504 | The request exceeded the server time limit. |
| 500 | Internal error. Retain X-Request-ID for support. |
GET /v1/usage
Read your organization’s limits and usage. The optional days parameter accepts 1-90 and defaults to 30.
| HTTP | Response |
|---|---|
| 200 | Result returned. Inspect the business state and data coverage. |
| 422 | Correct the request fields. |
| 401 | API key is invalid or has been revoked. |
| 403 | Your organization or network does not have access. |
| 429 | A rate, concurrency or monthly quota limit was reached. |
| 500 | Internal error. Retain X-Request-ID for support. |
| 503 | API access or a required service is unavailable. |
| 504 | The request exceeded the server time limit. |
ErrorDetail
| Field | Type / values | Required |
|---|---|---|
code |
string |
Yes |
message |
string |
Yes |
retryable |
boolean |
Yes |
ErrorResponse
| Field | Type / values | Required |
|---|---|---|
request_id |
string |
Yes |
error |
ErrorDetail |
Yes |
ExposureResponse
| Field | Type / values | Required |
|---|---|---|
category |
string |
Yes |
exposure_type |
string |
Yes |
hops |
integer |
Yes |
percent |
string / null |
Yes |
FreshnessResponse
| Field | Type / values | Required |
|---|---|---|
checked_at |
string |
Yes |
transaction_at |
string / null |
Yes |
risk_data_at |
string / null |
Yes |
token_data_at |
string / null |
Yes |
max_source_age_seconds |
integer |
Yes |
ReasonResponse
| Field | Type / values | Required |
|---|---|---|
code |
string |
Yes |
category |
"transaction", "risk", "sanctions", "token", "expected" |
Yes |
severity |
"info", "warning", "high", "critical" |
Yes |
TransactionCheckRequest
| Field | Type / values | Required |
|---|---|---|
tx_hash |
string |
Yes |
network |
"tron" |
Yes |
expected_recipient |
string / null |
No |
expected_token_contract |
string / null |
No |
expected_amount |
string / null |
No |
TransactionCheckResponse
| Field | Type / values | Required |
|---|---|---|
request_id |
string |
Yes |
tx_hash |
string |
Yes |
network |
"tron" |
Yes |
transaction_type |
string |
Yes |
transaction_category |
"trx_transfer", "trc10_transfer", "trc20_transfer", "smart_contract_call", "smart_contract_deploy", "staking", "voting", "governance", "resource_operation", "account_operation", "exchange", "market", "permission_update", "other", "unknown" |
Yes |
transaction_state |
"confirmed", "pending", "failed", "not_found", "unknown" |
Yes |
confirmations |
integer / null |
Yes |
required_confirmations |
integer |
Yes |
sender |
string / null |
Yes |
recipient |
string / null |
Yes |
token_contract |
string / null |
Yes |
token_symbol |
string / null |
Yes |
decimals |
integer / null |
Yes |
amount |
string / null |
Yes |
transfers |
TransferResponse[] |
Yes |
transfers_truncated |
boolean |
Yes |
aml_risk_level |
"low", "moderate", "high", "severe", "unknown" |
Yes |
sanctions_hit |
boolean / null |
Yes |
key_exposure |
ExposureResponse[] |
Yes |
token_authenticity |
"official", "fake", "unknown" |
Yes |
recipient_match |
boolean / null |
Yes |
token_match |
boolean / null |
Yes |
amount_match |
boolean / null |
Yes |
coverage |
"complete", "partial" |
Yes |
freshness |
FreshnessResponse |
Yes |
overall_state |
"CLEAR", "REVIEW", "HIGH_RISK", "LIMITED_DATA" |
Yes |
reasons |
ReasonResponse[] |
Yes |
TransferResponse
| Field | Type / values | Required |
|---|---|---|
index |
integer |
Yes |
asset_type |
string |
Yes |
sender |
string / null |
Yes |
recipient |
string / null |
Yes |
token_contract |
string / null |
Yes |
token_id |
string / null |
Yes |
token_symbol |
string / null |
Yes |
decimals |
integer / null |
Yes |
amount |
string / null |
Yes |
UsageLimitsResponse
| Field | Type / values | Required |
|---|---|---|
rate_limit_per_minute |
integer |
Yes |
max_concurrent_requests |
integer |
Yes |
enabled_networks |
string[] |
Yes |
UsagePeriodResponse
| Field | Type / values | Required |
|---|---|---|
start |
string |
Yes |
end |
string |
Yes |
used |
integer |
Yes |
reserved |
integer |
Yes |
limit |
integer |
Yes |
remaining |
integer |
Yes |
UsagePlanResponse
| Field | Type / values | Required |
|---|---|---|
code |
string |
Yes |
version |
integer |
Yes |
name |
string |
Yes |
UsageRecentResponse
| Field | Type / values | Required |
|---|---|---|
days |
integer |
Yes |
requests |
integer |
Yes |
billable |
integer |
Yes |
limited_data |
integer |
Yes |
errors |
integer |
Yes |
average_latency_ms |
integer / null |
Yes |
by_network |
object |
Yes |
by_overall_state |
object |
Yes |
UsageResponse
| Field | Type / values | Required |
|---|---|---|
tenant_id |
string |
Yes |
plan |
UsagePlanResponse / null |
Yes |
limits |
UsageLimitsResponse |
Yes |
period |
UsagePeriodResponse |
Yes |
recent |
UsageRecentResponse |
Yes |
Changelog and versioning
v1.0.0
TRON transaction checks and organization usage reporting. The developer portal provides access by invitation, API key management, plan details and a page for testing requests within your organization.
Ethereum and TON remain planned and are not accepted by the public request schema. No wallet API, webhooks, monitoring or payment execution is part of this version.
Contract evolution
The public endpoint prefix is /v1. This documentation is versioned at /docs/v1/. The downloadable OpenAPI is generated from the same request and response models as the API and checked against a committed snapshot in CI.
Integrations should tolerate additional response fields while validating the fields they consume. Do not assume an unknown reason code means a clean result. Breaking changes require a versioned migration guide; documentation links for the existing major version remain stable.
Service status and support
Access status
The public status indicator reports whether business access is enabled. It does not run a live provider check and is not an uptime guarantee. An available network describes supported functionality; individual organizations may have narrower entitlements.
A 503 api_disabled response means business checks have not been enabled or have been deliberately paused. A 503 provider_unavailable response means required transaction data could not be retrieved for that request. Preserve the request ID and follow the public retry guidance.
Contact
Contact the ChainZap team for invitations, commercial terms, incident questions and support. Include X-Request-ID, the public error code and the UTC time. Never include API keys, identity tokens or raw transaction/provider payloads.
Networks: TRON: supported. Ethereum: planned. TON: planned.
Loading…