POST /apiv2/screening
Order an AML screening for a blockchain address. This is the version 2 contract: one response shape for every provider and every state of the order, decimal numbers as strings, and a single error format.
It replaces POST /apiv2/aml, which keeps working and is not going away without notice.
Endpoint URL
POST https://netts.io/apiv2/screeningRequest Headers
| Header | Required | Description |
|---|---|---|
| Content-Type | Yes | application/json |
| X-API-KEY | Yes | Your API key from the Netts dashboard |
| X-Idempotency-Key | No | Your own key for safe retries. See Idempotency |
Request Body
{
"address": "YOUR_ADDRESS_HERE",
"network": "trx",
"provider": "elliptic",
"wait_for_result": true
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| address | string | Yes | Address to screen, 10–128 characters |
| network | string | Yes | Network ticker. The tickers each provider covers are listed by GET /apiv2/screening/providers; the full table of networks with their names is here |
| provider | string | Yes | elliptic or bitok. There is no default |
| wait_for_result | boolean | No | true waits for the result for up to 15 seconds. Default false |
| language | string | No | Report language. Only en |
Unknown fields are rejected. A body carrying a field that is not in the table above returns 400 with code 4001. In version 1 unknown fields were silently ignored, and a misspelled wait meant the caller waited for a result that was never going to arrive synchronously.
provider is required and has no default. In version 1 an omitted provider meant Elliptic, so a caller who did not choose paid for a provider they never named.
provider is a free-form string in the schema, not an enumeration. Today two values are accepted; a third provider must not be a breaking change for anyone who validates responses against the schema. The current list, the networks each provider covers and the scale each one scores on come from GET /apiv2/screening/providers.
Example Requests
cURL — wait for the result
curl -X POST https://netts.io/apiv2/screening \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-d '{
"address": "YOUR_ADDRESS_HERE",
"network": "trx",
"provider": "elliptic",
"wait_for_result": true
}'cURL — accept and poll
curl -X POST https://netts.io/apiv2/screening \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-d '{
"address": "YOUR_ADDRESS_HERE",
"network": "trx",
"provider": "bitok"
}'The response is 202 Accepted with a Location header pointing at the order.
Python
import requests
from decimal import Decimal
url = "https://netts.io/apiv2/screening"
headers = {
"Content-Type": "application/json",
"X-API-KEY": "your_api_key",
}
payload = {
"address": "YOUR_ADDRESS_HERE",
"network": "trx",
"provider": "elliptic",
"wait_for_result": True,
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code in (200, 202):
body = response.json()
print("Order:", body["order"]["client_order_id"])
print("Status:", body["order"]["status"])
if body["risk"]["score"] is not None:
# Parse provider numbers as Decimal, never as float
print("Score:", Decimal(body["risk"]["score"]), "of", body["risk"]["scale"]["max"])
print("Level:", body["risk"]["level"], "-", body["risk"]["level_source"])
print("Charged:", body["billing"]["charged_amount"], body["billing"]["charged_currency"])
else:
problem = response.json()
print(problem["code"], problem["title"], "-", problem["detail"])Response Codes
| Situation | Code | Headers |
|---|---|---|
| Order created, screening runs in the background | 202 Accepted | Location: /apiv2/screening/{client_order_id} |
Result is in the response (wait_for_result) | 200 OK | — |
| Result reused from a recent check, nothing charged | 200 OK | — |
| Address has no blockchain activity, nothing charged | 200 OK | — |
| Error | see Errors | Content-Type: application/problem+json |
Responses that carry a screening result are sent with Cache-Control: private, no-store.
Response
{
"schema_version": 2,
"order": {
"client_order_id": "A90D21F68C9AEA2",
"status": "completed",
"api_version": "v2",
"cache_hit": false,
"created_at": "2026-09-13T08:14:29.614988Z",
"started_at": "2026-09-13T08:14:30.288251Z",
"completed_at": "2026-09-13T08:14:34.993174Z"
},
"request": {
"address": "YOUR_ADDRESS_HERE",
"network": "trx",
"provider": "elliptic"
},
"billing": {
"charged": true,
"price_usdt": "0.98",
"base_amount": "2.882421",
"markup_amount": "0",
"charged_amount": "2.882421",
"charged_currency": "TRX",
"exchange_rate": "0.33999200",
"payment_status": "pending"
},
"precheck": {
"activity_checked": true,
"activity_status": "active",
"source": "tron-address-checker"
},
"check": {
"provider": "elliptic",
"provider_check_id": "b4903a5d-9f22-4075-b51b-beb40b419b97",
"checked_at": "2026-09-13T08:14:32.144000Z",
"status": "completed",
"provider_status": "complete"
},
"risk": {
"score": "0.9634087310611608",
"scale": { "min": 0, "max": 10 },
"level": "low",
"level_source": "computed",
"provider_level": null,
"policy": "netts-risk-v1",
"by_direction": {
"source": "0.12332156899576921",
"destination": "0.9634087310611608"
}
},
"sanctions": {
"self": false,
"self_entities": null,
"exposure": {
"entity": "HTX (formerly Huobi) - Post-Sanctions - UK Sanctions List - 26 May 2026",
"category": "Exchange",
"share_fraction": "0.004409451392423414",
"proximity": "indirect",
"hops": 3,
"direction": "source",
"rule_name": "Sanctioned, TF & CSAM"
},
"items": []
},
"exposure": [
{
"category": "Exchange",
"share_fraction": "0.1888065152442461",
"direction": "source",
"hops": 2,
"is_screened_address": false,
"entities": [
{ "name": "Binance", "category": "Exchange", "is_vasp": true,
"is_primary": true, "is_after_sanction_date": null }
]
}
],
"rules": [
{ "name": "Sanctioned, TF & CSAM", "type": "exposure", "level": null,
"score": "0.061426483114820525", "share_fraction": null, "category": null,
"proximity": null, "direction": "source", "detected_at": null }
],
"entities": [
{ "name": "Unknown", "category": "Unknown", "is_vasp": null,
"is_primary": true, "is_after_sanction_date": false }
],
"primary_entity": {
"name": "Unknown", "category": "Unknown", "is_vasp": null,
"is_primary": true, "is_after_sanction_date": false
},
"sanctioned_entities": [],
"wallet": {
"inflow_usd": "354663.6116669158",
"outflow_usd": "80248.63081808022"
},
"provider_data": { }
}The field set never changes
Every block listed above is present in every response, whatever the provider and whatever the state of the order. What a provider does not supply is null; a list that has nothing in it is [], not null; a block that has no data yet is filled with nulls rather than omitted. One parser handles a check that has just been accepted and the same check once it is finished.
Two consequences for your code:
- ignore fields you do not know. New fields are added to these blocks without a new version. Rejecting an unknown field is your bug, not ours;
provider_datais not part of the contract. Its shape follows the provider, and it changes when the provider changes. Everything the contract guarantees lives in the blocks above.
order
| Field | Type | Description |
|---|---|---|
| client_order_id | string | Order identifier, used to read the result later |
| status | string | pending, processing, completed, skipped, failed |
| api_version | string | Contract that created the order |
| cache_hit | boolean | true when a recent result was reused and nothing was charged |
| created_at | string | RFC 3339, UTC, microseconds |
| started_at | string | null | When the provider call started. null for skipped |
| completed_at | string | null | When the result arrived |
| error | string | Only for failed: why it failed |
| reason | string | Only for skipped: address_inactive |
All timestamps are UTC, RFC 3339, with a Z suffix and microsecond precision.
billing
| Field | Type | Description |
|---|---|---|
| charged | boolean | Whether money was taken |
| price_usdt | string | List price of the provider in USDT |
| base_amount | string | Order price in the charged currency, without sub-user markup |
| markup_amount | string | Sub-user markup. "0" for a direct account |
| charged_amount | string | What was actually taken from the balance |
| charged_currency | string | TRX |
| exchange_rate | string | null | Rate used for the conversion |
| payment_status | string | paid, pending, failed, not_charged |
payment_status is pending for a short time after a successful check: the charge is held first and settles within the hour. failed means the money was returned. not_charged means no charge was ever created — a reused result or a skipped address.
precheck
Before a paid screening the address is checked for blockchain activity. An address with no activity is not sent to the provider and is not charged.
| Field | Type | Description |
|---|---|---|
| activity_checked | boolean | Whether the check ran. false on networks where it does not exist |
| activity_status | string | active, inactive, unknown |
| source | string | null | Name of the mechanism |
unknown does not stop the paid screening: if the activity service is unavailable the address is treated as active.
check
| Field | Type | Description |
|---|---|---|
| provider | string | Provider that performed the check |
| provider_check_id | string | null | The provider's own identifier — quote it when disputing a result with them |
| checked_at | string | null | When the provider produced the result |
| status | string | See the table below |
| provider_status | string | null | The provider's own wording, unmodified |
order.status | check.status | Meaning |
|---|---|---|
pending | pending | Order accepted, not started yet |
processing | running | The provider is working on it |
completed | completed | Result received |
failed | failed | Refused before or during the provider call |
skipped | not_performed | The address has no activity; the provider was never called and nothing was charged |
risk
| Field | Type | Description |
|---|---|---|
| score | string | null | The provider's own score, as a decimal string |
| scale | object | min and max of that provider's scale |
| level | string | none, low, medium, high, severe |
| level_source | string | computed — we derived the level from the score; provider — the provider stated it |
| provider_level | string | null | The provider's own word, when it returns one |
| policy | string | Name of the threshold policy, netts-risk-v1 |
| by_direction | object | Score split into source and destination, when the provider splits it |
The score is never rescaled. Elliptic runs 0–10 and BitOK runs 0–1, and 7 on one scale is not 0.7 on the other in any meaningful sense. The scale arrives in the response so that an integration written against one provider does not misread another after a single configuration change.
The level is one vocabulary across the whole API. Where the provider states a level of its own we pass that through and say so in level_source; where it does not, we derive the level from the score with the netts-risk-v1 thresholds and say that instead. The same word appears in the API response, the dashboard and the PDF report for the same check.
exposure[], rules[], entities[]
exposure[] breaks the funds down by counterparty category. rules[] lists the provider's rules that fired. entities[] lists the entities the address itself belongs to; primary_entity picks one of them by a fixed rule — the entity the provider marked as primary, otherwise the first one, otherwise null. sanctioned_entities[] holds those of entities[] that are flagged as active after a sanctions date.
Shares are fractions, never percentages
Every share in the response is a single field, share_fraction, a decimal string between "0" and "1".
Elliptic reports 31.574732212596924 % -> "share_fraction": "0.31574732212596924"
BitOK reports 0.8488 -> "share_fraction": "0.8488"The providers disagree on units: the same one third of exposure arrives as 31.57 from one and 0.3157 from the other. A single field carrying both would be impossible to read without knowing the provider. The provider's own number in the provider's own units stays in provider_data.
Numbers are strings
Every number that comes from a provider — scores, shares, USD volumes, and every amount in billing — is a decimal string.
"score": "0.9634087310611608"Parsing that as a JSON number in JavaScript, Go or any other language with binary floating point gives back an approximation, and the value you print stops matching the value the provider issued. Parse these fields with a decimal type: Decimal in Python, BigDecimal in Java, decimal.Decimal or a string in JavaScript.
Fields that are ours rather than the provider's — scale.min, scale.max, hops — are plain JSON numbers.
Reusing a recent result
When you screen the same address, network and provider again within 60 seconds, the earlier result is returned and nothing is charged.
Each request still creates its own order with its own client_order_id; the reused one is marked "cache_hit": true and its billing block reports "charged": false with "payment_status": "not_charged". The identifier of the order the result came from is not disclosed — it may belong to another account.
Reuse only happens within one account. A result screened by somebody else is never returned to you.
Idempotency
Send X-Idempotency-Key with a value of your own to make a retry safe: the same key with the same body returns the stored response instead of ordering a second check.
| Situation | Code | Response |
|---|---|---|
| The first request with this key is still running | 409 | 4090 |
| The same key, a different request body | 409 | 4093 |
| The same key, the same body, already finished | the stored code | the stored response |
If you do not send the header, a key is generated for you from the API key, the address, the provider and your IP address, in a two-second window. It protects against a double click and a gateway retry, not against a repeat a minute later: that one is a genuine new order and is charged.
Keys are scoped per endpoint. The same value sent to POST /apiv2/aml and to this endpoint is two independent promises about two different requests — the bodies differ, and so do the responses. Reusing your key while moving an integration from version 1 to version 2 is safe: it neither returns you a version 1 response nor counts as the same key used with a different body.
Errors
Every error raised by the application uses RFC 9457 with Content-Type: application/problem+json:
{
"type": "https://doc.netts.io/api/v2/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 403,
"detail": "Insufficient funds. Required: 2.888742 TRX, Available: 1.203000 TRX",
"instance": "/apiv2/screening",
"code": 1004,
"required_trx": "2.888742",
"available_trx": "1.203000"
}type, title, status, detail and instance are the standard fields. The numeric code is kept as an extension so that integrations written against version 1 can keep matching on it. Extra fields depend on the error and must be ignored when you do not know them.
| Code | HTTP | Meaning |
|---|---|---|
4000 | 400 | Body is not valid JSON |
4001 | 400 | A field failed validation, or an unknown field was sent |
4002 | 403 | The provider is not open for your account. BitOK is granted per account; ask support for access. Such a provider is not listed in providers or in the price list either |
4003 | 400 | Malformed order identifier |
4004 | 400 | The provider does not support the requested network |
4010 | 401 | No API key |
4011 | 401 | API key or IP address not accepted |
4040 | 404 | Order not found |
4041 | 404 | Account not found |
4090 | 409 | A request with this idempotency key is still running |
4091 | 409 | Duplicate request |
4093 | 409 | This idempotency key was used with a different body |
1004 | 403 | Not enough balance |
5000 | 500 | Internal error |
5001 | 500 | The charge did not go through |
5002 | 500 | The order was not created |
5030 | 503 | The provider is unavailable |
Errors that do not use this format
Some failures happen at the gateway, before the application is reached, and they keep the gateway's own shape. Treat any response whose Content-Type is not application/problem+json as one of these:
| Situation | HTTP | Body |
|---|---|---|
| No API key, or a key that is not accepted | 401 | {"detail":{"code":-1,"msg":"Invalid or missing API key"}} |
| Rate limit exceeded | 429 | {"message":"API rate limit exceeded"} |
| Unknown path, or a method the route does not serve | 404 / 405 | {"detail":"Method Not Allowed"} |
Rate Limits
The limit is shared with POST /apiv2/aml and the other AML paths: 5 requests per second and 150 per minute. Moving to this endpoint does not give you a second allowance.
Notes
- Pricing: Elliptic $0.98, BitOK $0.50 per check, charged from the TRX balance at the rate at the moment of the charge.
- Processing time: most checks finish in a few seconds; an address with a long history can take up to three minutes. Use the asynchronous mode and read the result with GET /apiv2/screening/{client_order_id}.
- Inactive addresses return
skippedand are not charged. - The raw provider response is never returned.
provider_datais a reviewed projection; fields that belong to our account with the provider rather than to the screened address are not published to anyone.
Reports
The endpoint returns JSON and nothing else. There is no PDF and no Markdown.
Everything a report is made of is already in the response: the unified block and provider_data. Rendering it on your side gives you the document you actually want — your branding, your language, your layout — which is particularly the point if you resell checks, because a report carrying our name is the wrong document to hand your own customer.
If you need a report as evidence for a third party — a bank, a regulator, a counterparty — note that an unsigned PDF is not evidence whoever generates it: it can be edited in a text editor in a minute. A verifiable artifact needs a signature or a public verification page, and that is a different feature. If that is your case, tell us what your counterparty requires.
Human-readable PDF reports do exist for the same checks in the Netts dashboard, in seventeen languages.
See Also
- GET /apiv2/screening/{client_order_id} — read a check
- GET /apiv2/screening/history — your checks, cursor paginated
- GET /apiv2/screening/providers — providers, prices, networks, scales
- GET /apiv2/screening/price — price of one provider
- Sanctions in an AML result — what
sanctionssays and what the provider flag says