Address screening¶
One endpoint, for checking an address against our AML provider before you send money to it.
curl -X POST https://api.pay.nullswap.com/api/v1/aml/check-address \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"address": "0xfedcba0987654321fedcba0987654321fedcba09",
"chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10"
}'
{
"amlResultId": "5c6d7e8f-9a0b-41c2-8d34-5e6f7a8b9c0d",
"merchantId": "9f1d2c3b-4a5e-4f60-8b71-2c3d4e5f6a7b",
"address": "0xfedcba0987654321fedcba0987654321fedcba09",
"chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
"status": "PASSED",
"score": 12,
"tags": [],
"checkedAt": "2026-01-14T09:00:00.000Z",
"fromCache": false
}
| Field | Type | Required | Notes |
|---|---|---|---|
address |
string | yes | The address to screen, in the chain's native format |
chainId |
uuid | yes | Which chain to interpret it on |
Both matter. The same string of characters is a different address on different chains, and the same EVM address can come back with different verdicts on Ethereum and on BNB Chain.
Reading the verdict¶
status |
Meaning |
|---|---|
PASSED |
Screened, and nothing disqualifying was found |
FAILED |
Screened, and flagged |
SKIPPED |
Not screened. The provider had no coverage, or screening did not apply |
SKIPPED is not PASSED
It is the absence of an answer, not a clean one. Whether you treat it as acceptable is a policy
decision — but it must be a decision. Writing status !== "FAILED" accepts every unscreenable
address.
| Field | Notes |
|---|---|
score |
Integer risk score. Higher is riskier. Meaningless when SKIPPED |
tags |
Provider labels, e.g. ["mixer", "sanctions"]. Empty on a clean result |
checkedAt |
When the screening ran — not when you called. Older than now on a cache hit |
fromCache |
Whether this reused a recent result rather than re-screening |
amlResultId |
Quote this if you want a verdict reviewed |
Do not build your own thresholds on score. Ours already sits behind status, and the provider's
scale is not something we hold fixed. Use status to decide and keep score and tags for your
audit log.
Limits of this endpoint¶
Calling this does not screen an incoming payment
Payments are screened automatically, on their own path, and the result lands on the payment's
amlStatus and amlResultId. This route is a separate, on-demand lookup that you drive. A
PASSED here has no effect on any invoice or payment record, and cannot release a WITHHELD
static-wallet deposit or turn a DIRTY balance CLEAN.
It also does not block payouts on its own. Nothing in
POST /api/v1/merchant/withdrawals or in a
refund consults it. If you want screening before you send
money, you have to call this yourself and act on the answer.
When to call it¶
The main use is screening a destination address before you commit to it:
flowchart TD
A["Customer submits a payout address"] --> B["POST /aml/check-address"]
B --> C{"status"}
C -- "PASSED" --> D["Save to the address book<br/>and proceed"]
C -- "FAILED" --> E["Refuse, and tell the customer<br/>the address was rejected"]
C -- "SKIPPED" --> F["Your policy decides"]
D --> G["POST /merchant/withdrawals"]
Two concrete cases:
- A refund address.
POST .../refundrecords a refund you already sent — you cannot take it back. Screen the address while the customer is still on the page. - A withdrawal destination. Screen it once when it is added to your address book rather than on every payout.
Caching and cost¶
Repeated checks of the same address inside the provider's freshness window come back with
fromCache: true and checkedAt pointing at the original run. That is cheaper and faster, but it
also means a result can be hours old, and this endpoint gives you no way to force a fresh one.
Cache on your side too. There is no need to screen the same address on every page load: screening it when it is first supplied, and again if it has gone stale by your own standard, is enough.
Errors¶
| Status | message |
Cause |
|---|---|---|
400 |
InvalidRequestBodyError |
address or chainId missing, or chainId is not a uuid |
403 |
MerchantIsBlockedError |
The merchant is blocked |
404 |
ChainWithIdNotFoundError |
Unknown chainId |
422 |
EmptyAMLAddressError |
address was present but empty or whitespace |
502 |
AMLServiceError |
The screening provider was unreachable or timed out |
A 502 is not a verdict
AMLServiceError means the question was never answered — it is not a quiet SKIPPED. Retry it,
and if the retry fails too, fall back to whatever you would do for an address you could not
check rather than to whatever you do for a clean one.