<!-- Generated from https://zscreen.zeruai.org/docs/endpoints by scripts/agent-docs.mjs. Do not edit. -->

# Endpoints

Every route, what it takes, and what comes back. All of them need a bearer key and all of them answer in JSON.

v2 is the API

Screening, polling and the report are all on `/v2`. The older `/v1` paths still answer and return the same body, so nothing built on them breaks, but they are not where new work goes. Three endpoints have no `/v2` address yet and are marked below: the batch screen, one permanent record, and a wallet's own history.

## Screen an address

v2 is the screening surface

Screening and its jobs are on `/v2`, where the number and the word always agree. Batch, the permanent record and wallet history have no v2 yet and stay on `/v1`. Both read the same stored row, so a wallet cannot be CLEAR on one door and queued on the other.

Takes one field, `address`. Returns `200` with the scored body when the address has a record, or `202` with a job id when its history still has to be fetched.

```
curl https://zscreenapi.zeruai.org/v2/screen \
  -H "Authorization: Bearer zsk_..." \
  -H "Content-Type: application/json" \
  -d '{"address":"0xd08a234c5215a8122c836187b23f60046a90afd8"}'
```

```
const res = await fetch("https://zscreenapi.zeruai.org/v2/screen", {
  method: "POST",
  headers: {
    "Authorization": "Bearer zsk_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ address: "0xd08a234c5215a8122c836187b23f60046a90afd8" }),
});
const data = await res.json();
```

Identical behaviour with the address in the path. Convenient from a browser or a shell, and it is a read, so it is safe to retry.

```
curl https://zscreenapi.zeruai.org/v2/screen/0xd08a234c5215a8122c836187b23f60046a90afd8 \
  -H "Authorization: Bearer zsk_..."
```

```
const res = await fetch("https://zscreenapi.zeruai.org/v2/screen/0xd08a234c5215a8122c836187b23f60046a90afd8", {
  headers: { "Authorization": "Bearer zsk_..." },
});
const data = await res.json();
```

Both are billable

A screen spends one unit of your allowance whether it comes back scored or queued, and whether it was computed now or served from the stored record. Reading a record you already have, through the two routes below, costs nothing.

## Screen a batch

Takes `addresses`, an array of 1 to 100. Each one is screened in turn and sorted into one of three lists, so a malformed address in position 4 does not cost you the other 99 answers.

```
curl https://zscreenapi.zeruai.org/v1/screen/batch \
  -H "Authorization: Bearer zsk_..." \
  -H "Content-Type: application/json" \
  -d '{"addresses":["0xd08a234c5215a8122c836187b23f60046a90afd8","0x97b1043abd9e6fc31681635166d430a458d14f9c"]}'
```

```
const res = await fetch("https://zscreenapi.zeruai.org/v1/screen/batch", {
  method: "POST",
  headers: {
    "Authorization": "Bearer zsk_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ addresses: ["0xd08a234c5215a8122c836187b23f60046a90afd8", "0x97b1043abd9e6fc31681635166d430a458d14f9c"] }),
});
const { results, queued, errors } = await res.json();
```

```
{
  "results": [ { "address": "0xd08a234c5215a8122c836187b23f60046a90afd8", "verdict": "REVIEW", "...": "..." } ],
  "queued":  [ { "address": "0x1f39...7125", "status": "queued", "job_id": 90114 } ],
  "errors":  [ { "address": "0xnope", "error": "address must be 0x followed by 40 hex characters" } ],
  "counts":  { "scored": 1, "queued": 1, "failed": 1 }
}
```

If the allowance runs out partway through, everything already screened stands and every address after it is reported in `errors` as not screened. No verdict is ever invented to fill a gap.

Batch is key-only: it never takes payment per screen. A spent allowance is told where a single screen can be bought instead.

## The full report

A screen is built to be read in two seconds: a word, a number, one reason. The report is the same answer for somebody who has to file the wallet, with money in and out, the counterparties behind it, how close it sits to a listed address, the patterns it shows, and three paragraphs of prose.

```
curl https://zscreenapi.zeruai.org/v2/report/0xd08a234c5215a8122c836187b23f60046a90afd8 \
  -H "Authorization: Bearer zsk_..."
```

It shares its serving path with the screen, so the verdict can never disagree, and the three answers are the same three. Field by field, it is on [its own page](https://zscreen.zeruai.org/docs/report).

## Check a job

`state` moves through `queued`, `running`, and then either `done` or `failed`. Once it is `done`, the full scored body is inside `result`, so a successful poll ends the exchange with no second call.

```
const res = await fetch("https://zscreenapi.zeruai.org/v2/job/90114", {
  headers: { "Authorization": "Bearer zsk_..." },
});
const job = await res.json();
if (job.state === "done") console.log(job.result.verdict);
```

```
{
  "job_id": 90114,
  "address": "0x1f39eb3896756172728a2cfb116a6ec37d807125",
  "state": "done",
  "attempts": 1,
  "error": null,
  "created_at": "2026-09-01T09:12:40Z",
  "finished_at": "2026-09-01T09:14:22Z",
  "result": { "verdict": "CLEAR", "risk_score": 4.1, "...": "the v2 screen body" }
}
```

A `failed` job carries the reason in `error` and no result. It is a failure to gather data, not a finding about the wallet.

## Records and history

Every scored response is written down and handed back an id. This route returns that record byte for byte, which is what makes a decision you made last quarter reviewable this quarter.

Records are scoped to the key that created them. A record belonging to another customer answers `404` rather than `403`, because which ids exist is not yours to learn either.

```
curl https://zscreenapi.zeruai.org/v1/screening/418822 \
  -H "Authorization: Bearer zsk_..."
```

```
const res = await fetch("https://zscreenapi.zeruai.org/v1/screening/418822", {
  headers: { "Authorization": "Bearer zsk_..." },
});
const record = await res.json();
```

Every check your key has run against one address, newest first. Takes an optional `limit`, default 50 and capped at 500. Scoped the same way: one customer's search history is not another customer's business.

## Operational

These three need no key. `/healthz` answers as soon as the process is running; `/readyz` answers only once its dependencies are reachable, which is the one to put in front of a load balancer.

Status codes, rate limits and quota behaviour are on [Limits and errors](https://zscreen.zeruai.org/docs/limits).
