Aithos Agent Card Registry (0.2.0)

Download OpenAPI specification:

A public registry of signed A2A Agent Cards, where the key is the account.

Anyone may publish an Agent Card; the card is signed by its owner's key; only a holder of an authorized key can change it. There are no accounts, no passwords and no sessions, so no endpoint here takes a credential — a write is authorized by a signature over the operation, carried in the request body.

An entry may also certify ownership of domains, each proved by a record in that domain's own DNS zone.

Two documents are normative and this one is not:

  • SPEC.md — identity model, card processing, writes, public endpoints, limits, and the problem table.
  • DOMAIN-CERTIFICATION.md — the domain certification profile, additive to the above.

SPEC.md is normative and this document is not. Where the two disagree, SPEC.md wins and this document is the defect.

It is written by hand rather than derived from the code, for the same reason the field-presence table of SPEC.md §5.2 is: a description generated from an implementation documents that implementation, including its mistakes, and stops being a second opinion about what the protocol says. The cost of writing it by hand is drift, so drift is what the test guards — crates/registry-api/tests/openapi.rs fails when the operations here stop matching the router, or when the problem codes stop matching the catalogue, SPEC.md §9 or the published problem pages.

JSON rather than YAML because this file is served verbatim at /v1/openapi.json: there is no build step between what is reviewed here and what a client fetches, and the test reads it with the JSON parser the service already depends on.

read

Anonymous and read-only. Served from the object store behind the CDN, never from compute — which is why a withdrawn entry answers 404 here and the record (§7.3) is where its status is stated.

List entries, newest-updated first

Cursor-paginated. There is no search, no ranking and no scoring, and entries carry no badge of any kind.

Ties on updatedAt are broken by agentId, so the order is total and a cursor can neither skip nor repeat an entry. A client ends a listing on an empty page, not on a missing nextCursor: the registry may return a cursor whenever the page filled, without knowing whether anything follows.

query Parameters
limit
integer [ 1 .. 100 ]
Default: 25

Page size. Values outside the range are clamped, not refused.

cursor
string

Opaque, and belonging to the registry that issued it. One this registry did not issue — or one naming an entry it no longer holds — is 400 CURSOR_INVALID, never silently the first page.

Responses

Response samples

Content type
application/json
{
  • "agents": [
    ],
  • "nextCursor": "string"
}

The registration record for an entry

A projection: status, current digest, the authorized key set, timestamps, certified domains and same-origin links. It is a convenience, never a substitute for verifying the card.

This is also the only endpoint that distinguishes a withdrawn entry from one that never existed: it answers 200 with status: WITHDRAWN, where the card and key-set paths answer 404 for both.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

Responses

Response samples

Content type
application/json
{
  • "agentId": "NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY",
  • "status": "ACTIVE",
  • "seq": 1,
  • "cardDigest": "string",
  • "cardVersion": "string",
  • "authorizedKids": [
    ],
  • "createdAt": "2026-09-01T08:44:46.283Z",
  • "updatedAt": "2026-09-01T08:44:46.283Z",
  • "agentCardUrl": "http://example.com",
  • "jwksUrl": "http://example.com",
  • "domains": [
    ]
}

The current card, as the exact bytes that were signed

The body is cardBytes verbatim. The registry never re-serializes a stored card on read, because a re-serialized card is a different document and its signatures no longer verify.

ETag is opaque, in the sense of RFC 9110, and a client must not parse it as a digest. The API origin happens to derive it from the card digest; the CDN origin serves whatever validator the object store computed — two origins, two shapes, one endpoint. A client that needs the digest computes it from the bytes it received, which is not a workaround but the only correct behaviour: a digest handed over by the same server whose answer it is meant to check establishes nothing.

On 404 and 410. The API origin distinguishes a withdrawn entry from an absent one; the CDN origin, which serves objects the reconciler deletes on withdrawal, has nothing left to answer 410 from. Consumers MUST treat 404 from this path as "no current card" without inferring which of the two it means, and read the record (§7.3) when the difference matters.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

header Parameters
If-None-Match
string

Responses

Response samples

Content type
application/a2a+json
{
  • "name": "string",
  • "description": "string",
  • "version": "string",
  • "signatures": [
    ]
}

