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 an EVM address screened against Ethereum and against BNB Chain are two separate questions with two potentially different answers.
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" means you have silently
chosen to accept 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.
What this endpoint is not¶
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 is not a payout gate. 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.
Where it is worth calling¶
The pattern that pays for itself is screening a destination 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 — if you need a fresh verdict for a decision of consequence,
note that you cannot force one from this endpoint.
Cache on your side too. Screening the same address on every page load is wasted work; 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.