This page is the endpoint reference for the broker at https://agentregistrar.dev: what exists, who may call it, what it returns, and which errors it returns. It is not a tutorial. For the client flow, start at the harness install guide; for tool behavior, use MCP tools; for the code list, use Error codes.
All examples use synthetic values. No endpoint needs an API key in a query string, and no example contains a real token.
Authentication at a glance
| Endpoint group | Caller | Authentication |
|---|---|---|
/.well-known/* |
Anyone | None (public metadata) |
POST /mcp |
Agent harness | Authorization: Bearer mca_... OAuth 2.1 access token |
GET /oauth/authorize |
Human, in a browser | Human login session (anonymous requests are redirected to /login) |
POST /api/oauth/authorize/decision |
Human, in the consent page | Human login session cookie, same-origin JSON |
POST /api/oauth/register |
MCP client | None (RFC 7591 dynamic registration) |
POST /api/oauth/token |
MCP client | None beyond the one-time code or refresh token; PKCE S256 is enforced |
POST /api/broker/report-abuse |
Relying party | RP attestation + signed report JWS |
POST /api/broker/report-provider |
Human owner | Session |
POST /api/broker/signup-receipt |
Relying party server | Exact RP attestation + RP-signed signup_token |
POST /api/webhooks/resend |
Mail provider only | Provider webhook signature (operator surface) |
Discovery and metadata
| Method | Path | Status | Content type | Notes |
|---|---|---|---|---|
GET |
/.well-known/oauth-protected-resource |
200 |
application/json |
RFC 9728 protected resource metadata for /mcp. |
GET |
/.well-known/oauth-authorization-server |
200 |
application/json |
RFC 8414 authorization server metadata. |
GET |
/.well-known/jwks.json |
200 |
application/json |
Public Ed25519 keys. Cache-Control: public, max-age=300. |
OPTIONS |
each of the above | 204 |
none | CORS preflight. |
All three set Access-Control-Allow-Origin: *. Only the JWKS sets an explicit cache directive; the two metadata documents carry no cache header.
GET /.well-known/oauth-protected-resource, live on 2026-10-21:
{
"resource": "https://agentregistrar.dev/mcp",
"authorization_servers": ["https://agentregistrar.dev"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:tools", "tos:accept", "signup:free_tier"]
}GET /.well-known/oauth-authorization-server, live on 2026-10-21:
{
"issuer": "https://agentregistrar.dev",
"authorization_endpoint": "https://agentregistrar.dev/oauth/authorize",
"token_endpoint": "https://agentregistrar.dev/api/oauth/token",
"registration_endpoint": "https://agentregistrar.dev/api/oauth/register",
"jwks_uri": "https://agentregistrar.dev/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true,
"scopes_supported": ["mcp:tools", "tos:accept", "signup:free_tier"]
}GET /.well-known/jwks.json, live on 2026-10-21, with the key material shortened:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "<32-byte public key, base64url>",
"kid": "broker-key-1",
"alg": "EdDSA",
"use": "sig"
}
]
}The iss in an assertion equals this issuer origin. Assertions carry alg EdDSA, kid broker-key-1 and typ agent-id+jwt. Verify against the JWKS, never against a key URL supplied by the assertion. See Issuer and JWKS assumptions.
OAuth 2.1
The broker implements authorization code with PKCE S256 and rotating refresh tokens. It supports three kinds of client: clients registered through dynamic client registration, two pre-seeded loopback CLI clients, and Client ID Metadata Document (CIMD) clients whose client_id is an HTTPS metadata URL.
POST /api/oauth/register
RFC 7591 dynamic client registration. No authentication. Request body:
| Field | Type | Required | Default | Rule |
|---|---|---|---|---|
client_name |
string | no | MCP Client |
Display name. |
redirect_uris |
array of strings | yes | - | At least one. Each must be HTTPS, or HTTP on a loopback host (localhost, 127.0.0.1, ::1). Fragments and other schemes are rejected. |
grant_types |
array of strings | no | ["authorization_code","refresh_token"] |
|
response_types |
array of strings | no | ["code"] |
|
token_endpoint_auth_method |
string | no | none |
Public clients use none. |
Success is 201 with the stored client information:
{
"client_id": "cli_01jdocsverify000000000000",
"client_id_issued_at": 1792576800,
"client_name": "Example MCP client",
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}| Status | error |
Meaning |
|---|---|---|
400 |
invalid_redirect_uri |
A redirect URI is not loopback HTTP or HTTPS, or the list is empty. |
400 |
invalid_client_metadata |
Invalid JSON or a body that fails the metadata schema. |
500 |
server_error |
The client could not be persisted. |
GET /oauth/authorize
The consent page. Query parameters:
| Parameter | Required | Rule |
|---|---|---|
client_id |
yes | A registered cli_... id, a seed client id, or an HTTPS CIMD URL. |
redirect_uri |
yes | Must match one registered redirect URI for that client. |
response_type |
yes | code. |
code_challenge |
yes | PKCE challenge, non-empty. |
code_challenge_method |
yes | S256. |
state |
no | Returned unchanged on the callback. |
scope |
no | Space-delimited; defaults to mcp:tools tos:accept signup:free_tier. |
resource |
no | Stored with the authorization code. |
Behavior:
- An anonymous request gets
307to/login?next=<the encoded authorize URL>, so the human returns to the same consent request after signing in. - The parameters are validated before anything is rendered. An invalid request renders an error card and never redirects to an unvalidated
redirect_uri; the card namesinvalid_client,invalid_redirect_uri,unsupported_response_typeorinvalid_request(missing PKCE). The page response itself is a rendered HTML document, not an OAuth JSON error. - A valid request renders the consent card with the resolved client name, the requested scopes and the Master Agent Delegation agreement.
POST /api/oauth/authorize/decision
Submits the human's decision from the consent page. Same-origin JSON with the human session cookie. Body:
| Field | Type | Required | Meaning |
|---|---|---|---|
client_id |
string | yes | The client being authorized. |
redirect_uri |
string | yes | Must match the client registration. |
code_challenge |
string | yes | The PKCE challenge from the request. |
code_challenge_method |
string | no | S256 (default). |
state |
string | no | Echoed into the redirect URL. |
scope |
string | no | Space-delimited scopes. |
resource |
string | no | Stored with the code. |
decision |
approve or deny |
yes | |
accept_master_tos |
boolean | no | Record the Master ToS signature inline. |
Success is 200 with { "redirect_url": "..." }. The broker does not redirect the browser itself; the consent page follows the returned URL.
| Status | error |
Meaning |
|---|---|---|
401 |
unauthorized |
No human session. |
400 |
invalid_request |
Invalid JSON or a body that fails the decision schema. |
400 |
invalid_client |
The client cannot be resolved. |
400 |
invalid_redirect_uri |
The redirect URI does not match the client. |
403 |
access_denied |
The human account is suspended. |
403 |
email_verification_required |
accept_master_tos was set but the human email is unverified. |
400 |
master_tos_required |
The Master ToS is unsigned and was not accepted in this decision. |
500 |
server_error |
The signature or the authorization code could not be stored. |
A denial is not an error response: it returns 200 with a redirect_url carrying error=access_denied.
POST /api/oauth/token
Exchanges a one-time authorization code, or rotates a refresh token. Accepts application/x-www-form-urlencoded or application/json.
grant_type=authorization_code:
| Parameter | Required | Meaning |
|---|---|---|
grant_type |
yes | authorization_code. |
code |
yes | The mcc_... code from the callback. One use, 10-minute lifetime. |
code_verifier |
yes | PKCE verifier for the stored S256 challenge. |
client_id |
no | When present, must equal the code's client. |
redirect_uri |
no | When present, must equal the code's redirect URI. |
grant_type=refresh_token:
| Parameter | Required | Meaning |
|---|---|---|
grant_type |
yes | refresh_token. |
refresh_token |
yes | The mcr_... token. Rotation revokes the presented token. |
client_id |
no | When present, must equal the token's client. |
Success is 200 with Cache-Control: no-store and Pragma: no-cache:
{
"access_token": "mca_<64 hex>",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "mcr_<64 hex>",
"scope": "mcp:tools tos:accept signup:free_tier"
}The access token lifetime is 24 hours. The token is opaque; only its SHA-256 hash is stored.
| Status | error |
Meaning |
|---|---|---|
400 |
invalid_request |
code or code_verifier missing, or refresh_token missing. |
400 |
invalid_grant |
Unknown, expired or already used code; PKCE mismatch; unknown or revoked refresh token; or the human account is not active or has no Master ToS signature. |
400 |
unsupported_grant_type |
Any grant_type other than the two above. |
500 |
server_error |
The token pair could not be persisted. |
MCP transport
POST /mcp
The Streamable HTTP MCP endpoint. One stateless handler per request; no session id is issued. Send JSON-RPC 2.0 with Content-Type: application/json and Authorization: Bearer mca_....
| Request | Result |
|---|---|
| Valid bearer, valid JSON-RPC | 200 with a JSON-RPC response. initialize, tools/list, tools/call, resources/list and resources/read are supported. |
| Missing, invalid, expired or revoked token | 401, JSON-RPC code -32001, plus WWW-Authenticate. |
| Human suspended, or Master ToS unsigned | 403, JSON-RPC code -32003. |
| No acceptable media type | 406, JSON-RPC code -32000. |
| CORS preflight | 204, no body. |
401 body, live on 2026-10-21, with the challenge header:
HTTP/2 401
content-type: application/json
www-authenticate: Bearer resource_metadata="https://agentregistrar.dev/.well-known/oauth-protected-resource", scope="mcp:tools tos:accept signup:free_tier"
{"jsonrpc":"2.0","error":{"code":-32001,"message":"Unauthorized: Missing, invalid, expired, or revoked MCP Bearer token."},"id":null}403 bodies use code -32003 with either "Human owner account is suspended." or "Master Agent Delegation ToS has not been signed." and an id of null.
POST accepts Accept: application/json alone; the handler normalizes the header to application/json, text/event-stream and defaults a missing content type, so curl works. A GET with a valid bearer token and without Accept: text/event-stream returns 406 with code -32000; without a valid token the auth gate returns 401 code -32001 first.
CORS headers on every /mcp response, including errors:
| Header | Value |
|---|---|
Access-Control-Allow-Origin |
* |
Access-Control-Allow-Methods |
GET, POST, DELETE, OPTIONS |
Access-Control-Allow-Headers |
Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID |
Access-Control-Expose-Headers |
WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version |
GET and DELETE /mcp
The route exports GET and DELETE, but the transport is stateless, so there is no stream to resume and no session to terminate. DELETE returns 200 with an empty body. Both statuses below require a valid bearer token; an unauthenticated request fails at the auth gate with 401 code -32001 before the transport runs. GET without Accept: text/event-stream returns 406; with that header the deployed broker returned a Cloudflare 500 error page on 2026-10-21. Use POST for every request. The 500 is recorded as a defect against the broker's streamable-HTTP transport; this page will be corrected when it is fixed.
Broker APIs
POST /api/broker/report-abuse
Authenticated relying-party abuse reporting (Contract D.2). It opens a case and does not revoke immediately.
Request body, both fields required:
{
"rp_attestation": "<compact provider attestation JWS>",
"report": "<compact agent-rp-abuse+jwt JWS>"
}The report JWS has a five-minute window (exp - iat = 300), iss equal to the
attestation origin, aud equal to the broker, an abr_ jti, agent_id,
reason, and optional evidence. The broker checks the attestation against its
RP-broker trust list before any JWKS fetch, verifies the report with the key
named by kid, and requires the attestation to be current at report time.
A new accepted report is 202:
{
"case_id": "0d5f6a7c-...",
"report_jti": "abr_01j...",
"verification_status": "verified",
"deduplicated": false,
"agent_suspended": true,
"human_notified": true
}| Status | error / code |
Meaning |
|---|---|---|
401 |
unauthorized / unauthenticated_report |
A legacy unsigned body; the signed form is required. |
401 |
invalid_attestation / invalid_attestation |
The attestation failed parse/signature/claims/binding. |
401 |
invalid_attestation / rp_broker_untrusted |
The issuer is outside the broker's RP-broker trust list. |
409 |
invalid_request / signup_conflict |
Same report jti with a different JWS. |
429 |
rate_limited / rate_limited |
60 requests/minute/IP exceeded. |
503 |
service_unavailable / rp_broker_unavailable |
Transient broker metadata/JWKS outage; retryable, no write. |
verified requires a linked signup with proof_source rp_callback or
rp_signed and a valid signup-time verification. Only a verified report may set
the agent to suspended pending operator review; an unverified report opens a
case and does not suspend. A duplicate returns 200 with the same case_id.
The body limit is 64 KiB.
POST /api/broker/report-provider
Human session only. A person reports a provider through the portal. The body is
the RP origin, audience, optional attestation jti, reason, optional evidence,
and optional distrust_broker/block_provider flags. It creates a provider case
and can update the human's trust/policy; it does not change the provider's
attestation. See Abuse reports.
POST /api/broker/signup-receipt
Contract F ingestion. Request body, both fields required:
{
"signup_token": "<compact Contract E RP-signed receipt>",
"rp_attestation": "<the exact attestation named by service.attestation_jti>"
}The broker verifies the historical attestation (exact jti, not the current
one), checks the receipt iat/signed_up_at against that attestation's
[iat, exp] window, verifies the receipt with the key named by
service.key_id, and records proof_source:"rp_callback".
Success is 201 when a new proof row is created and 200 when an existing proof
is returned or refreshed:
{
"ok": true,
"signup_id": "7f651102-docs-4fa6-b700-000000000000",
"signup_token_jti": "srp_01jdocsverify00000000000",
"assertion_jti": "jti_01jdocsverify0000000000000",
"agent_id": "agt_01jdocsverify0000000000000",
"service_audience": "example-rp",
"provider_account_id": "840d6148-docs-4680-8f33-000000000000",
"human_email_shared": "human@example.com",
"context_url": "https://example.com/docs/SKILL.md",
"context_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"proof_source": "rp_callback",
"deduplicated": false
}| Status | error / code |
Meaning |
|---|---|---|
400 |
invalid_request / invalid_request |
Missing/malformed body or non-JWS strings. |
401 |
invalid_attestation / invalid_attestation |
Contract G parse/signature/claims/binding failure. |
401 |
invalid_attestation / rp_broker_untrusted |
The issuer is outside the effective trust list. |
401 |
invalid_attestation / attestation_not_valid_at_signing |
The receipt time is outside the attestation window. |
401 |
invalid_attestation / attestation_unavailable |
The exact attestation is no longer retrievable and no snapshot exists. |
401 |
invalid_signup_token / invalid_signup_token |
Receipt parse/signature/claims failure; legacy sanitized body preserved. |
409 |
invalid_request / signup_conflict |
Same receipt jti with a different body. |
429 |
rate_limited / rate_limited |
Bounded ingestion rate exceeded. |
503 |
service_unavailable / rp_broker_unavailable |
Transient broker metadata/JWKS outage; retryable, no write. |
The endpoint is idempotent on the receipt jti; Tier 1 wins and a Tier 2 row can
be upgraded. context_url and context_sha256 are null when the receipt
carried no guide. There is no broker-key fallback and no substitution of a newer
attestation. See Receipt and context.
POST /api/webhooks/resend
Operator surface only. This endpoint receives mail-provider delivery events. It requires the provider's webhook signature headers; without them it returns 401 with error invalid_webhook_signature. It returns 200 with the processing result when the signature verifies. Agents, harnesses and relying parties must not call it, and it is not part of agent-identity-v1.
Status codes and errors
Each endpoint above lists its own statuses. The shared code lists, retryability and next actions are in Error codes. The short version:
401from/mcpis a transport gate, not a tool error. Re-run the client's OAuth step.403from/mcpis a human-account gate. The human must fix the account; retrying does not help.- A tool-level failure returns
200withisError: trueand a structured error body. Do not treat it as a transport failure. - OAuth errors use the RFC 6749
erroranderror_descriptionfields.error_descriptionis not normative.
Versioning
Paths are not versioned. /mcp, the OAuth endpoints and the broker APIs are the v1 surface; a breaking change to the wire contract would ship as a new path or a new protocol major version, not as a silent change to these paths. This page records what the deployed broker does and names the commit it was checked against in verifiedAgainst. The protocol version itself is documented in Versioning and the changelog.
Verification record
Live requests on 2026-10-21 against https://agentregistrar.dev, with broker commit dec84d9 as the code baseline:
- The three metadata documents returned
200 application/jsonwith the bodies shown,Access-Control-Allow-Origin: *, andCache-Control: public, max-age=300on the JWKS only. The JWKS key isOKP/Ed25519/EdDSA,kidbroker-key-1,usesig. POST /mcpwithout a token returned401with code-32001and theWWW-Authenticateheader shown.OPTIONS /mcpreturned204. At the v1.1 baseline an authenticatedinitializereturned serveragentregistrar1.0.0 withtoolsandresourcescapabilities andtools/listreturned eight tools; the v1.2 code has nine, includingidentity_signup, and that live count is a release gate.POST /api/oauth/registerwith a loopback redirect URI returned201with acli_...id; an empty body returned400 invalid_redirect_uri.GET /oauth/authorizeanonymously returned307to/login?next=....POST /api/oauth/authorize/decisionwithout a session returned401 unauthorized.POST /api/oauth/tokenwithgrant_type=weirdreturned400 unsupported_grant_type; with an unknown code it returned400 invalid_grant.POST /api/broker/report-abusewith an empty body returned400 invalid_request; the v1.2 route now rejects a legacy body with401 unauthenticated_report. That route and Contract F's two-field body are code-complete but their live v1.2 responses are part of the release gate.POST /api/broker/signup-receiptwith an invalid token returned401 invalid_signup_token; v1.2 requires bothsignup_tokenandrp_attestation.POST /api/webhooks/resendwithout signature headers returned401 invalid_webhook_signature.GET /mcpreturned406without an SSEAcceptheader and a Cloudflare500with one (see above).DELETE /mcpreturned200.