Skip to main content
The directory exposes a small HTTP API at registry.afauth.org/v1. Write endpoints require proof of control of the discovery host (a one-time challenge + ~30-min token + 7-day session); read endpoints are unauthenticated and CORS-open for browser-based aggregators.
This page is a reference. For the step-by-step walkthrough, see List your service on registry.afauth.org.

Base URL

All requests and responses are JSON (application/json). All URLs are HTTPS.

Endpoints at a glance


POST /v1/listings/challenge

Request a one-time challenge bound to a discovery host.

Request

Response — 200 OK

Challenge tokens are single-use and expire 30 minutes after issuance.

POST /v1/listings

Verify the challenge proof and create (or re-register) a listing.

Request

Server-side flow

  1. Validate that the challenge exists, is unexpired, and was issued for the same discovery_url.
  2. Consume the challenge (it cannot be reused).
  3. Fetch proof_url = https://{host}/.well-known/afauth-registry-proof over HTTPS. The body must be text/plain and equal the challenge_token byte-for-byte.
  4. Fetch discovery_url over HTTPS and validate against schemas/well-known.json.
  5. If the discovery doc’s service_did is a did:web:host, the host portion must equal the discovery host.
  6. If a listing already exists for that service_did from the same host, this is the re-challenge path: prior session tokens are revoked and a new one issued.
  7. If a listing already exists from a different host, the request is rejected as a conflict.

Response — 201 Created (new) / 200 OK (re-challenge)

Session tokens are bearer credentials and expire in 7 days. Treat them like API keys.

PATCH /v1/listings/{service_did}

Update the writeable fields of a listing.

Request

At least one writeable field must be present — an empty body ({}) is rejected with invalid_request. To change discovery_url, soft-delete the existing listing and re-register from the new host.

Response — 200 OK

The full listing record after the update (same shape as GET /v1/listings/{service_did}).

DELETE /v1/listings/{service_did}

Soft-delete the listing. The record’s status becomes deleted and it disappears from default list responses, but persists so mirrors converge.

Response — 204 No Content

Soft-deleted listings reappear in GET /v1/listings?include_deleted=true with status: "deleted". Hard-erase (removing the record entirely) is reserved for unlawful content under the take-down policy.

GET /v1/listings

Cursor-paginated list of listings.

Query parameters

Response — 200 OK

next_cursor is null when there are no more pages. The cursor encoding is unspecified and may change without notice — submit it unmodified.

Cache headers

  • Cache-Control: public, max-age=30, s-maxage=120
  • Access-Control-Allow-Origin: *
Friendly to mirrors that poll with updated_since and to browser-based aggregators.

GET /v1/listings/{service_did}

Single listing. The path parameter is the full DID (e.g. did:web:api.example.com). Colons are valid in URL path segments; no encoding is required.

Response — 200 OK

Same shape as a single entry in the list response above.

Response — 404 Not Found

If no listing exists for that service_did. Soft-deleted listings still return 200 with status: "deleted" — unlike the list endpoint (which hides deleted records unless include_deleted=true), a single fetch by DID always returns the record regardless of status.

Cache headers

  • Cache-Control: public, max-age=60, s-maxage=300
  • Access-Control-Allow-Origin: *

Errors

Every non-2xx response uses a structured envelope:

Rate limits

Per-IP, fixed-window. The directory returns 429 rate_limited with a Retry-After header (in seconds) when a bucket overflows. In addition, per-host challenge issuance is capped at 10 active challenges per hour — anyone can request a challenge for your host, but only you (the host’s controller) can satisfy one. The SEO GET /sitemap.xml endpoint (outside /v1) is separately capped at 60 / minute per IP.

Federation

The schema and the surface described above are public — anyone may run a mirror or a private directory implementing the same contract. The schema is at schemas/listing.json; the reference implementation is at AFAuthHQ/registry.