Developers

The NANDA Index has a REST API. Public reads need no key; writes use the session token from sign-in.

Reference

API base URL: https://api.nandaindex.org. Every route is described in the API reference (Swagger UI), generated from the server's own route schemas; the OpenAPI document behind it can feed a code generator. The source and README cover self-hosting. Identifiers follow the ARD form urn:air:<publisher-FQDN>:<namespace...>:<short-name>; see How it works for the model.

Quickstart: read the index

Resolve an identifier

Returns the entry registered under that identifier, or the publisher's catalog entry when the publisher fronts its agents with an AI Catalog (match is "exact" or "publisher").

curl "https://api.nandaindex.org/api/v1/resolve?locator=urn:air:moonbakery39.com:agent:orders"
{ "locator": "urn:air:moonbakery39.com:agent:orders", "identifier": "orders", "match": "exact",
  "entry": { "identifier": "...", "displayName": "...", "type": "application/a2a-agent-card+json",
             "url": "...", "publisher": { ... }, "extensions": { "org.projectnanda": { ... } } } }

List the index

A JSON array of live entries in the AI Catalog entry shape. Pages with limit (1 to 500, default 100) and offset; the total is in the X-Total-Count response header.

curl -i "https://api.nandaindex.org/api/v1/index?limit=20&offset=0"
X-Total-Count: 105

[ { "identifier": "urn:air:...", "displayName": "...", "type": "...", "url": "...", "tags": [...], ... } ]

Get one entry by org ID

The same shape as a list item. Pending, suspended, unproven and taken-down entries answer 404, the same as an unknown org ID.

curl "https://api.nandaindex.org/api/v1/index/moon-bakery"

Every public response is JSON. Entries from resolve and index use camelCase AI Catalog fields; search results use the snake_case owner record.

Register, verify, update, delete

The same flow the dashboard uses, as API calls. The web app at Register is the easier path if you only have one entry.

  1. Create an account

    Returns a session token right away and emails a verification link. Until you confirm the link, every write to the index answers 403 ACCOUNT_NOT_VERIFIED.

    curl -X POST "https://api.nandaindex.org/auth/register" -H "Content-Type: application/json" \
      -d '{"email":"[email protected]","password":"a-long-password"}'
    # 201 {"token":"eyJ..."}
    
    curl -X POST "https://api.nandaindex.org/auth/verify-account" -H "Content-Type: application/json" \
      -d '{"token":"<token from the email link>"}'
    # 200 {"ok":true}
  2. Sign in

    Every write needs the token in an Authorization header. Tokens expire after a day by default; sign in again to get a new one.

    curl -X POST "https://api.nandaindex.org/auth/login" -H "Content-Type: application/json" \
      -d '{"email":"[email protected]","password":"a-long-password"}'
    # 200 {"token":"eyJ..."}
    export TOKEN="eyJ..."
  3. Register an entry

    Required: org_id, display_name, contact_email and registry_url (HTTP or HTTPS; use HTTPS). hosting_path is registry (default), dns-svcb, smb or personal; every path except personal needs a domain, and personal must not send one. The server derives the identifier, media type and publisher from the path and domain. A personal entry has no domain: its identifier is urn:air:host39.org:personal:<contact email, lowercased>, so that email is public once the entry is live. The entry starts as pending.

    curl -X POST "https://api.nandaindex.org/api/v1/orgs" -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" -d '{
        "org_id": "acme", "display_name": "Acme Corp", "hosting_path": "registry",
        "domain": "acme.com", "contact_email": "[email protected]",
        "registry_url": "https://registry.acme.com",
        "description": "Acme enterprise AI Catalog.", "tags": ["enterprise"]
      }'
    # 201 {"org_id":"acme","status":"pending","identifier":"urn:air:acme.com:catalog:root", ...}
  4. Verify and go live

    An entry with a domain is published once a DNS TXT record proves the domain. Request the challenge, publish the record, then ask the index to check it; the same challenge is returned until it expires (7 days). A personal entry has no domain and goes live when its contact email is confirmed through the emailed link. Domain entries should confirm their contact email too, so the index can reach them.

    curl -X POST "https://api.nandaindex.org/api/v1/orgs/acme/domain-challenge" -H "Authorization: Bearer $TOKEN"
    # 200 {"domain":"acme.com","record_name":"_nanda-challenge.acme.com","record_type":"TXT",
    #      "record_value":"nanda-verify=<token>","expires_at":"..."}
    
    # publish that TXT record at your DNS host, then:
    curl -X POST "https://api.nandaindex.org/api/v1/orgs/acme/verify-domain" -H "Authorization: Bearer $TOKEN"
    # 200 {... "domain_verified":true,"status":"active" ...}
    # 400 DOMAIN_NOT_VERIFIED while the record is not visible yet
    
    # personal entries instead confirm the emailed link:
    curl -X POST "https://api.nandaindex.org/api/v1/verify-email" -H "Content-Type: application/json" -d '{"token":"<token from the email link>"}'
  5. Update

    Editable fields: display_name, domain, registry_url, ttl_seconds (3600 to 604800), description, tags, version, publisher.displayName, extensions, entry_data and trust_manifest. Anything else, including org_id, contact_email, hosting_path and the identifier, answers 400 UNKNOWN_FIELDS. Changing the domain means proving the new one.

    curl -X PUT "https://api.nandaindex.org/api/v1/orgs/acme" -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" -d '{"description":"Acme agents and tools.","ttl_seconds":21600}'
    # 200 {... the updated record ...}
  6. Suspend, reactivate or delete

    Suspending hides the entry from the public index and keeps it; reactivating puts it back. Deleting is permanent and answers 204 with no body.

    curl -X DELETE "https://api.nandaindex.org/api/v1/orgs/acme/suspend" -H "Authorization: Bearer $TOKEN"
    curl -X POST   "https://api.nandaindex.org/api/v1/orgs/acme/reactivate" -H "Authorization: Bearer $TOKEN"
    curl -X DELETE "https://api.nandaindex.org/api/v1/orgs/acme" -H "Authorization: Bearer $TOKEN"
    # 204

