# auth.md

You are an agent. This service supports **agentic registration**: discover → register → (claim if needed) → exchange for an access_token → call API. Follow the steps in order; do not skip ahead.

Resource server: https://api.doctorsim.com/v2. Authorization server: https://www.doctorsim.com.

## TL;DR for remote MCP connectors (Claude.ai, ChatGPT)

- **Dynamic Client Registration is supported.** Leave any "OAuth Client ID / Secret" fields **blank** — the server issues a **public client** (`token_endpoint_auth_method: none`) and uses **authorization code + PKCE**. There is no client secret.
- Connector URL is the MCP endpoint from the server card, **not** this document.
- Use the **`identity_assertion` + verified email** path below. Do **not** use the `anonymous` / `api_key` path for a remote connector — that is for direct API/server integrations.
- Tokens: access ~1h, refresh ~30d; the connector refreshes automatically. If it shows disconnected, reconnect to re-run consent.
- Grant least privilege: `orders:read balance:read` for exploration; add `orders:write` (debits real PRO credits) only when needed.

## Step 1 — Discover

Discovery is two hops — you may have already done this via a `401` response:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://www.doctorsim.com/.well-known/oauth-protected-resource/v2"
```

If you do not have the 401 in hand, fetch the Protected Resource Metadata (PRM):

### 1a. Fetch the Protected Resource Metadata

```http
GET /.well-known/oauth-protected-resource/v2
Host: api.doctorsim.com
Accept: application/json
```

Response shape:

```json
{
  "resource": "https://api.doctorsim.com/v2",
  "authorization_servers": ["https://www.doctorsim.com"],
  "scopes_supported": ["openid", "profile", "orders:read", "orders:write", "balance:read", "webhooks:read", "webhooks:write"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://www.doctorsim.com/api-docs/"
}
```

What each field tells you:

- `resource` — canonical API audience (`https://api.doctorsim.com/v2`). Use as OAuth `resource` / JWT `aud` where applicable.
- `authorization_servers` — OAuth Authorization Server base URL (`https://www.doctorsim.com`). Fetch AS metadata at Step 1b.
- `scopes_supported` — scopes the access_token may carry.
- `bearer_methods_supported` — send credentials as `Authorization: Bearer …` (`header`).

### 1b. Fetch the Authorization Server metadata

```http
GET /.well-known/oauth-authorization-server
Host: www.doctorsim.com
Accept: application/json
```

Response shape (abbreviated — read `agent_auth` in full):

```json
{
  "issuer": "https://www.doctorsim.com",
  "authorization_endpoint": "https://www.doctorsim.com/oauth/authorize",
  "token_endpoint": "https://www.doctorsim.com/oauth/token",
  "registration_endpoint": "https://www.doctorsim.com/oauth/register",
  "agent_auth": {
    "skill": "https://www.doctorsim.com/auth.md",
    "register_uri": "https://www.doctorsim.com/oauth/register",
    "claim_uri": "https://www.doctorsim.com/oauth/authorize",
    "identity_types_supported": [
        "identity_assertion",
        "anonymous"
    ],
    "identity_assertion": {
        "assertion_types_supported": [
            "verified_email"
        ],
        "credential_types_supported": [
            "access_token",
            "refresh_token"
        ]
    },
    "anonymous": {
        "credential_types_supported": [
            "api_key"
        ]
    }
}
}
```

The `agent_auth` block is the registration bootstrap surface:

- `agent_auth.skill` — URL of this document (`https://www.doctorsim.com/auth.md`)
- `agent_auth.register_uri` — `https://www.doctorsim.com/oauth/register` (RFC 7591 dynamic client registration)
- `agent_auth.claim_uri` — `https://www.doctorsim.com/oauth/authorize` (user login + scope consent)
- `agent_auth.identity_types_supported` — `identity_assertion`, `anonymous`
- `agent_auth.identity_assertion.assertion_types_supported` — `verified_email` (OAuth authorization code + PKCE)
- `agent_auth.identity_assertion.credential_types_supported` — `access_token`, `refresh_token`
- `agent_auth.anonymous.credential_types_supported` — `api_key`
- `registration_endpoint`, `authorization_endpoint`, `token_endpoint` — standard RFC 8414 OAuth endpoints

| Document | URL |
|---|---|
| OAuth Authorization Server | https://www.doctorsim.com/.well-known/oauth-authorization-server |
| OpenID Configuration | https://www.doctorsim.com/.well-known/openid-configuration |
| Protected Resource Metadata | https://www.doctorsim.com/.well-known/oauth-protected-resource/v2 |
| API Catalog (RFC 9727) | https://www.doctorsim.com/.well-known/api-catalog |
| OpenAPI 3.1 | https://www.doctorsim.com/api-docs/openapi.yaml |
| Interactive OAuth Lab | https://www.doctorsim.com/api-docs/oauth_lab.html |

## Step 2 — Pick a method

Use this decision tree:

1. **You have (or can obtain) the user's verified email and can run a browser consent step** → [identity_assertion + email](#identity_assertion--email) (recommended — OAuth PKCE).
2. **You have neither a provider ID-JAG nor a user email session** → [anonymous](#anonymous) (PRO API key via dashboard).

doctorSIM does **not** accept ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`). If you only have a provider-issued ID-JAG, fall back to `identity_assertion + email` or `anonymous`.

Cross-check your pick against `agent_auth.identity_types_supported` and the matching `*_supported` arrays in Step 1b before sending.

## Step 3 — Register

### identity_assertion + email

doctorSIM maps `verified_email` to **OAuth dynamic client registration + authorization code + PKCE**. Register the client, then run the claim ceremony at Step 4.

```http
POST /oauth/register
Host: www.doctorsim.com
Content-Type: application/json

{
  "client_name": "My Agent",
  "redirect_uris": ["https://my-agent.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "orders:read orders:write balance:read webhooks:read webhooks:write"
}
```

Response (201):

```json
{
  "client_id": "cli_…",
  "client_secret": "",
  "redirect_uris": ["https://my-agent.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none",
  "scope": "orders:read orders:write balance:read webhooks:read webhooks:write"
}
```

Store `client_id`. No `access_token` yet — the user must complete Step 4 (authorize with verified email login).

### anonymous

For server integrations without per-user OAuth, provision a PRO account API key:

1. Direct the user to https://www.doctorsim.com/developers-agents.html to apply for a PRO account (if needed).
2. User signs in → **My Account → API** → generate `{api_id}` and `{api_secret}`.
3. Use the credential immediately:

```json
{
  "registration_type": "anonymous",
  "credential_type": "api_key",
  "credential": "{api_id}:{api_secret}",
  "scopes": ["orders:read", "orders:write", "balance:read"]
}
```

Skip to [Step 6](#step-6--use-the-access_token) with `Authorization: Bearer {api_id}:{api_secret}`.

## Step 4 — Claim ceremony

Required for `identity_assertion + email` only. The PRO user signs in with a verified email and approves scopes at `agent_auth.claim_uri`.

### 4a. Get the ceremony materials

Generate PKCE values: `code_verifier` (43–128 URL-safe chars) and `code_challenge = BASE64URL(SHA256(code_verifier))`.

Build the authorize URL:

```
GET https://www.doctorsim.com/oauth/authorize?
  response_type=code&
  client_id={client_id}&
  redirect_uri={redirect_uri}&
  scope=orders:read+orders:write+balance:read&
  state={random_state}&
  code_challenge={code_challenge}&
  code_challenge_method=S256
```

### 4b. Hand off to the user

Open the authorize URL in a browser (or redirect the user). Tell them: sign in to doctorSIM with the PRO account email, review scopes, and approve access for this agent.

### 4c. Poll for completion

Capture `code` and `state` from the redirect to your `redirect_uri`. Verify `state` matches. Proceed to [Step 5](#step-5--exchange-the-assertion) with the authorization `code`.

## Step 5 — Exchange the assertion

Exchange the authorization code for an access_token at the AS `token_endpoint`:

```http
POST /oauth/token
Host: www.doctorsim.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code={authorization_code}&
redirect_uri={redirect_uri}&
client_id={client_id}&
code_verifier={code_verifier}
```

Response (200):

```json
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_…",
  "scope": "orders:read orders:write balance:read"
}
```

Extract `access_token` and go to [Step 6](#step-6--use-the-access_token). Use `grant_type=refresh_token` at the same endpoint to renew without repeating Step 4.

If `/oauth/token` returns `invalid_grant`, restart at [Step 3](#step-3--register) and re-run the claim ceremony.

## Step 6 — Use the access_token

Present the credential as a bearer token:

```http
GET /v2/status
Host: api.doctorsim.com
Authorization: Bearer <access_token>
Accept: application/json
```

**Refresh.** When `expires_in` elapses, POST `grant_type=refresh_token` to `/oauth/token`. If refresh fails with `invalid_grant`, restart at [Step 1](#step-1--discover).

If you get a 401 on a previously-working access_token: try refresh once. If that fails, discard the token and restart at [Step 1](#step-1--discover).

Full API reference: https://www.doctorsim.com/api-docs/openapi.yaml

## Scopes

- `orders:read`, `orders:write` — list and place top-up / gift card orders
- `balance:read` — read PRO credit balance
- `webhooks:read`, `webhooks:write` — manage order webhooks
- `openid`, `profile` — OIDC identity (optional)

## Errors

Errors at `/oauth/register` and `/oauth/authorize` use OAuth-standard vocabulary per [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749). Errors at `/oauth/token` use the same.

| Code | Where | What to do |
|---|---|---|
| `verified_email_not_enabled` | registration | Email/OAuth path disabled. Pick `anonymous` or stop. |
| `invalid_client_metadata` | `/oauth/register` | Fix registration JSON (redirect_uris, grant_types). |
| `invalid_request` | `/oauth/authorize` | Missing or invalid OAuth parameters. |
| `access_denied` | `/oauth/authorize` | User declined consent. Stop or retry later. |
| `invalid_grant` | `/oauth/token` | Code expired or PKCE mismatch. Restart at Step 3. |
| `invalid_client` | `/oauth/token` | Unknown `client_id`. Re-read AS metadata. |
| `unsupported_grant_type` | `/oauth/token` | Use `authorization_code` or `refresh_token`. |
| `insufficient_scope` | API | Request broader scopes in Step 4. |
| `rate_limited` (429) | any | Back off and retry. |

Retry policy:

- 5xx → exponential backoff, retry the same request.
- 4xx → do not retry the same payload; act on the table above.
- 401 on a previously-working access_token → refresh once; if that fails, restart at Step 1.

## Revocation

doctorSIM does not expose a WorkOS-style `/agent/auth/revoke` endpoint. Two practical paths:

- **Credential layer** — discard the `access_token` / API key locally. Use `refresh_token` rotation only while the refresh token remains valid.
- **Account layer** — the PRO user revokes access by deleting the OAuth client or API key in **My Account → API**, or by changing their password.

On a 401 for a previously-working access_token: try refresh once. If `/oauth/token` returns `invalid_grant`, restart at [Step 3](#step-3--register).

## Credits

Agents spend PRO account credits. Fund credits via the web dashboard. Programmatic credit purchase (x402/ACP/UCP) is planned separately.
