# 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:

```http
GET /agent/auth HTTP/1.1
Host: yappy.biz
```

```http
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:

```json
{
  "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.

```http
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": ["you@example.com"]
}
```

```json
{
  "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`:

```http
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
```

```json
{
  "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.

```http
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`:

```http
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.

---

Canonical: https://yappy.biz/auth.md · Last updated: 2026-08-25
