Skip to content

POST /apiv2/screening ​

Order an AML screening for a blockchain address: one response shape for every provider and every state of the order, decimal numbers as strings, and a single error format.

Endpoint URL ​

POST https://netts.io/apiv2/screening

Request Headers ​

HeaderRequiredDescription
Content-TypeYesapplication/json
X-API-KEYYesYour API key from the Netts dashboard
X-Idempotency-KeyNoYour own key for safe retries. See Idempotency

Request Body ​

json
{
    "address": "YOUR_ADDRESS_HERE",
    "network": "trx",
    "provider": "elliptic",
    "wait_for_result": true
}

Parameters ​

ParameterTypeRequiredDescription
addressstringYesAddress to screen, 10–128 characters
networkstringYesNetwork ticker. The tickers each provider covers are listed by GET /apiv2/screening/providers
providerstringYeselliptic or bitok. There is no default
wait_for_resultbooleanNotrue waits for the result for up to 15 seconds. Default false
languagestringNoReport language. Only en

Unknown fields are rejected. A body carrying a field that is not in the table above returns 400 with code 4001.

provider is required and has no default. You always choose the provider you pay for.

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 ​

bash
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 ​

bash
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 ​

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 ​

SituationCodeHeaders
Order created, screening runs in the background202 AcceptedLocation: /apiv2/screening/{client_order_id}
Result is in the response (wait_for_result)200 OK—
Result reused from a recent check, nothing charged200 OK—
Address has no blockchain activity, nothing charged200 OK—
Errorsee ErrorsContent-Type: application/problem+json

Responses that carry a screening result are sent with Cache-Control: private, no-store.

Response ​

json
{
  "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_data is 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 ​

FieldTypeDescription
client_order_idstringOrder identifier, used to read the result later
statusstringpending, processing, completed, skipped, failed
api_versionstringContract that created the order
cache_hitbooleantrue when a recent result was reused and nothing was charged
created_atstringRFC 3339, UTC, microseconds
started_atstring | nullWhen the provider call started. null for skipped
completed_atstring | nullWhen the result arrived
errorstringOnly for failed: why it failed
reasonstringOnly for skipped: address_inactive

All timestamps are UTC, RFC 3339, with a Z suffix and microsecond precision.

billing ​

FieldTypeDescription
chargedbooleanWhether money was taken
price_usdtstringList price of the provider in USDT
base_amountstringOrder price in the charged currency, without sub-user markup
markup_amountstringSub-user markup. "0" for a direct account
charged_amountstringWhat was actually taken from the balance
charged_currencystringTRX
exchange_ratestring | nullRate used for the conversion
payment_statusstringpaid, pending, failed, not_charged, postpaid

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. postpaid — see Postpaid Accounts.

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.

FieldTypeDescription
activity_checkedbooleanWhether the check ran. false on networks where it does not exist
activity_statusstringactive, inactive, unknown
sourcestring | nullName of the mechanism

unknown does not stop the paid screening: if the activity service is unavailable the address is treated as active.

check ​

FieldTypeDescription
providerstringProvider that performed the check
provider_check_idstring | nullThe provider's own identifier — quote it when disputing a result with them
checked_atstring | nullWhen the provider produced the result
statusstringSee the table below
provider_statusstring | nullThe provider's own wording, unmodified
order.statuscheck.statusMeaning
pendingpendingOrder accepted, not started yet
processingrunningThe provider is working on it
completedcompletedResult received
failedfailedRefused before or during the provider call
skippednot_performedThe address has no activity; the provider was never called and nothing was charged

risk ​

FieldTypeDescription
scorestring | nullThe provider's own score, as a decimal string
scaleobjectmin and max of that provider's scale
levelstringnone, low, medium, high, severe
level_sourcestringcomputed — we derived the level from the score; provider — the provider stated it
provider_levelstring | nullThe provider's own word, when it returns one
policystringName of the threshold policy, netts-risk-v1
by_directionobjectScore 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".

text
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.

json
"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.

SituationCodeResponse
The first request with this key is still running4094090
The same key, a different request body4094093
The same key, the same body, already finishedthe stored codethe 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.

Errors ​

Every error raised by the application uses RFC 9457 with Content-Type: application/problem+json:

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 an extension: match your error handling on it. Extra fields depend on the error and must be ignored when you do not know them.

CodeHTTPMeaning
4000400Body is not valid JSON
4001400A field failed validation, or an unknown field was sent
4002403The provider is not open for your account
4003400Malformed order identifier
4004400The provider does not support the requested network
4010401No API key
4011401API key or IP address not accepted
4040404Order not found
4041404Account not found
4090409A request with this idempotency key is still running
4091409Duplicate request
4093409This idempotency key was used with a different body
1004403Not enough balance
5000500Internal error
5001500The charge did not go through
5002500The order was not created
5030503The 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:

SituationHTTPBody
No API key, or a key that is not accepted401{"detail":{"code":-1,"msg":"Invalid or missing API key"}}
Rate limit exceeded429{"message":"API rate limit exceeded"}
Unknown path, or a method the route does not serve404 / 405{"detail":"Method Not Allowed"}

Rate Limits ​

5 requests per second and 150 per minute, shared by all screening paths.

Postpaid Accounts ​

Accounts on postpaid billing use the same endpoint, URL, headers and request body as everyone else. The difference is how the check is paid:

  • Nothing is deducted from your deposit. The price of each completed check in US dollars (for example, $0.98 for Elliptic) is added to your account balance due in the settlement currency agreed in your contract. You pay it by invoice, according to your agreement.
  • Your account has a credit limit. While the amount due is within the limit, checks are accepted even with a zero deposit.
  • Only completed checks are billed. A check that failed, an address with no activity (skipped) and a reused recent result cost nothing.

Postpaid billing is enabled by Netts for an account on request.

billing of a postpaid check ​

json
"billing": {
    "charged": false,
    "price_usdt": "0.98",
    "base_amount": "2.960725",
    "markup_amount": "0",
    "charged_amount": "2.960725",
    "charged_currency": "TRX",
    "exchange_rate": "0.33100000",
    "payment_status": "postpaid"
}
FieldDescription
chargedfalse: nothing is taken from the deposit
price_usdtPrice of the check in US dollars. This is the amount added to your balance due
charged_amountThe same price in TRX, for reference
payment_statuspostpaid. For a failed or skipped check — not_charged

Credit limit reached (402) ​

json
{
  "type": "https://doc.netts.io/api/v2/errors/credit-limit-reached",
  "title": "Postpaid credit limit reached",
  "status": 402,
  "detail": "Postpaid credit limit reached.",
  "instance": "/apiv2/screening",
  "code": 1004,
  "payment_mode": "postpaid",
  "reason": "limit_reached"
}

The check is not ordered and nothing is billed. New checks are accepted again once your payment is registered or your limit is increased.

Postpaid error codes ​

CodeHTTPDescription
1004402Postpaid credit limit reached
5032503Price temporarily unavailable
5034503Billing temporarily unavailable — the check was not accepted
5036503Credit check temporarily unavailable — retry later

For all responses above nothing is ordered and nothing is billed.

Notes ​

  • Pricing: a fixed price per check for each provider, see GET /apiv2/screening/providers; 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 skipped and are not charged.
  • The raw provider response is never returned. provider_data is 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 ​