Skip to content

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/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; the full table of networks with their names is here
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. 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

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

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.

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.

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:

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.

CodeHTTPMeaning
4000400Body is not valid JSON
4001400A field failed validation, or an unknown field was sent
4002403The 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
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

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