Skip to content

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 .../refund records 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.