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
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
POST /v1/listings
Verify the challenge proof and create (or re-register) a listing.
Request
Server-side flow
- Validate that the challenge exists, is unexpired, and was issued for the same
discovery_url. - Consume the challenge (it cannot be reused).
- Fetch
proof_url = https://{host}/.well-known/afauth-registry-proofover HTTPS. The body must betext/plainand equal thechallenge_tokenbyte-for-byte. - Fetch
discovery_urlover HTTPS and validate againstschemas/well-known.json. - If the discovery doc’s
service_didis adid:web:host, the host portion must equal the discovery host. - If a listing already exists for that
service_didfrom the same host, this is the re-challenge path: prior session tokens are revoked and a new one issued. - If a listing already exists from a different host, the request is rejected as a
conflict.
Response — 201 Created (new) / 200 OK (re-challenge)
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 asGET /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 inGET /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=120Access-Control-Allow-Origin: *
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 thatservice_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=300Access-Control-Allow-Origin: *
Errors
Every non-2xx response uses a structured envelope:Rate limits
Per-IP, fixed-window. The directory returns429 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 atschemas/listing.json; the reference implementation is at AFAuthHQ/registry.