# auth.md

How an application or AI agent authenticates with Endorphins.

Issuer: `https://app.endorphinsrunning.com`

## Discovery documents

These are authoritative. Prefer them over this prose.

| Document | URL |
| --- | --- |
| Protected Resource Metadata (RFC 9728) | https://app.endorphinsrunning.com/.well-known/oauth-protected-resource |
| Authorization Server Metadata (RFC 8414) | https://app.endorphinsrunning.com/.well-known/oauth-authorization-server |
| OpenID Connect discovery | https://app.endorphinsrunning.com/.well-known/openid-configuration |
| JWKS | https://app.endorphinsrunning.com/.well-known/jwks.json |
| MCP server card | https://endorphinsrunning.com/.well-known/mcp/server-card.json |
| API catalog | https://endorphinsrunning.com/.well-known/api-catalog |
| Agent skills | https://endorphinsrunning.com/.well-known/agent-skills/index.json |

## Protected resources

| Resource | Endpoint |
| --- | --- |
| MCP server (Streamable HTTP) | https://app.endorphinsrunning.com/mcp |
| REST API | https://app.endorphinsrunning.com/gpt |
| Agent registration (auth.md) | https://app.endorphinsrunning.com/agent/identity |
| Claim ceremony | https://app.endorphinsrunning.com/agent/identity/claim |

Bearer tokens are accepted in the `Authorization` header only.

## Registration

Two independent paths. Neither creates or provisions a user: every request acts
on behalf of a runner who explicitly approved it.

### Agentic registration (auth.md profile)

For an agent with no user identity yet. Identity types supported: **anonymous**.
We do not accept `identity_assertion` (no agent providers are on our trust
list) or `service_auth`.

**1. Register.**

```
POST https://app.endorphinsrunning.com/agent/identity
Content-Type: application/json

{ "type": "anonymous", "agent_name": "Your agent" }
```

Returns an `identity_assertion` (RS256, verifiable against our JWKS) and a
`claim_token`, returned exactly once. Note `pre_claim_scopes` is `[]`:
every Endorphins resource belongs to a runner, so an unclaimed agent can read
nothing. There is no useful pre-claim credential to mint, and the JWT-bearer
grant will tell you so until the registration is claimed.

**2. Start the claim ceremony.**

```
POST https://app.endorphinsrunning.com/agent/identity/claim
Content-Type: application/json

{ "claim_token": "clm_...", "email": "runner@example.com" }
```

Returns a 6-digit `user_code` and a `verification_uri`. The email binds the
registration to one runner: only that signed-in account can complete it, so an
intercepted code is useless to anyone else.

**3. Hand off to the runner.** Show them the `verification_uri` and the
`user_code`. They sign in and type the code.

**4. Poll the token endpoint.**

```
POST https://app.endorphinsrunning.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

Returns `authorization_pending` until they finish, `expired_token` if the
code times out (call the claim endpoint again for a fresh one), then a standard
token response plus a v2 `identity_assertion`. That successful response is
delivered exactly once and consumes the `claim_token` — persist the
`identity_assertion` immediately; it is your only credential from here on.

**5. Refresh** by re-exchanging the assertion:

```
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>
```

There is no refresh token in this profile; the assertion replaces it. Each
exchange returns a fresh `identity_assertion` alongside the access token —
always store the newest one. A post-claim assertion is valid for 30 days, so an
agent that exchanges at least monthly keeps its access alive indefinitely; one
that lapses must re-register and be re-claimed. The runner can revoke the
connection at any time from their account settings, which invalidates the
assertion as well as all tokens.

### OAuth client registration

For an app that will run the standard Authorization Code + PKCE flow.

#### Automated (RFC 7591 Dynamic Client Registration)

```
POST https://app.endorphinsrunning.com/oauth/register
Content-Type: application/json

{
  "client_name": "Your agent",
  "redirect_uris": ["https://your-app.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

A dynamically registered client is granted read scopes only. Write scopes are
never self-granted.

#### Manual

Register at https://endorphinsrunning.com/developer to choose redirect URIs and
scopes, and to request `write:training`.

## Obtaining a token

Authorization Code with PKCE (S256) is the only supported user flow. There is no
client-credentials grant: Endorphins data always belongs to a runner, so there is
no token that is not tied to one.

1. `GET https://app.endorphinsrunning.com/oauth/authorize` with `response_type=code`, `client_id`,
   `redirect_uri`, `scope`, `state`, `code_challenge`, `code_challenge_method=S256`.
2. The runner approves the requested scopes on the Endorphins consent screen.
3. `POST https://app.endorphinsrunning.com/oauth/token` with `grant_type=authorization_code`, the
   `code`, and your `code_verifier`.

Refresh tokens rotate on each use. Revoke at `https://app.endorphinsrunning.com/oauth/revoke`.
Runners can revoke your client at any time from their account settings, so treat
a 401 as a prompt to re-authorize.

## Scopes

| Scope | Grants |
| --- | --- |
| `read:profile` | Your profile details (name, bio, preferences) |
| `read:training` | Your training plan, paces, and scheduled workouts |
| `read:workouts` | Your activities and workout history |
| `read:health` | Your wellness data (heart rate, sleep, recovery) |
| `write:training` | Propose training-plan adjustments (you approve each one before it applies) |
| `write:activities` | Record completed workouts (e.g. partner studio classes) on your account |
| `openid` | Verify your Endorphins identity |
| `email` | Your email address |
| `profile` | Your name and profile photo |

All data scopes are read-only. `write:training` is propose-only: it creates a
PENDING proposal the athlete approves in the app. It never mutates a live plan
directly, and it is granted only through the developer portal.

## Using the credential

```
Authorization: Bearer <access_token>
```

A token missing the scope an endpoint requires receives `403`, not partial data.

## Skills

Step-by-step instructions for agents are published as skills:

- https://endorphinsrunning.com/.well-known/agent-skills/endorphins-connect/SKILL.md
- https://endorphinsrunning.com/.well-known/agent-skills/endorphins-training-data/SKILL.md
- https://endorphinsrunning.com/.well-known/agent-skills/endorphins-plan-adjustments/SKILL.md
