---
key: docsApi
path: /api
section: reference
priority: 0.8
title: API reference
description: The ZadQ HTTP API, compiled from its OpenAPI contract — a curl quickstart, the authentication schemes, every public operation with its parameters, request body and responses, and the schemas they use.
---

# API reference

The ZadQ API is plain HTTPS with JSON bodies at `https://api.zadq.net`. This page is
generated at build time from the service's OpenAPI contract, so every
operation, parameter and schema below is the contract itself, rendered.

The verification path is free and public: it needs no account and no
credential. Every integration keeps working when no verdict is available —
the answer is `unknown`, and what to do with it stays the caller's decision.

## Quickstart

Each operation in the reference carries a ready-to-run `curl` example; the
three calls below are the ones most integrations start with. Values in angle
brackets are placeholders to replace with your own.

**1. Read the substrate status.** Public, no credential: whether the identity
substrate behind the verdicts is `ok`, `degraded` or `unknown`, and when a
standing read last succeeded.

```bash
curl -sS "https://api.zadq.net/v0/status" -H "Accept: application/json"
```

**2. Verify a seller endpoint.** The free verification path: the current
signed verdict for an x402 endpoint, or an explicit no-signal outcome. No
account, no credential, never charged.

```bash
curl -sS "https://api.zadq.net/v0/verifications?chain=base&payTo=<wallet-address>&resourceUrl=<endpoint-url>" \
  -H "Accept: application/json"
```

Signatures verify locally against the published keys at `https://api.zadq.net/v0/verification-keys`.

**3. Open a session for the write operations.** The dashboard's only
credential is control of the payment wallet: request a challenge for the
`payTo` address, sign it with the wallet, redeem the signature for a
short-lived session credential and send it as a bearer on the write
operations.

```bash
curl -sS -X POST "https://api.zadq.net/v0/auth/challenge" \
  -H "Content-Type: application/json" \
  -d '{"chain":"base","address":"<wallet-address>"}'

curl -sS -X POST "https://api.zadq.net/v0/auth/session" \
  -H "Content-Type: application/json" \
  -d '{"chain":"base","address":"<wallet-address>","nonce":"<challenge-nonce>","signature":"<wallet-signature>"}'

curl -sS "https://api.zadq.net/v0/sellers/<seller-id>" \
  -H "Authorization: Bearer <session-credential>"
```

The exact request and response bodies of every call are in the reference
below (`AuthChallengeRequest`, `AuthSessionRequest`, `Verdict`).

## Overview

| Field | Value |
| --- | --- |
| Contract | Seller-Verification Service API |
| Version | `0.1.0` |
| Specification | OpenAPI `3.0.3` |

v0 seller-verification API: seller intake, admission state, attestation
issuance metadata, and the free verification path.

The free verification path is never charged for, to anyone, ever.

Graceful degradation: consumers must treat an unreachable service or an
absent verdict as "no signal", never as a failure of their own flow.

## Authentication

Operations name the schemes they accept; an operation marked as public takes no credential at all.

### `walletSession`

HTTP header `Authorization: Bearer <credential>`.

Session bearer from POST /v0/auth/session (wallet-proven payTo).

## Other operations

### POST `/v0/auth/challenge`

**Wallet login, step 1 — challenge for a payTo address**

Returns a one-time message for the seller's payment wallet to sign
(EVM `personal_sign` or Solana message signing). Proving control of
the `payTo` address is the only credential the dashboard uses — no
account, no personal data. Challenges expire after 5 minutes.

Authentication: none — this operation is public.

#### Request body

`application/json` — required