The currently authorized public keys

Each key carries its kid — the RFC 7638 thumbprint of that key, matching the kid in the protected header of any signature it produced.

The registry rebuilds each entry from the key material it verified rather than republishing the JWK as submitted, so a submitter cannot place a kid, an alg or any other member of their choosing into the published set. The only members served are kty, crv, the coordinates, kid and use: "sig".

A withdrawn entry serves nothing here, with the same 404 shape as the card path.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

Responses

Response samples

Content type
application/jwk-set+json
{
  • "keys": [
    ]
}

The publication history of an entry

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

query Parameters
limit
integer [ 1 .. 100 ]
Default: 25

Page size. Values outside the range are clamped, not refused.

cursor
string

Opaque, and belonging to the registry that issued it. One this registry did not issue — or one naming an entry it no longer holds — is 400 CURSOR_INVALID, never silently the first page.

Responses

Response samples

Content type
application/json
{
  • "versions": [
    ],
  • "nextCursor": "string"
}

One historical version, by its digest

Immutable exact bytes. The digest is in the request path, so it never needs carrying in a header, and these responses are cached for a year.

One version has one URL. {cardDigest} must be the exact form the registry publishes — sha256: then 64 lowercase hex characters — and any other spelling, the bare hex included, is 404: a second spelling would be a second permanently cached entry for the same bytes, with its own ETag.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

cardDigest
required
string (Digest) ^sha256:[0-9a-f]{64}$

Exactly as the registry publishes it. Any other spelling is 404.

Responses

Response samples

Content type
application/a2a+json
{
  • "name": "string",
  • "description": "string",
  • "version": "string",
  • "signatures": [
    ]
}

write

Rare, and authorized by a signature over the operation rather than by a credential. Each carries its own signed payload; none accepts an Authorization header, because there is nothing to put in one.

Publish a card, creating or updating the entry

agentId is client-derived — the RFC 7638 thumbprint of the genesis key — so the address is known before anything is published and this operation needs no server-allocated identifier.

The body carries the card, the public keys that signed it, and one publication proof per key entering the authorized set: the set of proof signers must equal the set of card signers. A key that signed the card but produced no proof is UNPROVEN_KEY.

That requirement is not bookkeeping. A card's signing payload is public the moment the card is published, so anyone can append a signature to someone else's card without invalidating the original; were one proof enough, an attacker could open an entry in their own name whose published key set names a holder who never asked for it.

Idempotent by construction. Submitting bytes identical to the current version answers 200 and changes nothing, including the sequence — which is exactly what a client retrying after a network timeout does, and its earlier write may well have succeeded. Different bytes at the same version are still refused by §6.4.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

header Parameters
If-Match
string
Example: "sha256:0dc224884a76d8bcbf841c01c501d5e94b1abba0ada08d93bd966db897100709"

The digest of the version this write expects to replace. Comparison is strong, the opposite of the weak comparison If-None-Match uses on reads, because this one guards a write.

Request Body schema: application/json
required
required
object (SignedAgentCard)

A complete A2A AgentCard including its signatures[], pinned to A2A v1.0.1 at commit 3303592588e388e62e0f69f701af531d2f4e3991. Parsing is strict: RFC 8785 canonicalization, I-JSON, no duplicate members, and the field-presence rules of SPEC.md §5.2 — whose derivation digest is in the manifest.

required
Array of objects (Jwk) [ 1 .. 8 ] items

The public keys that signed the card. Every proof signer must be among them, and no key may appear twice.

required
Array of objects (PublicationProof) [ 1 .. 8 ] items

One per key entering the authorized set, each a JWS over a PublishPayload. On a creation, one proof's kid must equal the agentId — the genesis key names the entry, so the genesis key is what asks for the entry to exist. On an update, at least one proof must come from the current authorized set.

Responses

Request samples

Content type
application/json
{
  • "agentCard": {
    },
  • "keys": [
    ],
  • "proofs": [
    ]
}

Response samples

