Authenticating an agent with Yappy
This document is written for an autonomous agent that has landed on yappy.biz and needs to know whether it must authenticate, and if so, how.
Read this first: you almost certainly do not need a credential. Every read endpoint under https://yappy.biz/api/v1 and both MCP servers are public and unauthenticated. Try the call before you try to authenticate.
Credentials exist for exactly two reasons: a higher rate ceiling, and the export:docs scope. Nothing behind them is secret.
Discover
Start from one request. Probing a protected entry point returns a 401 whose header names the metadata document:
GET /agent/auth HTTP/1.1
Host: yappy.biz
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="yappy", resource_metadata="https://yappy.biz/.well-known/oauth-protected-resource", scope="read:releases read:docs"
Follow resource_metadata:
| Document | URL | Spec |
|---|---|---|
| Protected resource metadata | https://yappy.biz/.well-known/oauth-protected-resource | RFC 9728 |
| Authorization server metadata | https://yappy.biz/.well-known/oauth-authorization-server | RFC 8414 |
The protected-resource document lists authorization_servers, scopes_supported, and bearer_methods_supported. The authorization-server document carries the endpoints, code_challenge_methods_supported, and an agent_auth block whose skill field points back at this file.
Pick a method
The agent_auth block in the authorization-server metadata advertises which identity types this deployment accepts:
{
"agent_auth": {
"skill": "https://yappy.biz/auth.md",
"register_uri": "https://yappy.biz/oauth/register",
"claim_uri": "https://yappy.biz/oauth/token",
"revocation_uri": "https://yappy.biz/oauth/revoke",
"identity_types_supported": ["anonymous", "identity_assertion"],
"anonymous": {
"credential_types_supported": ["oauth_client_credentials"]
},
"identity_assertion": {
"assertion_types_supported": [
"urn:ietf:params:oauth:token-type:id-jag",
"urn:ietf:params:oauth:token-type:jwt"
],
"credential_types_supported": ["oauth_client_credentials"]
}
}
}
**Choose anonymous** unless the credential must be attributable to a specific human. Yappy's API serves public product data; there is no per-user state to protect, so an anonymous client credential is the correct, least-privilege choice and it needs no browser and no human.
Choose identity_assertion — presenting an id-jag or a JWT from an identity provider you already hold — when your platform requires every outbound call to be attributable.
Register
RFC 7591 dynamic client registration. No human approval, no waiting.
POST /oauth/register HTTP/1.1
Host: yappy.biz
Content-Type: application/json
{
"client_name": "example-agent",
"grant_types": ["client_credentials"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "read:releases read:docs read:pricing",
"software_id": "example-agent",
"contacts": ["[email protected]"]
}
{
"client_id": "yc_…",
"client_secret": "ys_…",
"client_id_issued_at": 1787000000,
"client_secret_expires_at": 0,
"grant_types": ["client_credentials"],
"scope": "read:releases read:docs read:pricing",
"registration_client_uri": "https://yappy.biz/oauth/register"
}
Store client_secret. It is not retrievable later; losing it means registering again, which is free.
Claim
Exchange the registration for an access token at the claim_uri:
POST /oauth/token HTTP/1.1
Host: yappy.biz
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=yc_…&client_secret=ys_…&scope=read:releases
{
"access_token": "yat_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:releases"
}
If the credential must be tied to a person, use the authorization-code grant instead. **PKCE with S256 is required**; plain is refused.
GET /oauth/authorize
?response_type=code
&client_id=yc_…
&redirect_uri=https://example.com/callback
&scope=read:docs
&state=<opaque>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
Exchange the returned code at /oauth/token with grant_type=authorization_code and the original code_verifier.
Use the credential
Bearer token in the Authorization header. That is the only accepted presentation — no query parameter, no cookie.
GET /api/v1/releases HTTP/1.1
Host: yappy.biz
Authorization: Bearer yat_…
Scopes, all of them read-shaped:
| Scope | Grants |
|---|---|
read:releases | Release history and the current version |
read:docs | Documentation pages and search |
read:pricing | Pricing and plan data |
read:faq | Frequently asked questions |
export:docs | Starting and polling export jobs |
Request the narrowest set that does the job. A token raises your ceiling from 120 to 600 requests per minute.
Errors
Every failure is a JSON document, never an HTML page.
| Status | Header | code | Do this |
|---|---|---|---|
| 401 | WWW-Authenticate: Bearer error="invalid_token" | invalid_token | Get a new token. Do not retry with the old one. |
| 401 | WWW-Authenticate: Bearer resource_metadata="…" | unauthorized | You sent no token. Read the metadata and register. |
| 403 | WWW-Authenticate: Bearer error="insufficient_scope", scope="export:docs" | insufficient_scope | Re-issue a token with the scope named in the header. |
| 400 | — | invalid_request | Read errors[]; a parameter is malformed. |
| 429 | Retry-After: <seconds> | rate_limited | Wait, then retry. Batch your reads. |
Full list: <https://yappy.biz/developers/errors>.
Revocation
RFC 7009, at the revocation_uri:
POST /oauth/revoke HTTP/1.1
Host: yappy.biz
Content-Type: application/x-www-form-urlencoded
token=yat_…&token_type_hint=access_token&client_id=yc_…&client_secret=ys_…
Returns 200 whether or not the token existed, as the RFC requires — a caller cannot use this endpoint to probe which tokens are real.
Revoke when: the agent's task is finished and the credential is no longer needed, the secret may have been exposed, or your own client is being retired. Revoking a client's secret invalidates every token issued under it.
There is nothing to clean up on our side beyond that. We store no user data against a client, because the API exposes none.