Schema: [`AuthChallengeRequest`](#schema-authchallengerequest)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes |  |  |

#### Responses

`201` — Challenge issued.

Content type `application/json`.

Schema: [`AuthChallenge`](#schema-authchallenge)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `nonce` | string | yes |  |  |
| `message` | string | yes | Exact text to sign, byte for byte. |  |
| `expiresAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/auth/challenge" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/auth/session`

**Wallet login, step 2 — redeem the signed challenge**

Verifies the wallet signature over the challenge message and returns a
bearer credential bound to `{chain, address}` (12 h). Send it as
`Authorization: Bearer <bearer>` on the write operations.

Authentication: none — this operation is public.

#### Request body

`application/json` — required

Schema: [`AuthSessionRequest`](#schema-authsessionrequest)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes |  |  |
| `nonce` | string | yes |  |  |
| `signature` | string | yes | EVM — 0x-hex r\|\|s\|\|v from personal_sign; Solana — base58 (or hex) 64-byte signature. |  |

#### Responses

`201` — Session issued.

Content type `application/json`.

Schema: [`AuthSession`](#schema-authsession)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `bearer` | string | yes | Opaque session credential for the Authorization header. |  |
| `expiresAt` | string (date-time) | yes |  |  |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/auth/session" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/healthz`

**Liveness probe**

Authentication: none — this operation is public.

#### Responses

`200` — Service is up.

Content type `application/json`.

Schema: [`Health`](#schema-health)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `status` | string | yes |  | `"ok"` |
| `version` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/healthz"
```

### GET `/v0/status`

**Substrate status (public)**

What an explorer or a seller can check before asking: whether the
identity substrate behind the verdicts is `ok`, `degraded` or
`unknown`, when a standing read last succeeded, and the cadence
promise. Read from the verifier's own status contract when wired
(`source: verifier`); otherwise inferred from the cadence — degraded
only after three consecutive cycles in which every attestation was
withheld, never from a single unknown (`source: cadence`). No
credentials, no identifiers, nothing personal (ZADQ-70). While the
substrate is degraded every integration keeps working: verdicts read
`no_signal`, badges read unknown, nothing blocks.

Authentication: none — this operation is public.

#### Responses

`200` — The substrate's state as this service sees it.

Content type `application/json`.

Schema: [`SubstrateStatus`](#schema-substratestatus)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `substrate` | string | yes |  | `"ok"`, `"degraded"`, `"unknown"` |
| `source` | string | yes | Where the state comes from — the verifier's status read, or this service's cadence accounting. | `"verifier"`, `"cadence"` |
| `reason` | string | no | Why the substrate is not ok, in the verifier's standing vocabulary (for example `height_stale`, `root_expired`, `state_unavailable`). Absent when ok. |  |
| `checkedAt` | string (date-time) | yes |  |  |
| `lastSuccessfulStandingRead` | string (date-time) | no | The last time a standing read answered with a known status. Absent before the first. |  |
| `cadence` | object | yes |  |  |
| `cadence.recheckEveryHours` | integer | yes | The published cadence promise. |  |
| `cadence.lastCycleAt` | string (date-time) | no |  |  |
| `cadence.checked` | integer | no | Attestations the last cycle asked the verifier about. |  |
| `cadence.withheld` | integer | no | Of those, how many came back with an unknown standing. |  |

#### Example

```bash
curl -sS "<base-url>/v0/status"
```

### POST `/v0/interest`

**Register interest (public web form target)**

The target of the native HTML form on the public web's
register-interest page (ZADQ-103): a vendor or an agent operator says
which side of the trust perimeter it is on, optionally the x402
resource URL it sells at or the size band of its fleet, and nothing
else. No contact data is accepted or stored here — the person sends
their address through the mailbox from the page the browser is sent
back to. Submissions are counted by side for the funnel; the optional
details are cleared after twelve months, the count stays.

Answers with a redirect to the public web in every case, so a browser
without scripts lands on a page: the "received" page (with the side
as the fragment) on success, the "not received" page when the input
is invalid, when the client is over its rate limit (a few submissions
per client per ten minutes, and a global bound), or when storage
fails. The page reached always shows the mailbox route, so nothing a
person can do depends on this service being up. The field named
`website` is a trap for automated submitters: when it carries a
value the submission is dropped and the browser still lands on the
"received" page.

Authentication: none — this operation is public.

#### Request body

`application/x-www-form-urlencoded` — required

Schema: [`InterestForm`](#schema-interestform)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `side` | string | yes | Which side of the trust perimeter the person stands on. | `"vendor"`, `"agent_operator"` |
| `resource_url` | string (uri) | no | The x402 resource URL a vendor sells at (optional, vendors). |  |
| `agent_count_band` | string | no | The size band of an operator's fleet (optional, agent operators). | `"up_to_10"`, `"up_to_100"`, `"more_than_100"` |
| `website` | string | no | Trap field for automated submitters; a person leaves it empty. |  |

#### Responses

`303` — The browser is sent back to the public web.

#### Example

```bash
curl -sS -X POST "<base-url>/v0/interest" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d @request-body.json
```

### POST `/v0/sellers`

**Seller intake (onboarding flow 1)**

Registers a seller's x402 identifiers and segment declaration and opens
a KYC session at the issuer-side KYC boundary. No identity data is
accepted here. Requires a wallet session for the intake `payTo`
address (401/403 otherwise): control of the address is proven from the
first minute.

Authentication: [`walletSession`](#auth-walletsession).

#### Request body

`application/json` — required

Schema: [`SellerIntake`](#schema-sellerintake)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `segment` | [`SegmentDeclaration`](#schema-segmentdeclaration) | yes |  |  |

#### Responses

`201` — Seller registered; KYC session opened.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/v0/sellers/{sellerId}`

**Seller state (admission, attestation, billing)**

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`200` — Current seller state.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>"
```

### GET `/v0/sellers/{sellerId}/endpoints`

**The endpoints of this seller's operator**

Every endpoint the same accountable operator registered on its
approved verification (ZADQ-53). Requires the wallet session of this
seller's `payTo` (401/403): the grouping is never a public read, so an
operator's endpoints stay uncorrelatable to third parties.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`200` — The operator's endpoints, oldest first.

Content type `application/json`.

Schema: [`OperatorEndpoints`](#schema-operatorendpoints)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `endpoints` | array of [`EndpointSummary`](#schema-endpointsummary) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>/endpoints" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/sellers/{sellerId}/endpoints`

**Add an endpoint on the operator's approved verification**

Registers a further endpoint of the accountable operator behind this
admitted seller, reusing its approved session inside the reuse window
(ZADQ-53): no new KYC, the new seller reads `admitted` at once and can
be attested. Requires the wallet session of this seller's `payTo`
(401/403). When the new endpoint's `payTo` differs, `proof` must carry
a signed auth challenge for the new address (`POST /v0/auth/challenge`
for it, sign the message): control of every address is proven. 409
unless admitted with an open window (re-verify first), or when the
endpoint already exists.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`EndpointAddition`](#schema-endpointaddition)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `resourceUrl` | string (uri) | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | no |  |  |
| `proof` | [`AddressProof`](#schema-addressproof) | no |  |  |

#### Responses

`201` — The new seller, admitted on the shared verification.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/endpoints" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/sellers/{sellerId}/wallet`

**Rotate the payment wallet behind an attested endpoint**

Changes this admitted seller's `payTo` address inside the approved
session's reuse window (ZADQ-53): the attestation bound to the old
identifiers is revoked upstream and a new one is issued for the new
ones on the same verification — no new KYC. Requires the wallet
session of the current `payTo` (401/403) and `proof`: a signed auth
challenge for the new address. 409 unless admitted with an open window
and acknowledged terms, or when the endpoint already exists.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`WalletRotation`](#schema-walletrotation)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `proof` | [`AddressProof`](#schema-addressproof) | yes |  |  |

#### Responses

`200` — The seller on its new address, with the new attestation.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/wallet" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/sellers/{sellerId}/billing/checkout`

**Set up billing on the operator's hosted checkout**

Opens the operator's hosted checkout page for this admitted seller,
keyed by the seller id and the chosen tier only (ZADQ-54). The person
enters legal and billing data there — none of it enters this service,
which keeps the billing state, the tier name and an opaque customer
reference. Billing reads `pending` until the provider's signed event
lands. Requires the wallet session of the seller's `payTo` (401/403);
409 unless admitted, or while billing is already active. Manual bank
transfer for the first contracts is agreed with us directly, outside
the service.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`BillingCheckout`](#schema-billingcheckout)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `tier` | string | yes | The schedule tier, by name. Amounts are on the checkout page and in the operator agreement. | `"attested_vendor"`, `"regulated_compliance"` |

#### Responses

`201` — The hosted checkout to open, and the seller's billing state.

Content type `application/json`.

Schema: [`BillingHandoff`](#schema-billinghandoff)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `url` | string (uri) | yes | The hosted checkout page — open it as-is. |  |
| `billing` | string | yes |  | `"none"`, `"pending"`, `"active"` |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/billing/checkout" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/billing/events`

**Billing provider event (signed)**

Inbound event from the operator's billing provider, signed with the
webhook secret (`Stripe-Signature`: `t=<unix>,v1=<hmac-sha256>`), applied
once per event id. `checkout.session.completed` moves billing to
`active`; `checkout.session.expired` and
`customer.subscription.deleted` to `none`; anything else is
acknowledged and ignored. The payload is read for its id, type, seller
reference and customer reference only. 401 on a bad or stale signature.

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Stripe-Signature` | header | yes | string |  |

#### Request body

`application/json` — required

Type: object.

#### Responses

`200` — Acknowledged; whether the event changed a state.

Content type `application/json`.

Schema: [`BillingEventReceipt`](#schema-billingeventreceipt)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `applied` | boolean | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/billing/events" \
  -H "Stripe-Signature: <Stripe-Signature>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/sellers/{sellerId}/credential/offer`

**Hand out the v1 credential offer, once**

Creates the v1 credential offer for this admitted seller from its
approved verification (ZADQ-83) and hands it out exactly once, over
the wallet session of the seller's `payTo` (401/403). The offer is a
one-shot pre-authorized code the seller's own middleware redeems at
once with the holder library, in the seller's own infrastructure:
this service never stores or logs it, never sees the credential, a
key or a proof — the dashboard shows state, never secrets. The code
and the credential it yields expire exactly with the verification
(no grace); past the window the issuer answers 409 and the seller
re-verifies. 409 while a credential is already held or a
re-verification is pending; 501 when the substrate offers no v1
credential rail.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`201` — The offer, handed out once, and when it expires.

Content type `application/json`.

Schema: [`CredentialOfferHandoff`](#schema-credentialofferhandoff)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `offer` | object | yes | The credential offer object, as the issuer returned it — opaque, one-shot; redeem it at once. |  |
| `expiresAt` | string (date-time) | yes | When the code and the credential it yields expire — the verification's own expiry at the latest. No grace. |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/credential/offer" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/sellers/{sellerId}/credential`

**Report the credential's state from the seller's infrastructure**

The seller's middleware reports what it did with the offer: `held`
(optionally with an opaque reference of the seller's choosing, and
with the `presentationUrl` its holder answers this service's
challenges at — that URL puts the seller on the v1 verdict path:
on the cadence this service forwards its verifier's 10-second
challenge there, byte for byte, and the verifier's decision on the
holder's envelope governs the positive claim, ZADQ-84) or
`self_revoked` (the holder revoked its own credential through the
network's privacy relay; from the next status root on the credential
presents nowhere; `revoked` is accepted as the older spelling of the
same report; the presentation URL is cleared). Without a presentation
URL the seller keeps the v0 standing path. Wallet session of the
seller's `payTo` required (401/403). Nothing secret travels here; the
reference is never a key, a code or a proof. `presentationUrl` must be
an absolute https URL (http only to a loopback host, local
development).

A `self_revoked` report is recorded as the state change on the
seller's own record and nothing else: no event, no time, no log line,
no notification — the reference, the hand-out time and the
presentation URL are cleared. The consequence lands at the next
verdict cadence, never at this moment: the v0 attestation is retired
(`statusReason: self_revoked`) and the public verdict reads `revoked`
from then on. The way back is a new verification (re-verify), which
yields a fresh offer; another offer on the same verification answers
409.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`CredentialReport`](#schema-credentialreport)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes | `held`, or `self_revoked` (`revoked` is the older spelling of the same report). | `"held"`, `"self_revoked"`, `"revoked"` |
| `credentialRef` | string | no | An opaque reference of the seller's choosing for the credential it holds. Never a key. |  |
| `presentationUrl` | string (uri) | no | Where the seller's holder answers this service's challenges (ZADQ-84): `POST` of the issued challenge as the verifier returned it (`{request, signature_hex}`), `200` with the envelope (`{proof_hex, issuer_id, status_root, claimed_predicates}`); any other status means the holder cannot present this time. Only with `held`; kept when omitted on a later report. |  |

#### Responses

`200` — The seller with the reported credential state.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/credential" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/sellers/{sellerId}/kyc/sessions`

**Start the identity verification again, or re-verify before the reuse window ends**

Opens a new KYC session at the boundary. From `kyc_expired`: start
again (the seller reads pending_kyc with the new handoff). From
`admitted`: a **re-verification** that renews the approved session
(ZADQ-91) — the seller stays admitted, its attestation keeps its
standing, and the pending renewal is returned in `renewal`; once
approved, the renewal is bound upstream to the attestation, which keeps
its reference, and a `rekyc_overdue` suspension is lifted at the next
root. 409 from any other state or while a renewal is pending. Requires
the wallet session of the seller's `payTo` address (401/403 otherwise).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`201` — New session opened; the seller reads pending_kyc again.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/kyc/sessions" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/sellers/{sellerId}/consent/withdraw`

**Withdraw consent to the service (terminal)**

The accountable party withdraws its consent to the service (ZADQ-91).
The boundary records it on the approved session; every attestation it
backs is revoked upstream with reason `consent_withdrawn` at the next
root, the verdict reads `revoked`, and the seller is `withdrawn` —
terminal (a new intake is a new session). Consent given to the identity
provider itself is withdrawn at the provider and has no status effect
here. Requires the wallet session of the seller's
`payTo` address (401/403); 409 unless admitted; 501 when the substrate
cannot record it (raised with the operator; nothing changes).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`200` — Consent withdrawn; the seller is terminal.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/consent/withdraw" \
  -H "Authorization: Bearer <session-credential>"
```

### GET `/v0/terms`

**The terms-acknowledgement document in force (free path)**

The versioned document a seller signs with its payment wallet before
the first issuance (ZADQ-55): the version, its SHA-256, where the full
Terms of Service are published, and the text itself — which carries
the related-party disclosure at contract time. Public and cacheable,
so anyone can check what an attested seller acknowledged.

Authentication: none — this operation is public.

#### Responses

`200` — The current document.

Content type `application/json`.

Schema: [`TermsDocument`](#schema-termsdocument)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `version` | string | yes | Version name of the acknowledgement document. |  |
| `sha256` | string | yes | Hex SHA-256 of `text`. |  |
| `url` | string (uri) | yes | Where the full Terms of Service are published. |  |
| `text` | string | yes | The acknowledgement text the wallet signs for. |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/terms"
```

### GET `/v0/sellers/{sellerId}/terms`

**Terms acknowledgement state of a seller, and the message to sign**

Whether the seller's wallet has acknowledged the current terms
version, and — if not — the exact message to sign, with the signing
time the service will verify it against (`messageSignedAt`; submit it
back unchanged within fifteen minutes). Requires the wallet session of
the seller's `payTo` address (401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`200` — Acknowledgement state.

Content type `application/json`.

Schema: [`SellerTerms`](#schema-sellerterms)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `current` | [`TermsDocument`](#schema-termsdocument) | yes |  |  |
| `acknowledged` | boolean | yes | Whether the current version is acknowledged by this seller. |  |
| `acknowledgedVersion` | string | no |  |  |
| `signedAt` | string (date-time) | no | When the current version was signed, if it was. |  |
| `message` | string | yes | The exact text to sign for the current version. |  |
| `messageSignedAt` | string (date-time) | yes | The signing time embedded in `message`; submit it back as `signedAt`. |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>/terms" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/sellers/{sellerId}/terms`

**Acknowledge the current terms with the seller's wallet**

Records the wallet signature over the acknowledgement message for the
current version (same challenge mechanism as the login, a different
message). Stored against the seller: version, SHA-256, signature,
signing time — no mailbox, no account, no identity data. Issuance
requires an acknowledgement of the current version (409 `terms not
acknowledged` otherwise). 401 on a signature that does not verify for
the seller's `payTo`; 409 when the version or hash is not the current
one; 400 when the signing time is outside the accepted window. A
second acknowledgement of the same version changes nothing.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`TermsAcknowledgement`](#schema-termsacknowledgement)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `version` | string | yes |  |  |
| `sha256` | string | yes |  |  |
| `signedAt` | string (date-time) | yes |  |  |
| `signature` | string | yes | Wallet signature over the message (hex for EVM, base58 or hex for Solana). |  |

#### Responses

`201` — Acknowledged.

Content type `application/json`.

Schema: [`SellerTerms`](#schema-sellerterms)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `current` | [`TermsDocument`](#schema-termsdocument) | yes |  |  |
| `acknowledged` | boolean | yes | Whether the current version is acknowledged by this seller. |  |
| `acknowledgedVersion` | string | no |  |  |
| `signedAt` | string (date-time) | no | When the current version was signed, if it was. |  |
| `message` | string | yes | The exact text to sign for the current version. |  |
| `messageSignedAt` | string (date-time) | yes | The signing time embedded in `message`; submit it back as `signedAt`. |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/terms" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/v0/sellers/{sellerId}/webhooks`

**The seller's own receivers (secrets never shown again)**

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`200` — The seller's receivers.

Content type `application/json`.

Schema: [`SellerWebhooks`](#schema-sellerwebhooks)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `webhooks` | array of [`Webhook`](#schema-webhook) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>/webhooks" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/sellers/{sellerId}/webhooks`

**Register a receiver for the seller's own events**

Owner-bound (ZADQ-51): the wallet session must prove the seller's
`payTo` (401/403). The per-subscription secret is returned once;
https is required except for loopback receivers (local development).
Events and signing: see the webhooks note at the top of this
document. 409 when the URL is already registered for this seller.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`WebhookRegistration`](#schema-webhookregistration)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `url` | string (uri) | yes |  |  |

#### Responses

`201` — Receiver registered; secret shown once.

Content type `application/json`.

Schema: [`Webhook`](#schema-webhook)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `url` | string (uri) | yes |  |  |
| `secret` | string | no | Hex signing secret for X-Signature. Returned once at registration. |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/webhooks" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### DELETE `/v0/sellers/{sellerId}/webhooks/{webhookId}`

**Remove one of the seller's receivers**

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |
| `webhookId` | path | yes | string |  |

#### Responses

`204` — Receiver removed.

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X DELETE "<base-url>/v0/sellers/<sellerId>/webhooks/<webhookId>" \
  -H "Authorization: Bearer <session-credential>"
```

### GET `/v0/sellers/{sellerId}/events`

**What changed — the seller's recent events, newest first**

The append-only event stream of the seller (intake, KYC outcomes and
expiry, issuance, status changes, consent withdrawal), for the
dashboard's "what changed" list (ZADQ-51). Wallet-bound; metadata
only, never identity data.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |
| `limit` | query | no | integer |  |

#### Responses

`200` — Recent events.

Content type `application/json`.

Schema: [`SellerEvents`](#schema-sellerevents)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `events` | array of [`SellerEvent`](#schema-sellerevent) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>/events" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/sellers/{sellerId}/kyc/result`

**KYC outcome callback (issuer-side boundary → this service)**

Callback endpoint for the KYC boundary. Carries outcome metadata only —
never identity data. HMAC-authenticated: `X-Signature: sha256=<hex>`
over the raw body with the shared callback secret and `X-Signature-Key`
(first 8 bytes of the secret's SHA-256, hex); 401 otherwise. Local
fake mode without a secret leaves it open for the smoke.

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`KycResult`](#schema-kycresult)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `sessionId` | string | yes |  |  |
| `outcome` | string | yes |  | `"approved"`, `"rejected"` |
| `assuranceLevel` | string | no | Assurance grade met by the proofing process (e.g. "G"). |  |

#### Responses

`200` — Admission state updated.

Content type `application/json`.

Schema: [`Seller`](#schema-seller)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/kyc/result" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/sellers/{sellerId}/attestations`

**Issue the seller-side attestation (flow 2)**

Orchestrates issuance for an admitted seller via the issuer path and
records attestation metadata. Fails for non-admitted sellers. Requires
a wallet session for the seller's `payTo` address.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`201` — Attestation issued.

Content type `application/json`.

Schema: [`Attestation`](#schema-attestation)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `sellerId` | string | yes |  |  |
| `assuranceLevel` | string | yes |  |  |
| `status` | string | yes |  | `"active"`, `"suspended"`, `"revoked"` |
| `statusReason` | string | no | Why a non-active status was set — never silent. `rekyc_overdue`: the reuse window ended; restored under the same reference by a bound re-verification. `consent_withdrawn`: terminal. `self_revoked`: the holder revoked its own credential and the attestation was retired at the next verdict cadence — terminal for this attestation; a new verification yields a new one. Absent for operator changes and while active. | `"rekyc_overdue"`, `"consent_withdrawn"`, `"self_revoked"` |
| `freshnessBand` | string | no | Freshness band of the backing session, as the issuer reports it. | `"F4"`, `"F3"`, `"F2"`, `"F1"` |
| `issuedAt` | string (date-time) | yes |  |  |
| `updatedAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/sellers/<sellerId>/attestations" \
  -H "Authorization: Bearer <session-credential>"
```

### GET `/v0/sellers/{sellerId}/attestations/{attestationId}/receipt`

**The issuer's signed receipt of the attestation (for X-PAYMENT-IDENTITY)**

The issuer-signed receipt of the issuance, byte-shaped as the
`attestation-ref/v0` payload of the payment-identity companion header
(ZADQ-92): the seller's middleware puts it on `X-PAYMENT-IDENTITY`
beside `X-PAYMENT`, and a receiver may verify the issuer's Ed25519
signature locally against the issuer's registered status key. It is
proof of **issuance**, never of standing: standing is the verdict's
(`GET /v0/verifications`). Requires the wallet session of the
seller's `payTo` address (401/403). 404 when no receipt is stored
(local fakes, attestations recorded before receipts were kept).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |
| `attestationId` | path | yes | string |  |

#### Responses

`200` — The receipt.

Content type `application/json`.

Schema: [`AttestationReceipt`](#schema-attestationreceipt)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `attestation_ref` | string | yes | 32-byte opaque reference, hex. |  |
| `issuer_id` | string | yes |  |  |
| `chain` | string | yes |  |  |
| `pay_to` | string | yes |  |  |
| `resource_url` | string | yes |  |  |
| `assurance_grade` | integer | yes |  |  |
| `issued_at_unix` | integer | yes |  |  |
| `signature` | string | yes | Ed25519 by the issuer's registered status signing key, hex (64 bytes). |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>/attestations/<attestationId>/receipt" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/attestations/{attestationId}/status`

**Status-mirror update (network status cycle → this service)**

Applies a status change originating in the standard network status
machinery. The service never revokes by its own say-so (seed §3.1
flow 4); this endpoint mirrors upstream state. HMAC-authenticated like
the KYC callback (`X-Signature`, `X-Signature-Key`).

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `attestationId` | path | yes | string |  |

#### Request body

`application/json` — required

Schema: [`AttestationStatusUpdate`](#schema-attestationstatusupdate)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `status` | string | yes |  | `"active"`, `"suspended"`, `"revoked"` |

#### Responses

`200` — Mirrored status applied.

Content type `application/json`.

Schema: [`Attestation`](#schema-attestation)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `sellerId` | string | yes |  |  |
| `assuranceLevel` | string | yes |  |  |
| `status` | string | yes |  | `"active"`, `"suspended"`, `"revoked"` |
| `statusReason` | string | no | Why a non-active status was set — never silent. `rekyc_overdue`: the reuse window ended; restored under the same reference by a bound re-verification. `consent_withdrawn`: terminal. `self_revoked`: the holder revoked its own credential and the attestation was retired at the next verdict cadence — terminal for this attestation; a new verification yields a new one. Absent for operator changes and while active. | `"rekyc_overdue"`, `"consent_withdrawn"`, `"self_revoked"` |
| `freshnessBand` | string | no | Freshness band of the backing session, as the issuer reports it. | `"F4"`, `"F3"`, `"F2"`, `"F1"` |
| `issuedAt` | string (date-time) | yes |  |  |
| `updatedAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/attestations/<attestationId>/status" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/v0/verification-keys`

**Verdict-verification public keys (free path)**

The Ed25519 public keys verdict signatures are checked against. Free,
unauthenticated, cacheable. `keyId` matches `Verdict.keyId` (first 8
bytes of the key's SHA-256, hex). Consumers without the key keep
working: a signature they cannot check is "unverifiable", never an
error.

Authentication: none — this operation is public.

#### Responses

`200` — Current verification keys.

Content type `application/json`.

Schema: [`VerificationKeys`](#schema-verificationkeys)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `keys` | array of [`VerificationKey`](#schema-verificationkey) | yes |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/verification-keys"
```

### GET `/v0/metrics`

**Published metrics (free path)**

The published metric series (ZADQ-106): sellers attested, 90-day
retention, verifications served, independent registered verifiers,
anchor-service revenue — computed from this service's own records,
counts only, monthly, complete months only. Free, unauthenticated,
cacheable. Every series carries `relatedParty: true` and its period:
the operator runs a related-party pilot and nothing measured on it
counts as independent adoption. A series the records hold no source
for says so (`available: false`) rather than carrying a figure.
Registry-based or aggregate throughout — no per-verification record
exists — and nothing personal, no identifier of any kind.

Authentication: none — this operation is public.

#### Responses

`200` — The current report.

Content type `application/json`.

Schema: [`MetricsReport`](#schema-metricsreport)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `schema` | string | yes | The wire shape's name (`published-metrics/v1`). |  |
| `computedAt` | string (date-time) | yes |  |  |
| `asOf` | string (date) | yes | The cut-off — the last day of the last complete month (UTC). Partial months are never published. |  |
| `period` | [`MetricsPeriod`](#schema-metricsperiod) | no |  |  |
| `cadence` | object | yes |  |  |
| `cadence.publication` | string | yes | `monthly`. |  |
| `cadence.snapshot` | string | yes | `quarterly`, aligned to the two-consecutive-quarters clock. |  |
| `relatedParty` | boolean | yes | Always true — the operator's position is a property of the dataset. |  |
| `operator` | [`MetricsOperator`](#schema-metricsoperator) | yes |  |  |
| `series` | array of [`MetricsSeries`](#schema-metricsseries) | yes |  |  |
| `contextNote` | string | yes |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/metrics"
```

### GET `/v0/verifications`

**Free verification path (explorers, buyers)**

Returns the current signed verdict for an x402 endpoint, or a
no-signal outcome. Free of charge, no authentication, ever.

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `chain` | query | yes | [`Chain`](#schema-chain) |  |
| `payTo` | query | yes | string |  |
| `resourceUrl` | query | yes | string |  |

#### Responses

`200` — Verdict (or explicit no-signal outcome).

Content type `application/json`.

Schema: [`Verdict`](#schema-verdict)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `outcome` | string | yes |  | `"human_backed"`, `"revoked"`, `"stale"`, `"no_signal"` |
| `subject` | object | yes |  |  |
| `subject.chain` | [`Chain`](#schema-chain) | yes |  |  |
| `subject.payTo` | string | yes |  |  |
| `subject.resourceUrl` | string | yes |  |  |
| `assuranceLevel` | string | no |  |  |
| `verifiedAt` | string (date-time) | no |  |  |
| `recheckEveryHours` | integer | yes |  |  |
| `signature` | string | no | Ed25519 over the canonical verdict payload, base64. |  |
| `keyId` | string | no |  |  |
| `reason` | string | no | Why the outcome is not `human_backed`: the standing read's reason code (closed vocabulary), or `suspended` when the witnessed status is suspended. Absent on `human_backed`; absent on `revoked` too. The policy reason of a suspension (`rekyc_overdue`) is the seller's own record, never part of the public verdict. On the v1 path (ZADQ-84) a verifier-side condition of the presentation decision is carried with the same code (`issuer_not_active`, `status_root_stale`, `height_stale`) or as `state_unavailable`; a presentation the verifier refused on the holder's side withholds the positive claim with no `reason` — the decision's exact code is the seller's own record (`credential.lastPresentation`). | `"state_unavailable"`, `"issuer_unknown"`, `"issuer_not_active"`, `"status_root_stale"`, `"height_stale"`, `"witness_unavailable"`, `"witness_malformed"`, `"witness_invalid"`, `"witness_root_mismatch"`, `"bond_insufficient"`, `"suspended"` |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/verifications?chain=<chain>&payTo=<payTo>&resourceUrl=<resourceUrl>"
```

### POST `/v0/webhooks`

**Register an explorer receiver for verdict.updated**

Free and unauthenticated — the payload is the public verdict. The
per-subscription secret is returned once; https is required except
for loopback receivers (local development).

Authentication: none — this operation is public.

#### Request body

`application/json` — required

Schema: [`WebhookRegistration`](#schema-webhookregistration)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `url` | string (uri) | yes |  |  |

#### Responses

`201` — Subscription created; secret shown once.

Content type `application/json`.

Schema: [`Webhook`](#schema-webhook)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `url` | string (uri) | yes |  |  |
| `secret` | string | no | Hex signing secret for X-Signature. Returned once at registration. |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/webhooks" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### DELETE `/v0/webhooks/{webhookId}`

**Remove a receiver**

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | yes | string |  |

#### Responses

`204` — Subscription removed.

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X DELETE "<base-url>/v0/webhooks/<webhookId>"
```

### GET `/v0/sellers/{sellerId}/verdict`

**Latest verdict for a seller (dashboard view)**

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `sellerId` | path | yes | string |  |

#### Responses

`200` — Latest verdict.

Content type `application/json`.

Schema: [`Verdict`](#schema-verdict)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `outcome` | string | yes |  | `"human_backed"`, `"revoked"`, `"stale"`, `"no_signal"` |
| `subject` | object | yes |  |  |
| `subject.chain` | [`Chain`](#schema-chain) | yes |  |  |
| `subject.payTo` | string | yes |  |  |
| `subject.resourceUrl` | string | yes |  |  |
| `assuranceLevel` | string | no |  |  |
| `verifiedAt` | string (date-time) | no |  |  |
| `recheckEveryHours` | integer | yes |  |  |
| `signature` | string | no | Ed25519 over the canonical verdict payload, base64. |  |
| `keyId` | string | no |  |  |
| `reason` | string | no | Why the outcome is not `human_backed`: the standing read's reason code (closed vocabulary), or `suspended` when the witnessed status is suspended. Absent on `human_backed`; absent on `revoked` too. The policy reason of a suspension (`rekyc_overdue`) is the seller's own record, never part of the public verdict. On the v1 path (ZADQ-84) a verifier-side condition of the presentation decision is carried with the same code (`issuer_not_active`, `status_root_stale`, `height_stale`) or as `state_unavailable`; a presentation the verifier refused on the holder's side withholds the positive claim with no `reason` — the decision's exact code is the seller's own record (`credential.lastPresentation`). | `"state_unavailable"`, `"issuer_unknown"`, `"issuer_not_active"`, `"status_root_stale"`, `"height_stale"`, `"witness_unavailable"`, `"witness_malformed"`, `"witness_invalid"`, `"witness_root_mismatch"`, `"bond_insufficient"`, `"suspended"` |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/sellers/<sellerId>/verdict"
```

### POST `/v0/agent-operators`

**Agent-operator enrolment — the human or organisation behind an agent**

Enrols the accountable party behind an agent: the wallet the
operator's agent pays from, and the operator type (person →
identity verification, organisation → business verification). Opens
a KYC session at the issuer-side KYC boundary and returns the
operator in `pending_kyc` with the identity provider's hosted page in
`kyc.handoffUrl`. No identity data is accepted here. Requires the
wallet session of that wallet (401/403 otherwise): control of the
wallet is proven from the first minute. 409 when the wallet is
already enrolled. An agent operator is not an x402 endpoint: it
carries no resource URL and no attestation, and a wallet that is
also a vendor's `payTo` is not linked to that vendor by this service.

Authentication: [`walletSession`](#auth-walletsession).

#### Request body

`application/json` — required

Schema: [`AgentOperatorEnrolment`](#schema-agentoperatorenrolment)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from — the one the wallet session proves. |  |
| `operatorType` | string | yes | `person` is verified by identity verification, `organisation` by business verification; the boundary learns this and nothing else. | `"person"`, `"organisation"` |

#### Responses

`201` — Operator enrolled; KYC session opened.

Content type `application/json`.

Schema: [`AgentOperator`](#schema-agentoperator)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The operator's opaque reference (`aop_…`). |  |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from. |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `pending_kyc` → `approved` → `credential_held` (the principal's credential is held in the operator's infrastructure) → `active` (its first agent is minted). `kyc_expired`: the proofing deadline passed, or the approved verification's reuse window closed — the principal's credential expired with it; verify again (`POST …/kyc/sessions`). `rejected` and `withdrawn` are terminal. | `"pending_kyc"`, `"kyc_expired"`, `"approved"`, `"rejected"`, `"credential_held"`, `"active"`, `"withdrawn"` |
| `billing` | string | yes | The one-off entry fee at the operator's billing provider: none until a checkout starts, pending until the provider's signed event lands, active once it is recorded. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `agents` | [`AgentCounts`](#schema-agentcounts) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/v0/agent-operators/{agentOperatorRef}`

**Agent-operator state (verification, entry fee, the principal's credential)**

The operator's state as the dashboard shows it — state, never keys.
The read brings it up to date first: a pending session's outcome is
read from the KYC boundary, a session past its deadline reads
`kyc_expired`, and an approved verification whose reuse window
closed reads `kyc_expired` too (the principal's credential expired
with it, exactly — no grace). Wallet session of the operator's
wallet required (401/403): an operator's record is never a public
read.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Responses

`200` — Current agent-operator state.

Content type `application/json`.

Schema: [`AgentOperator`](#schema-agentoperator)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The operator's opaque reference (`aop_…`). |  |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from. |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `pending_kyc` → `approved` → `credential_held` (the principal's credential is held in the operator's infrastructure) → `active` (its first agent is minted). `kyc_expired`: the proofing deadline passed, or the approved verification's reuse window closed — the principal's credential expired with it; verify again (`POST …/kyc/sessions`). `rejected` and `withdrawn` are terminal. | `"pending_kyc"`, `"kyc_expired"`, `"approved"`, `"rejected"`, `"credential_held"`, `"active"`, `"withdrawn"` |
| `billing` | string | yes | The one-off entry fee at the operator's billing provider: none until a checkout starts, pending until the provider's signed event lands, active once it is recorded. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `agents` | [`AgentCounts`](#schema-agentcounts) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/agent-operators/<agentOperatorRef>" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/kyc/sessions`

**Start the verification again**

Opens a new KYC session at the boundary: from `kyc_expired` (the
proofing deadline passed, or the approved verification's reuse
window closed), or after the holder revoked the principal's
credential — the way back is a new verification, which yields a
fresh credential that carries nothing of the old one. The operator
reads `pending_kyc` with the new handoff. 409 from any other state.
Wallet session of the operator's wallet required (401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Responses

`201` — New session opened; the operator reads pending_kyc again.

Content type `application/json`.

Schema: [`AgentOperator`](#schema-agentoperator)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The operator's opaque reference (`aop_…`). |  |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from. |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `pending_kyc` → `approved` → `credential_held` (the principal's credential is held in the operator's infrastructure) → `active` (its first agent is minted). `kyc_expired`: the proofing deadline passed, or the approved verification's reuse window closed — the principal's credential expired with it; verify again (`POST …/kyc/sessions`). `rejected` and `withdrawn` are terminal. | `"pending_kyc"`, `"kyc_expired"`, `"approved"`, `"rejected"`, `"credential_held"`, `"active"`, `"withdrawn"` |
| `billing` | string | yes | The one-off entry fee at the operator's billing provider: none until a checkout starts, pending until the provider's signed event lands, active once it is recorded. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `agents` | [`AgentCounts`](#schema-agentcounts) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/kyc/sessions" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/kyc/result`

**KYC outcome callback for an agent operator (issuer-side boundary → this service)**

Callback endpoint for the KYC boundary, the same contract as the
sellers' one. Carries outcome metadata only — never identity data.
HMAC-authenticated: `X-Signature: sha256=<hex>` over the raw body
with the shared callback secret and `X-Signature-Key` (first 8 bytes
of the secret's SHA-256, hex); 401 otherwise. Local fake mode without
a secret leaves it open for the smoke.

Authentication: none — this operation is public.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Request body

`application/json` — required

Schema: [`KycResult`](#schema-kycresult)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `sessionId` | string | yes |  |  |
| `outcome` | string | yes |  | `"approved"`, `"rejected"` |
| `assuranceLevel` | string | no | Assurance grade met by the proofing process (e.g. "G"). |  |

#### Responses

`200` — Verification state updated.

Content type `application/json`.

Schema: [`AgentOperator`](#schema-agentoperator)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The operator's opaque reference (`aop_…`). |  |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from. |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `pending_kyc` → `approved` → `credential_held` (the principal's credential is held in the operator's infrastructure) → `active` (its first agent is minted). `kyc_expired`: the proofing deadline passed, or the approved verification's reuse window closed — the principal's credential expired with it; verify again (`POST …/kyc/sessions`). `rejected` and `withdrawn` are terminal. | `"pending_kyc"`, `"kyc_expired"`, `"approved"`, `"rejected"`, `"credential_held"`, `"active"`, `"withdrawn"` |
| `billing` | string | yes | The one-off entry fee at the operator's billing provider: none until a checkout starts, pending until the provider's signed event lands, active once it is recorded. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `agents` | [`AgentCounts`](#schema-agentcounts) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/kyc/result" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/agent-operators/{agentOperatorRef}/billing/checkout`

**Pay the entry fee on the operator's hosted checkout**

Opens the hosted checkout of the one-off entry fee, keyed by the
operator's reference and the tier of its operator type only (person
or organisation; the amount is on the checkout page). The person
enters legal and billing data there — none of it enters this
service, which keeps the billing state and an opaque customer
reference. `billing` reads `pending` until the provider's signed
event lands on `POST /v0/billing/events`, `active` afterwards. The
principal's credential is offered once the fee is recorded. Wallet
session of the operator's wallet required (401/403); 409 for a
rejected operator or once the fee is recorded.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Responses

`201` — The hosted checkout to open, and the operator's billing state.

Content type `application/json`.

Schema: [`BillingHandoff`](#schema-billinghandoff)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `url` | string (uri) | yes | The hosted checkout page — open it as-is. |  |
| `billing` | string | yes |  | `"none"`, `"pending"`, `"active"` |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/billing/checkout" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/credential/offer`

**Hand out the offer of the principal's identity credential, once**

Creates the offer of the operator's own identity credential — the
principal its agents are minted from — from its approved
verification, on the same issuer route as sellers' credentials, and
hands it out exactly once over the wallet session of the operator's
wallet (401/403). The offer is a one-shot pre-authorized code the
operator's own infrastructure redeems at once with the holder
library; the credential and its keys stay there — this service never
stores or logs the offer and never sees the credential, a key or a
proof. The operator's holder answers the issuer's mint challenge with
it when an agent is minted. The code and the credential expire
exactly with the verification (no grace). 409 until the entry fee is
recorded, unless approved, while a credential is held, after the
holder revoked it on this verification (verify again), or past the
reuse window; 501 when the substrate offers no credential rail.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Responses

`201` — The offer, handed out once, and when it expires.

Content type `application/json`.

Schema: [`CredentialOfferHandoff`](#schema-credentialofferhandoff)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `offer` | object | yes | The credential offer object, as the issuer returned it — opaque, one-shot; redeem it at once. |  |
| `expiresAt` | string (date-time) | yes | When the code and the credential it yields expire — the verification's own expiry at the latest. No grace. |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/credential/offer" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/credential`

**Report the principal's credential state from the operator's infrastructure**

The operator's infrastructure reports what it did with the offer:
`held` (optionally with an opaque reference of its choosing; the
operator reads `credential_held` and can have agents minted) or
`self_revoked` (the holder revoked the principal's credential through
the network's privacy relay; from the next status root on it
presents nowhere and the agents minted on it are revoked with it over
the following roots; `revoked` is accepted as the older spelling of
the same report). Nothing secret travels here; the reference is never
a key, a code or a proof. Wallet session of the operator's wallet
required (401/403).

A `self_revoked` report is recorded as the state change on the
operator's own record and nothing else: no event, no time, no log
line — the reference and the hand-out time are cleared. The way back
is a new verification (`POST …/kyc/sessions`), which yields a fresh
offer; another offer on the same verification answers 409.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Request body

`application/json` — required

Schema: [`AgentOperatorCredentialReport`](#schema-agentoperatorcredentialreport)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes | `held`, or `self_revoked` (`revoked` is the older spelling of the same report). | `"held"`, `"self_revoked"`, `"revoked"` |
| `credentialRef` | string | no | An opaque reference of the operator's choosing for the credential it holds. Never a key. |  |

#### Responses

`200` — The operator with the reported credential state.

Content type `application/json`.

Schema: [`AgentOperator`](#schema-agentoperator)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The operator's opaque reference (`aop_…`). |  |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from. |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `pending_kyc` → `approved` → `credential_held` (the principal's credential is held in the operator's infrastructure) → `active` (its first agent is minted). `kyc_expired`: the proofing deadline passed, or the approved verification's reuse window closed — the principal's credential expired with it; verify again (`POST …/kyc/sessions`). `rejected` and `withdrawn` are terminal. | `"pending_kyc"`, `"kyc_expired"`, `"approved"`, `"rejected"`, `"credential_held"`, `"active"`, `"withdrawn"` |
| `billing` | string | yes | The one-off entry fee at the operator's billing provider: none until a checkout starts, pending until the provider's signed event lands, active once it is recorded. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `agents` | [`AgentCounts`](#schema-agentcounts) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/credential" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/v0/agent-scopes`

**The delegation vocabulary an agent is minted with**

The scope values and key-custody classes an agent's delegation draws
from — the vocabulary carried on chain, read under its pinned content
hash (`contentSha256`), never hard-coded. The dashboard builds the
"Mint an agent" form from it. 503 when the content does not match its
pin (fail closed: no offer is validated against it); 501 when the
substrate supplies no vocabulary. Wallet session required (401).

Authentication: [`walletSession`](#auth-walletsession).

#### Responses

`200` — The vocabulary and the hash it was read under.

Content type `application/json`.

Schema: [`ScopeVocabulary`](#schema-scopevocabulary)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `contentSha256` | string | yes | The sha256 of the vocabulary content it was read under (hex). |  |
| `version` | string | no |  |  |
| `scopes` | array of string | yes |  |  |
| `custodyClasses` | array of string | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/agent-scopes" \
  -H "Authorization: Bearer <session-credential>"
```

### GET `/v0/agent-operators/{agentOperatorRef}/agents`

**The operator's agents (pseudonymous handles, state, lease)**

The agents minted for the operator, newest first: ZadQ's
pseudonymous handle (`agt_…`), the delegation summary, the lease and
the state — never a chain identifier, a key or the offer. Wallet
session of the operator's wallet required (401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Responses

`200` — The operator's agents.

Content type `application/json`.

Schema: [`Agents`](#schema-agents)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `items` | array of [`Agent`](#schema-agent) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/agent-operators/<agentOperatorRef>/agents" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/agents`

**Mint an agent — hand out the agent offer, once**

The operator declares the delegation (scope values from `GET
/v0/agent-scopes`, a per-transaction limit and its unit, the
key-custody class, a lease of 1..90 days). ZadQ records that it
collected its fee (the fee declaration) and creates the agent offer
on the issuer's operator route; the offer is handed out exactly once
over the wallet session of the operator's wallet (401/403) and never
stored or logged. The operator's agent client redeems it at once,
answering the issuer's mint challenge with the principal's
credential inside its 10-second window; the agent credential and its
keys stay in the operator's infrastructure, and the link between the
principal and the agent stays sealed with the issuer — this service
never sees it. The agent expires at the end of its lease or with the
principal's verification, whichever is first (`expiresAt`).

Refused here, before the issuer is asked: 400 for a delegation
outside the vocabulary or the lease bounds; 409 until the entry fee
is recorded, unless the principal's credential is held, or once the
verification's window is closed. The issuer enforces at most 10
active agents per person and 5 mints per sliding hour, renewals
included; ZadQ shows the answer, it does not re-count. Its refusals
are 409 and terminal — the problem's title names the reason (active
agent cap, mint rate, verification expired, fee declaration) and
nothing is retried. 501 when the substrate offers no agent route.

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |

#### Request body

`application/json` — required

Schema: [`AgentDelegation`](#schema-agentdelegation)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `scope` | array of string | yes | Scope values of the vocabulary (`GET /v0/agent-scopes`), each once. |  |
| `limit` | [`AgentLimit`](#schema-agentlimit) | yes |  |  |
| `custodyClass` | string | yes | The key-custody class of the agent's key, from the vocabulary. |  |
| `leaseDays` | integer | yes | The lease in days; the issuer allows at most 90. |  |

#### Responses

`201` — The agent offer, handed out once, and the agent's record.

Content type `application/json`.

Schema: [`AgentOfferHandoff`](#schema-agentofferhandoff)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `offer` | object | yes | The agent offer as the issuer returned it — opaque, one-shot; the operator's agent client redeems it at once. |  |
| `expiresAt` | string (date-time) | yes | When the agent's credential expires — its lease's end or the principal's verification, whichever is first. |  |
| `agent` | [`Agent`](#schema-agent) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/agents" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### GET `/v0/agent-operators/{agentOperatorRef}/agents/{agentId}`

**One agent (state, reason, delegation, lease)**

One agent of the operator. Another operator's agent reads 404.
Wallet session of the operator's wallet required (401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |
| `agentId` | path | yes | string | The agent's pseudonymous handle (`agt_…`). |

#### Responses

`200` — The agent.

Content type `application/json`.

Schema: [`Agent`](#schema-agent)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The agent's pseudonymous handle (`agt_…`), assigned by ZadQ. |  |
| `state` | string | yes |  | `"offered"`, `"held"`, `"revoked"`, `"expired"` |
| `reason` | string | no |  | `"operator_revoked"`, `"principal_revoked"`, `"self_revoked"`, `"lease_expired"` |
| `delegation` | [`AgentDelegation`](#schema-agentdelegation) | yes |  |  |
| `leaseUntil` | string (date-time) | yes | The lease's end as the issuer set it. |  |
| `generation` | integer | yes | 1 at the mint, one more per renewal (replacement). |  |
| `credentialRef` | string | no | The operator's own opaque reference for the held credential. Never a key. |  |
| `feeDeclared` | boolean | yes | Whether ZadQ's fee declaration for the current generation is recorded. |  |
| `offeredAt` | string (date-time) | yes |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS "<base-url>/v0/agent-operators/<agentOperatorRef>/agents/<agentId>" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/agents/{agentId}/credential`

**Report the agent credential's state from the operator's infrastructure**

The operator's agent client reports what it did with the offer:
`held` (optionally with an opaque reference of its choosing; the
operator's first agent held makes it `active`) or `self_revoked` (the
agent's holder revoked it through the network's privacy relay). A
`self_revoked` report is recorded as the state and the reason and
nothing else — no event, no time, no log line. Nothing secret
travels here. Wallet session of the operator's wallet required
(401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |
| `agentId` | path | yes | string | The agent's pseudonymous handle (`agt_…`). |

#### Request body

`application/json` — required

Schema: [`AgentCredentialReport`](#schema-agentcredentialreport)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes |  | `"held"`, `"self_revoked"` |
| `credentialRef` | string | no | An opaque reference of the operator's choosing for the agent's credential. Never a key. |  |

#### Responses

`200` — The agent with the reported state.

Content type `application/json`.

Schema: [`Agent`](#schema-agent)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The agent's pseudonymous handle (`agt_…`), assigned by ZadQ. |  |
| `state` | string | yes |  | `"offered"`, `"held"`, `"revoked"`, `"expired"` |
| `reason` | string | no |  | `"operator_revoked"`, `"principal_revoked"`, `"self_revoked"`, `"lease_expired"` |
| `delegation` | [`AgentDelegation`](#schema-agentdelegation) | yes |  |  |
| `leaseUntil` | string (date-time) | yes | The lease's end as the issuer set it. |  |
| `generation` | integer | yes | 1 at the mint, one more per renewal (replacement). |  |
| `credentialRef` | string | no | The operator's own opaque reference for the held credential. Never a key. |  |
| `feeDeclared` | boolean | yes | Whether ZadQ's fee declaration for the current generation is recorded. |  |
| `offeredAt` | string (date-time) | yes |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/agents/<agentId>/credential" \
  -H "Authorization: Bearer <session-credential>" \
  -H "Content-Type: application/json" \
  -d @request-body.json
```

### POST `/v0/agent-operators/{agentOperatorRef}/agents/{agentId}/renew`

**Renew an agent — replacement, with a new offer handed out once**

Renewal is replacement: a new credential with the same delegation
and a new lease, the old one revoked at the next root. ZadQ records
the renewal's fee declaration first, then asks the issuer; the new
offer is handed out once, like a mint, and the agent reads `offered`
(generation + 1) until the new credential is reported held. A
renewal counts against the issuer's mint rate. A revoked or expired
agent is not renewable (409): mint a new one. The issuer's refusals
are 409 and terminal. Wallet session of the operator's wallet
required (401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |
| `agentId` | path | yes | string | The agent's pseudonymous handle (`agt_…`). |

#### Responses

`201` — The renewal offer, handed out once, and the agent's record.

Content type `application/json`.

Schema: [`AgentOfferHandoff`](#schema-agentofferhandoff)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `offer` | object | yes | The agent offer as the issuer returned it — opaque, one-shot; the operator's agent client redeems it at once. |  |
| `expiresAt` | string (date-time) | yes | When the agent's credential expires — its lease's end or the principal's verification, whichever is first. |  |
| `agent` | [`Agent`](#schema-agent) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/agents/<agentId>/renew" \
  -H "Authorization: Bearer <session-credential>"
```

### POST `/v0/agent-operators/{agentOperatorRef}/agents/{agentId}/revoke`

**Revoke an agent (the operator's decision)**

The operator revokes the agent: the issuer's status change, then the
agent reads `revoked` with reason `operator_revoked`; from the next
status root on its credential presents nowhere. 409 for an agent
already revoked or expired. Wallet session of the operator's wallet
required (401/403).

Authentication: [`walletSession`](#auth-walletsession).

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `agentOperatorRef` | path | yes | string | The agent operator's opaque reference (`aop_…`). |
| `agentId` | path | yes | string | The agent's pseudonymous handle (`agt_…`). |

#### Responses

`200` — The revoked agent.

Content type `application/json`.

Schema: [`Agent`](#schema-agent)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The agent's pseudonymous handle (`agt_…`), assigned by ZadQ. |  |
| `state` | string | yes |  | `"offered"`, `"held"`, `"revoked"`, `"expired"` |
| `reason` | string | no |  | `"operator_revoked"`, `"principal_revoked"`, `"self_revoked"`, `"lease_expired"` |
| `delegation` | [`AgentDelegation`](#schema-agentdelegation) | yes |  |  |
| `leaseUntil` | string (date-time) | yes | The lease's end as the issuer set it. |  |
| `generation` | integer | yes | 1 at the mint, one more per renewal (replacement). |  |
| `credentialRef` | string | no | The operator's own opaque reference for the held credential. Never a key. |  |
| `feeDeclared` | boolean | yes | Whether ZadQ's fee declaration for the current generation is recorded. |  |
| `offeredAt` | string (date-time) | yes |  |  |
| `createdAt` | string (date-time) | yes |  |  |

`default` — RFC 9457 problem details.

Content type `application/problem+json`.

Schema: [`Problem`](#schema-problem)

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |

#### Example

```bash
curl -sS -X POST "<base-url>/v0/agent-operators/<agentOperatorRef>/agents/<agentId>/revoke" \
  -H "Authorization: Bearer <session-credential>"
```

## Schemas

### `TermsDocument`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `version` | string | yes | Version name of the acknowledgement document. |  |
| `sha256` | string | yes | Hex SHA-256 of `text`. |  |
| `url` | string (uri) | yes | Where the full Terms of Service are published. |  |
| `text` | string | yes | The acknowledgement text the wallet signs for. |  |

### `SellerTerms`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `current` | [`TermsDocument`](#schema-termsdocument) | yes |  |  |
| `acknowledged` | boolean | yes | Whether the current version is acknowledged by this seller. |  |
| `acknowledgedVersion` | string | no |  |  |
| `signedAt` | string (date-time) | no | When the current version was signed, if it was. |  |
| `message` | string | yes | The exact text to sign for the current version. |  |
| `messageSignedAt` | string (date-time) | yes | The signing time embedded in `message`; submit it back as `signedAt`. |  |

### `TermsAcknowledgement`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `version` | string | yes |  |  |
| `sha256` | string | yes |  |  |
| `signedAt` | string (date-time) | yes |  |  |
| `signature` | string | yes | Wallet signature over the message (hex for EVM, base58 or hex for Solana). |  |

### `SellerWebhooks`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `webhooks` | array of [`Webhook`](#schema-webhook) | yes |  |  |

### `SellerEvent`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | yes | Event name of the seller stream (e.g. intake, kyc_approved, attested, status_suspended, consent_withdrawn). |  |
| `occurredAt` | string (date-time) | yes |  |  |

### `SellerEvents`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `events` | array of [`SellerEvent`](#schema-sellerevent) | yes |  |  |

### `Chain`

The payment rail of a `payTo` address (Platform Decision 14, ZADQ-66):
USDC on Base (`base`, an EVM address, stored and compared lowercase) or
on Solana (`solana`, case-sensitive). Any other value is refused with a
400 problem; other EVM chains are not declared until a seller needs one.

Type: string, one of `"base"`, `"solana"`.

### `AuthChallengeRequest`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes |  |  |

### `AuthChallenge`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `nonce` | string | yes |  |  |
| `message` | string | yes | Exact text to sign, byte for byte. |  |
| `expiresAt` | string (date-time) | yes |  |  |

### `AuthSessionRequest`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes |  |  |
| `nonce` | string | yes |  |  |
| `signature` | string | yes | EVM — 0x-hex r\|\|s\|\|v from personal_sign; Solana — base58 (or hex) 64-byte signature. |  |

### `AuthSession`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `bearer` | string | yes | Opaque session credential for the Authorization header. |  |
| `expiresAt` | string (date-time) | yes |  |  |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes |  |  |

### `Health`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `status` | string | yes |  | `"ok"` |
| `version` | string | no |  |  |

### `CredentialOfferHandoff`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `offer` | object | yes | The credential offer object, as the issuer returned it — opaque, one-shot; redeem it at once. |  |
| `expiresAt` | string (date-time) | yes | When the code and the credential it yields expire — the verification's own expiry at the latest. No grace. |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | yes |  |  |

### `CredentialReport`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes | `held`, or `self_revoked` (`revoked` is the older spelling of the same report). | `"held"`, `"self_revoked"`, `"revoked"` |
| `credentialRef` | string | no | An opaque reference of the seller's choosing for the credential it holds. Never a key. |  |
| `presentationUrl` | string (uri) | no | Where the seller's holder answers this service's challenges (ZADQ-84): `POST` of the issued challenge as the verifier returned it (`{request, signature_hex}`), `200` with the envelope (`{proof_hex, issuer_id, status_root, claimed_predicates}`); any other status means the holder cannot present this time. Only with `held`; kept when omitted on a later report. |  |

### `CredentialState`

The v1 credential the seller holds in its own infrastructure, as
this service knows it (ZADQ-83): state only. `expired` is derived —
an offered or held credential past the verification's expiry — since
a credential lasts exactly what the verification lasts. `revoked`:
revoked by the issuer's policy (consent withdrawn). `self_revoked`
(ZADQ-89): revoked by its holder through the network's privacy relay;
terminal on this verification, with nothing recorded next to it — the
way back is a new verification.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes |  | `"none"`, `"offered"`, `"held"`, `"expired"`, `"revoked"`, `"self_revoked"` |
| `offeredAt` | string (date-time) | no |  |  |
| `expiresAt` | string (date-time) | no | The verification's expiry, which is the credential's. |  |
| `credentialRef` | string | no |  |  |
| `presentationUrl` | string (uri) | no | The holder's presentation endpoint registered with `held` (ZADQ-84); absent on the v0 path. |  |
| `lastPresentation` | [`PresentationOutcome`](#schema-presentationoutcome) | no |  |  |

### `PresentationOutcome`

The outcome of the last cadence presentation of the held credential
(ZADQ-84), as the verifier's own decision record keeps it: never the
challenge, the envelope or a proof. `stage` says where it ended —
`challenge` (no challenge issued: the verifier's proven state was
unavailable), `transport` (the holder did not present:
holder_unreachable, holder_refused, envelope_malformed) or
`decision` (the verifier decided; `reason` is its code — `accepted`,
or one of its closed vocabulary such as proof_invalid,
predicate_missing, deadline_exceeded, status_root_stale,
request_not_ours). A refused decision withholds the positive claim
on the public verdict; the public `reason` stays in the standing
vocabulary, so the exact code lives here and on the seller's channel.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `stage` | string | yes |  | `"challenge"`, `"transport"`, `"decision"` |
| `accepted` | boolean | yes |  |  |
| `reason` | string | yes |  |  |
| `sessionId` | string | no | The verifier's session id of the decision record. |  |
| `decidedAt` | string (date-time) | yes |  |  |

### `SubstrateStatus`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `substrate` | string | yes |  | `"ok"`, `"degraded"`, `"unknown"` |
| `source` | string | yes | Where the state comes from — the verifier's status read, or this service's cadence accounting. | `"verifier"`, `"cadence"` |
| `reason` | string | no | Why the substrate is not ok, in the verifier's standing vocabulary (for example `height_stale`, `root_expired`, `state_unavailable`). Absent when ok. |  |
| `checkedAt` | string (date-time) | yes |  |  |
| `lastSuccessfulStandingRead` | string (date-time) | no | The last time a standing read answered with a known status. Absent before the first. |  |
| `cadence` | object | yes |  |  |
| `cadence.recheckEveryHours` | integer | yes | The published cadence promise. |  |
| `cadence.lastCycleAt` | string (date-time) | no |  |  |
| `cadence.checked` | integer | no | Attestations the last cycle asked the verifier about. |  |
| `cadence.withheld` | integer | no | Of those, how many came back with an unknown standing. |  |

### `BillingCheckout`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `tier` | string | yes | The schedule tier, by name. Amounts are on the checkout page and in the operator agreement. | `"attested_vendor"`, `"regulated_compliance"` |

### `BillingHandoff`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `url` | string (uri) | yes | The hosted checkout page — open it as-is. |  |
| `billing` | string | yes |  | `"none"`, `"pending"`, `"active"` |

### `BillingEventReceipt`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `applied` | boolean | yes |  |  |

### `AddressProof`

Proof of control of a payment address: the nonce of an auth challenge
issued for it and the wallet's signature over the challenge message.
Consumed on use, valid or not, like a login.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `nonce` | string | yes |  |  |
| `signature` | string | yes |  |  |

### `EndpointAddition`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `resourceUrl` | string (uri) | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | no |  |  |
| `proof` | [`AddressProof`](#schema-addressproof) | no |  |  |

### `WalletRotation`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `proof` | [`AddressProof`](#schema-addressproof) | yes |  |  |

### `OperatorEndpoints`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `endpoints` | array of [`EndpointSummary`](#schema-endpointsummary) | yes |  |  |

### `EndpointSummary`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `sellerId` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string | yes |  |  |
| `state` | string | yes |  | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `attestationStatus` | string | no |  | `"active"`, `"suspended"`, `"revoked"` |

### `InterestForm`

The register-interest form's fields; contact data is not among them.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `side` | string | yes | Which side of the trust perimeter the person stands on. | `"vendor"`, `"agent_operator"` |
| `resource_url` | string (uri) | no | The x402 resource URL a vendor sells at (optional, vendors). |  |
| `agent_count_band` | string | no | The size band of an operator's fleet (optional, agent operators). | `"up_to_10"`, `"up_to_100"`, `"more_than_100"` |
| `website` | string | no | Trap field for automated submitters; a person leaves it empty. |  |

### `SellerIntake`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `segment` | [`SegmentDeclaration`](#schema-segmentdeclaration) | yes |  |  |

### `PayTo`

Chain-qualified x402 payment address.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `chain` | [`Chain`](#schema-chain) | yes |  |  |
| `address` | string | yes | The address that receives the payment; on `base` stored and compared lowercase. |  |

### `SegmentDeclaration`

PARAM-29 declaration. Recorded, not enforced in v0.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `annualVolumeUsdAtLeast300` | boolean | yes |  |  |
| `regulatedB2bCounterparties` | boolean | yes |  |  |

### `Seller`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `payTo` | [`PayTo`](#schema-payto) | yes |  |  |
| `resourceUrl` | string (uri) | yes |  |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `kyc_expired`: the proofing session ran past its deadline without an outcome; a new session can be opened (`POST …/kyc/sessions`). `withdrawn`: consent to the service withdrawn — terminal. | `"pending_kyc"`, `"kyc_expired"`, `"admitted"`, `"rejected"`, `"withdrawn"` |
| `billing` | string | yes | Billing state at the operator's billing provider (ZADQ-54): none until a checkout starts, pending until the provider's signed event lands, active afterwards. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `billingTier` | string | no | The schedule tier chosen at checkout, by name. | `"attested_vendor"`, `"regulated_compliance"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `renewal` | all of [`KycHandoff`](#schema-kychandoff) | no | The pending re-verification session, while one is open (ZADQ-91): the person completes it on `handoffUrl`; the approved session in `kyc` keeps backing the attestation meanwhile. |  |
| `attestation` | [`Attestation`](#schema-attestation) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

### `KycHandoff`

The person's side of the KYC session (ZADQ-52). `handoffUrl` is the
identity provider's hosted page where the proofing is completed — open
it as-is; it is present only while the session is pending. Nothing
here is identity data.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes |  | `"pending"`, `"approved"`, `"rejected"`, `"expired"`, `"withdrawn"` |
| `handoffUrl` | string (uri) | no |  |  |
| `expiresAt` | string (date-time) | no | Proofing deadline of the current session. |  |
| `reusableUntil` | string (date-time) | no | End of the approved session's reuse window (approval + the grade's validity — 12 months at grades 1–2, 6 at grade 3, 3 at grade 4). No grace and no extension: the attestation is suspended `rekyc_overdue` at the first root after it unless a re-verification was bound before. Re-verify ahead of it (`POST …/kyc/sessions`). |  |
| `freshnessBand` | string | no | Age of the last successful proofing as a band (F4 < 30 days, F3 < 90, F2 < 180, F1 < 365) — never a date. "Human-backed at G" means grade ≥ 2 and freshness ≥ F1; the issuer enforces it. | `"F4"`, `"F3"`, `"F2"`, `"F1"` |
| `reverificationDueAt` | string (date-time) | no | When "re-verification due" starts: `reusableUntil` minus the service's lead (ZADQ-79). Present while approved and no re-verification is pending. The issuer extends nothing; this is the whole of the notice. |  |
| `reverificationDue` | boolean | no | True from `reverificationDueAt` until the window ends: re-verify now (`POST …/kyc/sessions`) so the attestation keeps its standing and its reference. Past the window the issuer refuses every step with 409 session_reuse_expired — terminal: the seller reads `kyc_expired` and starts a new verification. |  |

### `KycResult`

Outcome metadata only — the contract carries no identity data.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `sessionId` | string | yes |  |  |
| `outcome` | string | yes |  | `"approved"`, `"rejected"` |
| `assuranceLevel` | string | no | Assurance grade met by the proofing process (e.g. "G"). |  |

### `Attestation`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `sellerId` | string | yes |  |  |
| `assuranceLevel` | string | yes |  |  |
| `status` | string | yes |  | `"active"`, `"suspended"`, `"revoked"` |
| `statusReason` | string | no | Why a non-active status was set — never silent. `rekyc_overdue`: the reuse window ended; restored under the same reference by a bound re-verification. `consent_withdrawn`: terminal. `self_revoked`: the holder revoked its own credential and the attestation was retired at the next verdict cadence — terminal for this attestation; a new verification yields a new one. Absent for operator changes and while active. | `"rekyc_overdue"`, `"consent_withdrawn"`, `"self_revoked"` |
| `freshnessBand` | string | no | Freshness band of the backing session, as the issuer reports it. | `"F4"`, `"F3"`, `"F2"`, `"F1"` |
| `issuedAt` | string (date-time) | yes |  |  |
| `updatedAt` | string (date-time) | yes |  |  |

### `AttestationReceipt`

The `attestation-ref/v0` payload, field for field (snake_case as the
issuer signs it): Ed25519 over `"mintid/seller-attestation/v0" || 0x00
|| SHA-256(canonical JSON of the seven fields but signature)`. Only
opaque handles and public endpoint identifiers.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `attestation_ref` | string | yes | 32-byte opaque reference, hex. |  |
| `issuer_id` | string | yes |  |  |
| `chain` | string | yes |  |  |
| `pay_to` | string | yes |  |  |
| `resource_url` | string | yes |  |  |
| `assurance_grade` | integer | yes |  |  |
| `issued_at_unix` | integer | yes |  |  |
| `signature` | string | yes | Ed25519 by the issuer's registered status signing key, hex (64 bytes). |  |

### `AttestationStatusUpdate`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `status` | string | yes |  | `"active"`, `"suspended"`, `"revoked"` |

### `Verdict`

The explorer-consumable signed verdict: "human-backed at assurance ≥ G,
verified at T, re-checked every N hours." Staleness is policy-driven.
Funded-recourse policy: the positive claim is published only while the
issuer's guarantee deposit meets the published minimum; otherwise the
outcome is `no_signal` with `reason: bond_insufficient`. Every
non-positive outcome carries `reason` from the closed vocabulary
(ZADQ-78); `human_backed` never does.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `outcome` | string | yes |  | `"human_backed"`, `"revoked"`, `"stale"`, `"no_signal"` |
| `subject` | object | yes |  |  |
| `subject.chain` | [`Chain`](#schema-chain) | yes |  |  |
| `subject.payTo` | string | yes |  |  |
| `subject.resourceUrl` | string | yes |  |  |
| `assuranceLevel` | string | no |  |  |
| `verifiedAt` | string (date-time) | no |  |  |
| `recheckEveryHours` | integer | yes |  |  |
| `signature` | string | no | Ed25519 over the canonical verdict payload, base64. |  |
| `keyId` | string | no |  |  |
| `reason` | string | no | Why the outcome is not `human_backed`: the standing read's reason code (closed vocabulary), or `suspended` when the witnessed status is suspended. Absent on `human_backed`; absent on `revoked` too. The policy reason of a suspension (`rekyc_overdue`) is the seller's own record, never part of the public verdict. On the v1 path (ZADQ-84) a verifier-side condition of the presentation decision is carried with the same code (`issuer_not_active`, `status_root_stale`, `height_stale`) or as `state_unavailable`; a presentation the verifier refused on the holder's side withholds the positive claim with no `reason` — the decision's exact code is the seller's own record (`credential.lastPresentation`). | `"state_unavailable"`, `"issuer_unknown"`, `"issuer_not_active"`, `"status_root_stale"`, `"height_stale"`, `"witness_unavailable"`, `"witness_malformed"`, `"witness_invalid"`, `"witness_root_mismatch"`, `"bond_insufficient"`, `"suspended"` |

### `WebhookRegistration`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `url` | string (uri) | yes |  |  |

### `Webhook`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  |  |
| `url` | string (uri) | yes |  |  |
| `secret` | string | no | Hex signing secret for X-Signature. Returned once at registration. |  |
| `createdAt` | string (date-time) | yes |  |  |

### `VerificationKeys`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `keys` | array of [`VerificationKey`](#schema-verificationkey) | yes |  |  |

### `VerificationKey`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `keyId` | string | yes | First 8 bytes of the public key's SHA-256, hex. |  |
| `algorithm` | string | yes |  | `"Ed25519"` |
| `publicKey` | string | yes | Raw 32-byte Ed25519 public key, base64. |  |
| `status` | string | no | `current` signs new verdicts; `previous` is a key a rotation retired, kept published for at least the re-check window so verdicts signed before the rotation keep verifying (select the key by `keyId`, never by position). | `"current"`, `"previous"` |

### `AgentOperatorEnrolment`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from — the one the wallet session proves. |  |
| `operatorType` | string | yes | `person` is verified by identity verification, `organisation` by business verification; the boundary learns this and nothing else. | `"person"`, `"organisation"` |

### `AgentOperator`

The accountable party behind an agent, as this service keeps it
(ZADQ-85): the wallet the operator's agent pays from, the operator
type, the verification, the entry fee and the principal's identity
credential — state only; the dashboard shows state, never keys.
Agents present proof that an accountable operator stands behind
them; they never disclose who.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The operator's opaque reference (`aop_…`). |  |
| `wallet` | all of [`PayTo`](#schema-payto) | yes | The wallet the operator's agent pays from. |  |
| `operatorType` | string | yes |  | `"person"`, `"organisation"` |
| `state` | string | yes | `pending_kyc` → `approved` → `credential_held` (the principal's credential is held in the operator's infrastructure) → `active` (its first agent is minted). `kyc_expired`: the proofing deadline passed, or the approved verification's reuse window closed — the principal's credential expired with it; verify again (`POST …/kyc/sessions`). `rejected` and `withdrawn` are terminal. | `"pending_kyc"`, `"kyc_expired"`, `"approved"`, `"rejected"`, `"credential_held"`, `"active"`, `"withdrawn"` |
| `billing` | string | yes | The one-off entry fee at the operator's billing provider: none until a checkout starts, pending until the provider's signed event lands, active once it is recorded. The service holds no legal or payment data. | `"none"`, `"pending"`, `"active"` |
| `kycSessionId` | string | no | Opaque session handle at the KYC boundary. Not identity data. |  |
| `kyc` | [`KycHandoff`](#schema-kychandoff) | no |  |  |
| `credential` | [`CredentialState`](#schema-credentialstate) | no |  |  |
| `agents` | [`AgentCounts`](#schema-agentcounts) | no |  |  |
| `createdAt` | string (date-time) | yes |  |  |

### `AgentOperatorCredentialReport`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes | `held`, or `self_revoked` (`revoked` is the older spelling of the same report). | `"held"`, `"self_revoked"`, `"revoked"` |
| `credentialRef` | string | no | An opaque reference of the operator's choosing for the credential it holds. Never a key. |  |

### `ScopeVocabulary`

The delegation vocabulary carried on chain, as read under its pinned
content hash: the scope values and the key-custody classes an agent
offer may name.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `contentSha256` | string | yes | The sha256 of the vocabulary content it was read under (hex). |  |
| `version` | string | no |  |  |
| `scopes` | array of string | yes |  |  |
| `custodyClasses` | array of string | yes |  |  |

### `AgentLimit`

The per-transaction limit the issuer signs into the agent's delegation.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `amount` | string | yes | A positive decimal amount (up to 8 decimals), as a string. |  |
| `unit` | string | yes | The limit's unit — a short currency code (for instance `USDC`). |  |

### `AgentDelegation`

What the agent is allowed to do, as the operator declares it and the issuer signs it.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `scope` | array of string | yes | Scope values of the vocabulary (`GET /v0/agent-scopes`), each once. |  |
| `limit` | [`AgentLimit`](#schema-agentlimit) | yes |  |  |
| `custodyClass` | string | yes | The key-custody class of the agent's key, from the vocabulary. |  |
| `leaseDays` | integer | yes | The lease in days; the issuer allows at most 90. |  |

### `Agent`

An agent minted for the operator, as this service keeps it: ZadQ's
pseudonymous handle — never a chain identifier —, the delegation
summary, the lease and the state; never a key, the offer or a link
to a person. States: `offered` (handed out, not yet reported held;
again after a renewal) → `held`; `revoked` and `expired` are
terminal, with the reason: `operator_revoked` (the operator revoked
it), `principal_revoked` (the principal stopped backing its agents —
self-revoked, verification window closed or consent withdrawn —
whatever triggered it), `self_revoked` (the agent's holder revoked
it) or `lease_expired` (past its lease).

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes | The agent's pseudonymous handle (`agt_…`), assigned by ZadQ. |  |
| `state` | string | yes |  | `"offered"`, `"held"`, `"revoked"`, `"expired"` |
| `reason` | string | no |  | `"operator_revoked"`, `"principal_revoked"`, `"self_revoked"`, `"lease_expired"` |
| `delegation` | [`AgentDelegation`](#schema-agentdelegation) | yes |  |  |
| `leaseUntil` | string (date-time) | yes | The lease's end as the issuer set it. |  |
| `generation` | integer | yes | 1 at the mint, one more per renewal (replacement). |  |
| `credentialRef` | string | no | The operator's own opaque reference for the held credential. Never a key. |  |
| `feeDeclared` | boolean | yes | Whether ZadQ's fee declaration for the current generation is recorded. |  |
| `offeredAt` | string (date-time) | yes |  |  |
| `createdAt` | string (date-time) | yes |  |  |

### `Agents`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `items` | array of [`Agent`](#schema-agent) | yes |  |  |

### `AgentOfferHandoff`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `offer` | object | yes | The agent offer as the issuer returned it — opaque, one-shot; the operator's agent client redeems it at once. |  |
| `expiresAt` | string (date-time) | yes | When the agent's credential expires — its lease's end or the principal's verification, whichever is first. |  |
| `agent` | [`Agent`](#schema-agent) | yes |  |  |

### `AgentCredentialReport`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `state` | string | yes |  | `"held"`, `"self_revoked"` |
| `credentialRef` | string | no | An opaque reference of the operator's choosing for the agent's credential. Never a key. |  |

### `AgentCounts`

The operator's live agents (offered or held), for display. The
issuer enforces at most 10 active agents per person and 5 mints per
sliding hour, renewals included; ZadQ shows the answer, it does not
re-count.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `active` | integer | yes |  |  |

### `MetricsReport`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `schema` | string | yes | The wire shape's name (`published-metrics/v1`). |  |
| `computedAt` | string (date-time) | yes |  |  |
| `asOf` | string (date) | yes | The cut-off — the last day of the last complete month (UTC). Partial months are never published. |  |
| `period` | [`MetricsPeriod`](#schema-metricsperiod) | no |  |  |
| `cadence` | object | yes |  |  |
| `cadence.publication` | string | yes | `monthly`. |  |
| `cadence.snapshot` | string | yes | `quarterly`, aligned to the two-consecutive-quarters clock. |  |
| `relatedParty` | boolean | yes | Always true — the operator's position is a property of the dataset. |  |
| `operator` | [`MetricsOperator`](#schema-metricsoperator) | yes |  |  |
| `series` | array of [`MetricsSeries`](#schema-metricsseries) | yes |  |  |
| `contextNote` | string | yes |  |  |

### `MetricsPeriod`

Inclusive month range, `YYYY-MM` at both ends. Absent while no complete month holds a record.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `from` | string | yes |  |  |
| `to` | string | yes |  |  |

### `MetricsOperator`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `service` | string | yes |  |  |
| `operator` | string | yes |  |  |
| `relatedParty` | boolean | yes |  |  |
| `position` | string | yes |  |  |
| `disclosure` | string | yes | The canonical metrics disclosure, embedded in every report. |  |

### `MetricsSeries`

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `id` | string | yes |  | `"sellers_attested"`, `"retention_r90"`, `"verifications_free_path"`, `"independent_verifiers"`, `"anchor_service_revenue"` |
| `title` | string | yes |  |  |
| `discipline` | string | yes |  | `"registry-based"`, `"aggregate"`, `"context"` |
| `definition` | string | yes |  |  |
| `relatedParty` | boolean | yes | Always true, on every series. |  |
| `period` | [`MetricsPeriod`](#schema-metricsperiod) | no |  |  |
| `available` | boolean | yes | False when the records hold no source for the series; `unavailableReason` says why and `points` is empty. |  |
| `unavailableReason` | string | no |  |  |
| `points` | array of [`MetricsPoint`](#schema-metricspoint) | yes |  |  |

### `MetricsPoint`

One month-end observation. Which fields carry a value depends on the
series: `cumulative`, `active`, `independent` (sellers_attested);
`cohort`, `retained`, `r` (retention_r90; `r` absent while the cohort
is empty); `count`, `registrations` (independent_verifiers).

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `period` | string | yes | `YYYY-MM`. |  |
| `cumulative` | integer | no |  |  |
| `active` | integer | no |  |  |
| `independent` | integer | no |  |  |
| `cohort` | integer | no |  |  |
| `retained` | integer | no |  |  |
| `r` | number | no | Ratio, two decimals. |  |
| `count` | integer | no |  |  |
| `registrations` | integer | no |  |  |

### `Problem`

RFC 9457 problem details.

| Property | Type | Required | Description | Values |
| --- | --- | --- | --- | --- |
| `type` | string | no |  |  |
| `title` | string | no |  |  |
| `status` | integer | no |  |  |
| `detail` | string | no |  |  |
| `instance` | string | no |  |  |
