# auth.md

Runtype is an AI product development platform. Agents can register for API credentials in two ways: present an identity assertion (ID-JAG) from a trusted identity provider for a full-access credential in one request, or register anonymously for a scoped credential and later bind it to a real user account via an OTP claim ceremony.

## Prefer the CLI

The Runtype CLI wraps this whole protocol in three non-interactive commands (no TTY, no browser) and stores credentials for every other CLI command:

```bash
runtype auth register --email user@example.com   # register + send the OTP email in one step
runtype auth verify 123456                       # complete the claim; stores the full credential
runtype auth claim user@example.com              # re-send or change the email for a pending signup
```

Use the raw HTTP protocol below when you cannot install `@runtypelabs/cli`.

- **Resource server**: `api.runtype.com`
- **Authorization server**: `api.runtype.com`

## Discover

1. Any 401 response from the API includes a `WWW-Authenticate` header:

   ```
   WWW-Authenticate: Bearer resource_metadata="https://api.runtype.com/.well-known/oauth-protected-resource"
   ```

2. Fetch the Protected Resource Metadata (PRM) at that URL to find the authorization server.

3. Fetch `https://api.runtype.com/.well-known/oauth-authorization-server` — the response includes an `agent_auth` block:
   ```json
   {
     "agent_auth": {
       "skill": "https://runtype.com/auth.md",
       "register_uri": "https://api.runtype.com/agent/auth",
       "claim_uri": "https://api.runtype.com/agent/auth/claim",
       "revocation_uri": "https://api.runtype.com/agent/auth/revoke",
       "identity_types_supported": ["anonymous", "identity_assertion"],
       "anonymous": {
         "credential_types_supported": ["api_key"]
       },
       "identity_assertion": {
         "assertion_types_supported": ["urn:ietf:params:oauth:token-type:id-jag"],
         "credential_types_supported": ["api_key"]
       }
     }
   }
   ```

## Pick a method

Two registration methods are supported:

- **`identity_assertion`** — present an ID-JAG (Identity Assertion JWT Authorization Grant, `urn:ietf:params:oauth:token-type:id-jag`) from a trusted agent identity provider. One request, no claim ceremony; the credential is full-access immediately. Use this when your runtime holds an assertion.
- **`anonymous`** — no identity required. Returns a scoped pre-claim credential plus a claim token; bind to a real user later via the OTP claim ceremony.

## Register with an identity assertion

Send a POST to the `register_uri` with the assertion:

```http
POST /agent/auth HTTP/1.1
Host: api.runtype.com
Content-Type: application/json

{
  "type": "identity_assertion",
  "assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
  "assertion": "<id-jag-jwt>",
  "requested_credential_type": "api_key"
}
```

Assertion requirements:

- **Trusted issuer**: currently `https://auth.workos.bot` (WorkOS). Assertions from other issuers are rejected with `untrusted_issuer`.
- **Audience**: `https://api.runtype.com/agent/auth` or `https://api.runtype.com`.
- **Verified email**: the payload must include `email` and `email_verified: true`.
- **Freshness**: assertions older than 10 minutes are rejected; each `jti` is single-use.

Success response (201):

```json
{
  "registration_id": "reg_...",
  "registration_type": "identity_assertion",
  "status": "claimed",
  "credential_type": "api_key",
  "credential": "rt_live_...",
  "scopes": ["*"]
}
```

The credential is full-access and does not expire; no claim ceremony is needed. If the asserted email already belongs to a Runtype account, the request is rejected with `email_already_registered` — sign in at `use.runtype.com` and create an API key directly.

## Register anonymously

Send a POST to the `register_uri`:

```http
POST /agent/auth HTTP/1.1
Host: api.runtype.com
Content-Type: application/json

{
  "type": "anonymous",
  "requested_credential_type": "api_key"
}
```

Success response (201):

```json
{
  "registration_id": "reg_...",
  "registration_type": "anonymous",
  "credential_type": "api_key",
  "credential": "rt_live_...",
  "credential_expires": "2026-05-27T12:00:00.000Z",
  "scopes": [
    "FLOWS:READ",
    "FLOWS:WRITE",
    "AGENTS:READ",
    "AGENTS:WRITE",
    "PRODUCTS:READ",
    "PRODUCTS:WRITE",
    "..."
  ],
  "claim_url": "https://api.runtype.com/agent/auth/claim",
  "claim_token": "...",
  "claim_token_expires": "2026-05-27T12:00:00.000Z",
  "post_claim_scopes": ["*"]
}
```