Content type
application/json
{
  • "agentId": "NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY",
  • "status": "ACTIVE",
  • "seq": 1,
  • "cardDigest": "string",
  • "cardVersion": "string",
  • "authorizedKids": [
    ],
  • "createdAt": "2026-09-01T08:44:46.283Z",
  • "updatedAt": "2026-09-01T08:44:46.283Z",
  • "agentCardUrl": "http://example.com",
  • "jwksUrl": "http://example.com",
  • "domains": [
    ]
}

Withdraw an entry, permanently

In V1 because the registry never holds the publisher's key: the publisher must be able to remove their own entry without asking an operator.

The signed payload names the current cardDigest, which is what prevents an older withdrawal being replayed.

The entry becomes WITHDRAWN, and the registry stops serving its current card and its JWKS. Published versions remain readable at their digest — what was published is not erased, only its status changes. The agentId is never reusable.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

Request Body schema: application/json
required
required
object (Withdrawal)

A JWS whose payload asks this registry to withdraw this entry.

required
Array of objects (Jwk) non-empty

Responses

Request samples

Content type
application/json
{
  • "withdrawal": {
    },
  • "keys": [
    ]
}

Response samples

Content type
application/json
{
  • "agentId": "NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY",
  • "status": "ACTIVE",
  • "seq": 1,
  • "cardDigest": "string",
  • "cardVersion": "string",
  • "authorizedKids": [
    ],
  • "createdAt": "2026-09-01T08:44:46.283Z",
  • "updatedAt": "2026-09-01T08:44:46.283Z",
  • "agentCardUrl": "http://example.com",
  • "jwksUrl": "http://example.com",
  • "domains": [
    ]
}

Certify the domains an entry owns

Two assertions, each made where that side actually has authority: the domain speaks in its own DNS zone, and the key speaks by signing. Neither half alone is accepted — a zone has no authority over someone's key, and a signed request naming example.com establishes nothing about example.com.

The signed domains array is the complete set: it replaces whatever was there, and an empty array removes every certification. There is no separate deletion operation, for the same reason there is no rotation operation — the request states the whole set, and stating a smaller one is how you shrink it.

The payload carries no cardDigest, unlike a withdrawal: a certification is a statement about domains, not about a card, and binding it to a digest would expire every certification at the next publish for no gain.

Only what the registry can currently observe is published. It re-resolves every requested domain hourly; a domain missing for three consecutive passes stops being listed and returns by itself when its record does.

path Parameters
agentId
required
string (AgentId) ^[A-Za-z0-9_-]{43}$
Examples: NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY

The RFC 7638 thumbprint of the genesis key — 43 base64url characters, computable offline before anything is published.

Request Body schema: application/json
required
required
object (Certification)

A JWS whose payload states the complete set of domains to certify.

required
Array of objects (Jwk) non-empty

Responses

Request samples

Content type
application/json
{
  • "certification": {
    },
  • "keys": [
    ]
}

Response samples

Content type
application/json
{
  • "agentId": "NTkoUedrnrVKX8n7EZWHpqbeI6klwPejKqcGvirtkgY",
  • "status": "ACTIVE",
  • "seq": 1,
  • "cardDigest": "string",
  • "cardVersion": "string",
  • "authorizedKids": [
    ],
  • "createdAt": "2026-09-01T08:44:46.283Z",
  • "updatedAt": "2026-09-01T08:44:46.283Z",
  • "agentCardUrl": "http://example.com",
  • "jwksUrl": "http://example.com",
  • "domains": [
    ]
}

discovery

What this registry pins, accepts and limits.

What this registry pins, accepts and limits

The canonical origin, the pinned A2A commit, accepted alg values, the limits of §8, the domain certification parameters, and a digest of the field-presence table of §5.2.

The presence-table digest is the load-bearing member. A2A pins a protocol commit but publishes no table derived from it, so two implementations can read the same commit and still disagree about one field — which yields two canonical documents and two signatures over what looks like the same card. The digest turns "we pinned the same commit" into something a second implementation can check.

Responses

Response samples

Content type
application/json
{
  • "origin": "http://example.com",
  • "documentation": {},
  • "claim": "string",
  • "a2a": {
    },
  • "canonicalization": "RFC 8785",
  • "keyIdentifiers": "RFC 7638",
  • "signatures": {
    },
  • "limits": {
    },
  • "presenceTable": {
    },
  • "domainCertification": {
    },
  • "testVectors": {}
}