Rate limits

Defaults for this index; an operator can change them. Each limited route has its own per-IP budget, so hitting the limit on one route does not block the others. Over a route limit, the API answers 429 with a Retry-After header (seconds) and X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers; the body's error text says how long to wait, for example "retry in 1 minute". The 429 codes below that the application raises itself (TOO_MANY_ATTEMPTS, TOO_MANY_UNVERIFIED, RESEND_COOLDOWN) carry no Retry-After header; the claim route's per-account limit does. Request bodies are capped at 64 KB (413).

RoutesLimit
Public reads: /api/v1/index, /api/v1/index/:org_id, /api/v1/search, /api/v1/resolve, GET /api/ard/agents, POST /api/ard/explore120 requests per minute per IP address, counted separately for each route
Agentic search: /api/v1/agentic-search, /api/ard/search30 requests per minute per IP address, counted separately for each route
Account routes: /auth/register, /auth/login, password reset, account verification, resend10 requests per minute per IP address, counted separately for each route; sign-in locks for 15 minutes after 5 failed attempts for one email from one network, doubling on further failures
Entry writes: POST /api/v1/orgs, PUT /api/v1/orgs/:org_id, POST /api/v1/orgs/:org_id/claim30 requests per hour per IP address, counted separately for each route; claim also has a 30 per hour budget per account across all IP addresses

Errors

Errors are JSON: { "error": "<code>", "detail": "<message>" }. Server failures answer 500 with internal_server_error and no detail. Codes are quoted exactly as the API sends them.

StatusCodeMeaning
400FST_ERR_VALIDATIONThe request does not match the route's schema; detail names the field.
400VALIDATIONA registration rule was broken: domain, hosting_path, registry_url or identifier.
400UNKNOWN_FIELDSPUT named a field that cannot be changed; detail lists the editable ones.
400PAYLOAD_TOO_LARGEextensions, entry_data or trust_manifest is over 16 KB or nested deeper than 6 levels.
400RESERVED_ORG_IDThat org_id is reserved; choose another.
400NO_DOMAINDomain verification was requested for a personal entry, which has no domain.
400NO_ACTIVE_CHALLENGEverify-domain was called before domain-challenge, or the challenge expired.
400DOMAIN_NOT_VERIFIEDThe TXT record was not found yet; detail says what was expected and what was seen.
400INVALID_TOKENAn account-verification or password-reset token is invalid or expired.
400invalid_locatorThe resolve locator is not a valid urn:air: identifier.
401UNAUTHORIZEDWrong email or password on sign-in, or no valid session token (missing, expired or revoked); sign in again.
403ACCOUNT_NOT_VERIFIEDConfirm your account email before writing to the index.
403FORBIDDENYou are not an admin of that entry.
403MODERATEDAn index operator took the entry down; it cannot be suspended or reactivated by its owner (edits and deletion still work).
404NOT_FOUNDNo such entry, or it is not live (index reads), or the verify-email token was not found, already used or expired.
404not_foundResolve found no live entry for the locator.
409CONFLICTAlready taken: the account email, an org_id held by another registration, or a domain or personal identity that another registration has already proven.
409ALREADY_VERIFIEDThe contact email is already verified.
409NOT_SUSPENDEDOnly a suspended entry can be reactivated.
422query_too_shortSearch needs at least 2 non-space characters.
429TOO_MANY_ATTEMPTSSign-in is locked for this email and network after repeated failures.
429TOO_MANY_UNVERIFIEDYou already hold 5 unverified registrations from the last 48 hours; verify or delete one first.
429RESEND_COOLDOWNA verification email was sent less than 60 seconds ago.
503db_unreachable/health could not reach the database.