**Deviation from spec**: The pre-claim credential expires after **24 hours**. Agents that do not claim within this window must re-register. Its purpose is discovery and drafting before the human verifies ownership: flow, agent, and tool execution; Records writes; public surfaces and credentials; schedules; messaging; and external integrations remain blocked. The agent's immediate next step should be the email claim ceremony below.

## Claim ceremony

The claim ceremony binds the anonymous credential to a real user via OTP email verification.

### Step 1 — Trigger claim email

```http
POST /agent/auth/claim HTTP/1.1
Host: api.runtype.com
Content-Type: application/json

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

The user receives an email with a 6-digit OTP (valid for 10 minutes, max 5 attempts).

### Step 2 — Submit OTP

```http
POST /agent/auth/claim/complete HTTP/1.1
Host: api.runtype.com
Content-Type: application/json

{
  "claim_token": "...",
  "otp": "123456"
}
```

Success response:

```json
{
  "registration_id": "reg_...",
  "status": "claimed",
  "credential_type": "api_key",
  "credential": "rt_live_...",
  "scopes": ["*"]
}
```

**Deviation from spec**: On successful claim, the pre-claim credential is **revoked** and a **fresh credential** is issued. The old key will return 401. This is a security measure — any copies of the pre-claim key (in agent memory, logs) do not silently gain elevated permissions.

## Use the credential

```http
GET /v1/flows HTTP/1.1
Host: api.runtype.com
Authorization: Bearer rt_live_...
```

On 401, drop the credential and restart from Step 1 (Discover).

## Pre-claim limits

Pre-claim credentials have broad platform access with cost controls:

- **Models**: Workers AI only (`@cf/google/gemma-4-26b-a4b-it`, `@cf/openai/gpt-oss-120b`)
- **Spend cap**: $0.10 total
- **Daily executions**: 5
- **Surfaces**: 1 development, 0 production
- **Blocked features**: Firecrawl, Exa, Daytona, BYOK, batch, evals, schedules

After claiming, all limits are removed and the account is on the Build plan.

## Errors

| Endpoint                          | Error code                    | HTTP | Recovery                                                          |
| --------------------------------- | ----------------------------- | ---- | ----------------------------------------------------------------- |
| `POST /agent/auth`                | `unsupported_credential_type` | 400  | Use a supported `type` and `requested_credential_type: "api_key"` |
| `POST /agent/auth`                | `unsupported_assertion_type`  | 400  | Use `assertion_type: "urn:ietf:params:oauth:token-type:id-jag"`   |
| `POST /agent/auth`                | `untrusted_issuer`            | 401  | Issuer not in the trust registry; fall back to anonymous          |
| `POST /agent/auth`                | `invalid_assertion`           | 401  | Mint a fresh assertion (signature, audience, expiry, or replay)   |
| `POST /agent/auth`                | `email_already_registered`    | 409  | Sign in at use.runtype.com directly                               |
| `POST /agent/auth`                | `rate_limited`                | 429  | Wait and retry                                                    |
| `POST /agent/auth`                | `service_disabled`            | 503  | Registration is temporarily disabled                              |
| `POST /agent/auth/claim`          | `invalid_claim_token`         | 404  | Re-register                                                       |
| `POST /agent/auth/claim`          | `email_already_registered`    | 409  | Sign in at use.runtype.com directly                               |
| `POST /agent/auth/claim`          | `claimed_or_in_flight`        | 409  | Claim already initiated                                           |
| `POST /agent/auth/claim`          | `claim_expired`               | 410  | Re-register                                                       |
| `POST /agent/auth/claim`          | `email_send_failed`           | 502  | Verification email could not be sent; retry the claim             |
| `POST /agent/auth/claim/complete` | `otp_invalid`                 | 401  | Re-enter OTP                                                      |
| `POST /agent/auth/claim/complete` | `otp_expired`                 | 410  | Re-initiate claim                                                 |
| `POST /agent/auth/claim/complete` | `previously_claimed`          | 409  | Already claimed                                                   |
| `POST /agent/auth/claim/complete` | `claim_expired`               | 410  | Re-register                                                       |
| `POST /agent/auth/claim/complete` | `credential_expired`          | 410  | Re-register                                                       |

## Revocation

No agent-facing revocation endpoint for v1. On 401, drop the credential and re-register.

The `revocation_uri` in the metadata returns 501 until the agent-verified flow ships.
