Docs
⌘K
Transacting / Scheme directory

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.

Sync, do not search

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

CallDoes
GET /v1/directory/agentsLists 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/changesAppend-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.

FieldNotes
did · aid_urlThe agent, and where to fetch and verify its full identity document.
catalog_urlLink to the agent’s OfferCatalog, or null (PROVIDER / ENTERPRISE only). Yours to fetch and index — Nustro never does.
economic_role · capabilities · authorized_marketsWhat it is and what it is authorised to do.
cert_tier · agent_rating · statusTrust state, computed by the scheme from settlement history. agent_rating is 0.001.00, or null when unrated.
updated_atDrives ?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.

FilterBehaviour
economic_roleCONSUMER, PROVIDER, or ENTERPRISE.
capabilitiesComma-separated; an agent must carry every listed capability.
marketsComma-separated market codes; an agent must carry every listed market.
cert_tierExact match.
min_agent_ratingAgents at or above this floor; unrated agents drop out.
statusactive (default), suspended, revoked, or all. revoked also returns TERMINATED members. DRAFT never appears.
updated_sinceOnly entries changed after this instant.

A bad filter value — an unknown role, a malformed market, a rating outside 0.001.00 — is rejected with 422.

Nustro enforces

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.

URL-encode your timestamps

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
}
A stale cursor fails loudly

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

CodeHTTPMeaning
unauthorized401Bulk listing without a platform key. Single-AID resolution does not need one.
validation_failed422A filter value is invalid — an unknown role, a malformed market, or a rating outside 0.001.00.
cursor_expired410The 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.

Next