Quickstart
This page takes you from a sandbox key to a webhook delivery you have verified, using only the sandbox environment. Nothing here is billed. Each step links the full guide, so this page stays short.
1. Get a sandbox key
Keys are issued by Leeri on request: write to hello@leeri.ai and ask for a sandbox key with the checks:write, checks:read and webhooks:manage scopes. A sandbox key starts with ak_sandbox_. It runs the real screening engine against your own data at no charge and never uses quota or balance; see Sandbox and live.
Keep the key in your environment, never in source code:
export LEERI_API_KEY="ak_sandbox_..."
2. Run an address check
Send the designated test address. Under a sandbox key it always answers pending, so you can build your polling and webhook handling on day one. Send an Idempotency-Key with every POST so a retry never screens twice; see Idempotency.
- curl
- Node.js
- Python
curl -s -X POST https://api.leeri.ai/v1/checks \
-H "Authorization: Bearer $LEERI_API_KEY" \
-H "Idempotency-Key: quickstart-1" \
-H "Content-Type: application/json" \
-d '{"subject":"address","asset":"BTC","address":"1PENDNGCHECKADDRESSFRSANDBXTEST99"}'
const response = await fetch('https://api.leeri.ai/v1/checks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LEERI_API_KEY}`,
'Idempotency-Key': 'quickstart-1',
'Content-Type': 'application/json',
},
body: JSON.stringify({ subject: 'address', asset: 'BTC', address: '1PENDNGCHECKADDRESSFRSANDBXTEST99' }),
});
const { data } = await response.json();
console.log(data.status, data.check_id); // "pending" and the id to poll
import os
import requests
response = requests.post(
"https://api.leeri.ai/v1/checks",
headers={
"Authorization": f"Bearer {os.environ['LEERI_API_KEY']}",
"Idempotency-Key": "quickstart-1",
},
json={"subject": "address", "asset": "BTC", "address": "1PENDNGCHECKADDRESSFRSANDBXTEST99"},
timeout=30,
)
data = response.json()["data"]
print(data["status"], data["check_id"]) # "pending" and the id to poll
The answer has status: "pending" and a check_id. Every check, pending or not, has the same shape; see Address checks for every field.
3. Read the verdict
Poll the check until its status leaves pending. Wait until next_attempt_at between polls instead of looping; nothing changes before it. See Pending and polling.
- curl
- Node.js
- Python
curl -s https://api.leeri.ai/v1/checks/$CHECK_ID \
-H "Authorization: Bearer $LEERI_API_KEY"
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let check;
do {
const response = await fetch(`https://api.leeri.ai/v1/checks/${checkId}`, {
headers: { Authorization: `Bearer ${process.env.LEERI_API_KEY}` },
});
check = (await response.json()).data;
if (check.status === 'pending') {
const wait = check.next_attempt_at ? Date.parse(check.next_attempt_at) - Date.now() : 6000;
await sleep(Math.min(Math.max(wait, 6000), 30000));
}
} while (check.status === 'pending');
console.log(check.status, check.band, check.score);
import os
import time
from datetime import datetime, timezone
import requests
while True:
response = requests.get(
f"https://api.leeri.ai/v1/checks/{check_id}",
headers={"Authorization": f"Bearer {os.environ['LEERI_API_KEY']}"},
timeout=30,
)
check = response.json()["data"]
if check["status"] != "pending":
break
wait = 6.0
if check["next_attempt_at"]:
due = datetime.fromisoformat(check["next_attempt_at"].replace("Z", "+00:00"))
wait = (due - datetime.now(timezone.utc)).total_seconds()
time.sleep(min(max(wait, 6.0), 30.0))
print(check["status"], check["band"], check["score"])
completed and failed are both terminal: no field changes afterwards. On a completed check look at these first:
bandandscore. The band islow,medium,highorsevere; read it from the field instead of recomputing it from the score. See Score and band.findings. Each one names the rule that fired and links its source insource_url, either a public document or a methodology page; see Findings and the Methodology.partialandpartial_reasons. A check never fails with a server error because a data source is down; it says so instead. See How complete the verdict is.report. The evidence report of the verdict, included with the check; see Evidence reports.
4. Register a webhook
With a webhook you register an endpoint once and receive one signed check.completed delivery per finished check, so there is nothing to poll. The URL must be https. The response carries the signing secret once: store it in your secret manager straight away. If you lose it, rotate the endpoint. See Register an endpoint.
- curl
- Node.js
- Python
curl -s -X POST https://api.leeri.ai/v1/webhooks \
-H "Authorization: Bearer $LEERI_API_KEY" \
-H "Idempotency-Key: quickstart-webhook-1" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-service.example.com/webhooks/leeri"}'
const response = await fetch('https://api.leeri.ai/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LEERI_API_KEY}`,
'Idempotency-Key': 'quickstart-webhook-1',
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: 'https://your-service.example.com/webhooks/leeri' }),
});
const { data } = await response.json();
// data.secret is shown only in this response.
console.log(data.webhook_id, data.secret);
import os
import requests
response = requests.post(
"https://api.leeri.ai/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['LEERI_API_KEY']}",
"Idempotency-Key": "quickstart-webhook-1",
},
json={"url": "https://your-service.example.com/webhooks/leeri"},
timeout=30,
)
data = response.json()["data"]
# data["secret"] is shown only in this response.
print(data["webhook_id"], data["secret"])
5. Verify deliveries
Anyone who knows your URL can send it a request, so a delivery is trusted only when its x-aml-signature header verifies against the secret from step 4. Compute the HMAC over the raw request body, compare in constant time, and reject a stale timestamp. The working samples and a minimal receiver are in Verify the signature. Respond 2xx quickly and dedupe on event_id, because deliveries are at least once; see Delivery and retries.
Run another sandbox check with the same key and confirm your receiver verifies the delivery and answers 2xx.
Go live
When the steps above work, switch from sandbox to live by changing the key and nothing else. Before you do:
- Use an
ak_live_key, issued the same way as the sandbox key. Keep it in your secret store; see Authentication. - Send an
Idempotency-Keyon everyPOST; see Idempotency. - Handle
402,429and503as described in Errors, and respectRetry-After. - Stay inside the limits in Rate limits and quotas. Plans are on the pricing page.
- Register your webhook endpoint again under the live key: endpoints belong to one environment.
- Screen transactions as well, where you need them; see Transaction checks.
Moving from an AMLBot integration? See Migration from AMLBot: your existing client keeps working after you repoint three environment variables.