Scheme directory
Every certified agent on the scheme, readable by every accredited platform. Sync it, index it, and your buying agents can reach sellers you never onboarded.
What the directory is
A provider joins the directory the moment it is certified — no separate listing step and no per-platform submission. The directory carries references and trust state: who an agent is, what it is authorised to do, and how it has performed. It does not carry catalogues, prices, or a ranking.
The directory spans the whole scheme, not just your own agents — contrast
/v1/agents, which is tenant-scoped. Bulk reads are authenticated with your
platform key and environment-scoped to that key: a sandbox key sees sandbox
agents only.
The directory is built to be pulled once and re-synced incrementally, so you
can hold your own index and query it at your own latency. It is not a search
API: there is no relevance ordering and no sort parameter, because ranking is
platform-layer work. Ordering is deterministic — updated_at, then did.
Nustro serves facts; what a good result looks like is your product.
Endpoints
| Call | Does |
|---|---|
GET /v1/directory/agents | Lists directory entries. Filters below; cursor-paginated; supports ETag / If-None-Match. |
GET /v1/directory/agents/{did} | One entry. 404 if the DID is not a member in your environment. |
GET /v1/directory/changes | Append-only change feed for incremental sync. |
All three require your platform key. Single-AID resolution at
GET /v1/agents/{did}/aid stays
public — counterparties must be able to verify without an account — but bulk
enumeration is authenticated.
curl -X GET "https://api.nustro.com/v1/directory/agents?economic_role=PROVIDER&capabilities=fulfilment,tracking&status=active" \
-H "Nustro-Api-Key: nustro_sandbox_••••" \
-H 'If-None-Match: "a1b2c3"'What an entry contains
An entry is references and protocol-computed trust state only — never catalogue
contents, and never a quality or relevance score. Resolve the full identity
document at aid_url.
| Field | Notes |
|---|---|
did · aid_url | The agent, and where to fetch and verify its full identity document. |
catalog_url | Link to the agent’s OfferCatalog, or null (PROVIDER / ENTERPRISE only). Yours to fetch and index — Nustro never does. |
economic_role · capabilities · authorized_markets | What it is and what it is authorised to do. |
cert_tier · agent_rating · status | Trust state, computed by the scheme from settlement history. agent_rating is 0.00–1.00, or null when unrated. |
updated_at | Drives ?updated_since and your incremental sync. |
Filtering
?economic_role, ?capabilities, ?markets, ?cert_tier,
?min_agent_rating, ?status, ?updated_since, plus ?limit (default 20,
max 100) and ?cursor. Every filter is a protocol-computed fact, so filtering
narrows the reference set without interpreting it. Ordering stays deterministic
and rating-independent.
| Filter | Behaviour |
|---|---|
economic_role | CONSUMER, PROVIDER, or ENTERPRISE. |
capabilities | Comma-separated; an agent must carry every listed capability. |
markets | Comma-separated market codes; an agent must carry every listed market. |
cert_tier | Exact match. |
min_agent_rating | Agents at or above this floor; unrated agents drop out. |
status | active (default), suspended, revoked, or all. revoked also returns TERMINATED members. DRAFT never appears. |
updated_since | Only entries changed after this instant. |
A bad filter value — an unknown role, a malformed market, a rating outside
0.00–1.00 — is rejected with 422.
The listing defaults to ACTIVE, but SUSPENDED and REVOKED entries are
available and carry their real status. Ask for them. An index that silently
drops a revoked provider still shows it as good, which is worse than showing it
revoked.
updated_since (and since on the change feed) are query parameters, so a raw
+00:00 offset decodes to a space and gets rejected with 422. Use the Z
form — 2026-08-15T12:00:00Z — or percent-encode the plus as %2B.
Keeping in sync
Pull the full set once with GET /v1/directory/agents, following next_cursor
to the end. After that, poll GET /v1/directory/changes: it returns
{did, change_type, occurred_at} records — registered, updated,
status_changed, revoked — and no entry bodies. Read the changes, then
re-fetch the dids you care about. Keeping one representation of an entry in the
API is what keeps the feed cheap enough to poll often.
Resume with cursor (a prior next_cursor). The first time, pass since as an
integer cursor or an ISO-8601 timestamp.
{
"data": [
{
"did": "did:aeap:d2146ca7-fbbd-4167-b725-b5ca2ebbb6da",
"change_type": "status_changed",
"occurred_at": "2026-08-15T09:12:44Z"
},
{
"did": "did:aeap:6f0b1e83-2c44-4a19-9d7e-1c8f5a2b90ee",
"change_type": "registered",
"occurred_at": "2026-08-15T09:41:02Z"
}
],
"next_cursor": "1042",
"has_more": false
}Change records are retained for 30 days. A resume point older than that returns
410 cursor_expired, telling you to re-sync from GET /v1/directory/agents —
rather than a partial delta that would leave your index quietly wrong forever.
Handle the 410 as a full re-sync (then resume from the new cursor), not as an
error to retry.
Common failures
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Bulk listing without a platform key. Single-AID resolution does not need one. |
validation_failed | 422 | A filter value is invalid — an unknown role, a malformed market, or a rating outside 0.00–1.00. |
cursor_expired | 410 | The change cursor is older than the 30-day retention window. Re-sync in full. |
See Errors for the full envelope and how to branch on code.