# auth.md

Not an agent? You are probably looking for https://docs.modelslab.com.

You are an agent. This document tells you how to register a credential for the **ModelsLab API** — image, video, audio, and 3D generation. Follow the steps in order; do not skip ahead.

ModelsLab uses **email-based registration with a claim ceremony** (`service_auth`). You register on behalf of a human user with that user's email address. Signup returns a working API key immediately — you do not have to wait for the claim to start generating. The claim ceremony is what makes the account permanent and unlocks the control-plane bearer token.

There is no anonymous registration and no identity-assertion (ID-JAG) flow today. A real, non-disposable email address is required.

- Resource server: `https://modelslab.com/api`
- Authorization server: `https://modelslab.com`
- Agent control plane: `https://modelslab.com/api/agents/v1`

## Discover

Two hops, both unauthenticated:

1. `GET https://modelslab.com/.well-known/oauth-protected-resource` — returns `authorization_servers`.
2. `GET https://modelslab.com/.well-known/oauth-authorization-server` — returns the `agent_auth` block with `register_uri`, `identity_types_supported`, and the claim and revocation URIs.

## Pick a method

`identity_types_supported` is `["service_auth"]`. There is one method, so there is nothing to choose — go to Step 1.

If you were hoping for `anonymous` or `identity_assertion`: neither exists here. Do not probe for them. You need the user's email address before you can register.

## Step 1 — Register

```http
POST /api/agents/v1/auth/signup HTTP/1.1
Host: modelslab.com
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "<generated>",
  "name": "Example User"
}
```

No authentication.

Response (201):

```json
{
  "message": "Signup successful. Verify your email to continue.",
  "user": { "id": 12345, "email": "user@example.com", "name": "Example User", "verified": false },
  "api_key": "ml-...",
  "requires_verification": true
}
```

What each field is:

- `api_key` — your credential for the generation APIs. It works the moment it is issued, on a trial account. Use it now ([Step 2](#step-2--use-the-credential)). Persist it; it is not re-displayed at signup.
- `requires_verification` — always `true`. A verification email has been sent to the address you registered. The `api_key` keeps working unclaimed, but `login` will refuse to issue a control-plane token until the user completes [Step 3](#step-3--claim-ceremony-email-verification).

Requirements:

- `email` — must be unique and must not be a disposable address. A duplicate or disposable address returns 422.
- `password` — minimum 6 characters. Generate a strong one and persist it alongside `api_key`; you need it in [Step 4](#step-4--exchange-credentials-for-a-control-plane-token). Do not log it.
- `name`, `phone` — optional.

Signup is rate limited per IP. On 429, back off exponentially; do not rotate addresses to evade the limit.

## Step 2 — Use the credential

The generation APIs take the API key in the `key` request header, or as `key` in the JSON body:

```http
POST /api/v6/realtime/text2img HTTP/1.1
Host: modelslab.com
Content-Type: application/json
key: ml-...

{ "prompt": "a red bicycle" }
```

The credential works against the generation APIs while the account is still unclaimed, subject to trial limits. An unclaimed account is not a degraded auth state — if you get an error here, read it as a quota or validation problem, not as a reason to re-register.

## Step 3 — Claim ceremony (email verification)

The user receives an email containing a **Verify Email** link:

```
https://modelslab.com/user/verify/<verification_code>
```

You cannot read the user's inbox, so you cannot complete this step alone. There are two paths.

### 3a. The user clicks the link (default)

Surface the ask in your UI. Suggested prompt:

> "I've created a ModelsLab account for `user@example.com`. Check your inbox and click **Verify Email** to finish setup."

Then poll [Step 4](#step-4--exchange-credentials-for-a-control-plane-token) every 5 seconds:

- 403 `email_not_verified` → still pending; keep polling.
- 200 → claimed. You have a token; stop polling.

Time out after ~5 minutes. If it hasn't completed, stop blocking and tell the user they can finish later — `api_key` keeps working in the meantime.

### 3b. The user pastes the link or code back to you

Extract the `verification_code` from the URL and submit it yourself:

```http
POST /api/agents/v1/auth/verify HTTP/1.1
Host: modelslab.com
Content-Type: application/json

{ "verification_code": "<code>" }
```

Response (200):

```json
{
  "message": "Email verified successfully.",
  "access_token": "<token>",
  "token_type": "Bearer",
  "expires_at": "2026-08-16T00:00:00+00:00",
  "api_key": "ml-..."
}
```

This returns the control-plane token directly — skip Step 4 and go to [Step 5](#step-5--use-the-access_token).

A 404 `verification_token_invalid` means the code was already used or is wrong. If the account is already verified, go to Step 4 instead of resending.

### Resend

```http
POST /api/agents/v1/auth/resend-verification HTTP/1.1
Content-Type: application/json

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

Always returns 200 — it deliberately does not disclose whether the address is registered. Do not treat the 200 as proof the email exists.

## Step 4 — Exchange credentials for a control-plane token

```http
POST /api/agents/v1/auth/login HTTP/1.1
Host: modelslab.com
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "<generated>",
  "device_name": "my-agent",
  "token_expiry": "1_month"
}
```

`token_expiry` is one of `1_week`, `1_month`, `3_months`, `6_months`, `1_year`, `never`. Default is `1_month`. Account policy may cap what you asked for — check `token_lifetime_capped` and `expires_at` in the response rather than assuming you got the lifetime you requested.

Response (200) includes `access_token`, `token_type: "Bearer"`, `scopes`, `expires_at`, and the account's `api_key`.

## Step 4b — Ask for only the access you need (scopes)

Omit `scopes` and you get `*` — a token that can also change the plan, add a card and drain the wallet. If your job does not need that, ask for less. Send a `scopes` array on the same login call:

```http
POST /api/agents/v1/auth/login HTTP/1.1
Host: modelslab.com
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "<generated>",
  "device_name": "catalogue-reader",
  "scopes": ["models:read", "usage:read"]
}
```

The full list is at `GET /api/agents/v1/auth/scopes` (no auth required), and in `scopes_supported` at `/.well-known/oauth-authorization-server`.

| Scope | Grants |
| --- | --- |
| `*` | Everything. The default when `scopes` is omitted. |
| `profile:read` / `profile:write` | Read / update the account profile, password and preferences. |
| `tokens:read` / `tokens:write` | List tokens / revoke tokens, log out, switch account context. |
| `api-keys:read` / `api-keys:write` | Read / create and delete generation API keys. |
| `usage:read` | Usage summaries, per-product usage, request history. |
| `models:read` | Model catalogue, filters, tags, providers. |
| `files:write` | Upload files and base64 payloads as generation inputs. |
| `billing:read` / `billing:write` | Read billing and invoices / manage payment methods and billing info. |
| `wallet:read` / `wallet:write` | Read balance and payment status / fund, withdraw, redeem coupons. |
| `subscriptions:read` / `subscriptions:write` | Read plans and subscriptions / buy, change, pause, resume. |
| `teams:read` / `teams:write` | Read teams / create, modify, invite. |

Calling outside your scopes returns `403` with `error.code` = `insufficient_scope`. The body names `required_scope` and `granted_scopes`, and the `WWW-Authenticate` header repeats the required scope. Re-authenticate asking for that scope; do not retry the same call.

`POST /auth/switch-account` accepts `scopes` too, and can never widen them: the token it issues is the intersection of what you ask for and what the token performing the switch already holds.

Each operation in [agents-openapi.json](https://modelslab.com/agents-openapi.json) carries its scope as `x-required-scope`, so you can compute the minimum set for your workflow before you authenticate.

## Step 5 — Use the access_token

```http
GET /api/agents/v1/me HTTP/1.1
Host: modelslab.com
Authorization: Bearer <access_token>
```

The control plane covers the account, profile, billing, teams, and API keys.

Keep the two credentials straight — they are not interchangeable:

| Credential | Sent as | Use for |
| --- | --- | --- |
| `api_key` (`ml-...`) | `key` header or body field | Generation APIs (`/api/v6`, `/api/v7`, ...) |
| `access_token` | `Authorization: Bearer` | Control plane (`/api/agents/v1`) |

## Errors

| Status | Code | Meaning | What to do |
| --- | --- | --- | --- |
| 422 | `validation_error` | Payload failed validation | Fix the payload; details are in the error body. Do not retry unchanged. |
| 422 | `invalid_email` | Disposable email address | Ask the user for a real address. Do not cycle through addresses. |
| 401 | `invalid_credentials` | Wrong email or password | Do not retry the same pair. Re-check what you persisted at signup. |
| 403 | `email_not_verified` | Claim not completed | Return to [Step 3](#step-3--claim-ceremony-email-verification). |
| 403 | `access_denied` | Account is banned | Stop. Do not re-register; contact support. |
| 403 | `insufficient_scope` | Token lacks the scope this call needs | Re-authenticate requesting `required_scope` from the error body. Do not retry unchanged. |
| 404 | `verification_token_invalid` | Code used or wrong | If already verified, go to Step 4; otherwise resend. |
| 429 | `rate_limited` | Rate limited | Read `Retry-After` and `RateLimit-Reset`, wait that long, then retry. |
| 5xx | — | Transient server error | Exponential backoff, retry the same request. |

Every response carries its rate-limit state, so you do not have to discover a limit by hitting it: `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (seconds), plus the structured `RateLimit-Policy` and `RateLimit` fields. Throttle against `RateLimit-Remaining` rather than retrying blind.

Retry policy:

- 5xx → exponential backoff, retry the same request.
- 4xx other than 429 → do not retry the same payload; act on the table above.
- 401 on a previously-working `access_token` → the token expired or was revoked. Re-authenticate at [Step 4](#step-4--exchange-credentials-for-a-control-plane-token). Do not re-register.

## Revocation

Control-plane tokens, with `Authorization: Bearer <access_token>`:

- `POST /api/agents/v1/auth/logout` — revoke the token you are using.
- `POST /api/agents/v1/auth/logout-all` — revoke every token on the account.
- `GET /api/agents/v1/auth/tokens` — list tokens.
- `DELETE /api/agents/v1/auth/tokens/{id}` — revoke one token.
- `POST /api/agents/v1/auth/tokens/revoke-others` — revoke all except the current one.

Revoking a token does **not** revoke `api_key`, and revoking an API key does not revoke your tokens — the two layers are independent. Manage keys at `/api/agents/v1/api-keys`.

There is no revocation event stream today. If a credential stops working, re-authenticate at Step 4 rather than waiting for a notification.

## Versioning

The control plane is versioned in the URL (`/api/agents/v1`) and breaking changes only ever land on a new version path. Every versioned API response carries `ModelsLab-Api-Version` and `ModelsLab-Api-Lifecycle` (`current`, `supported` or `deprecated`); once a version is deprecated it also carries `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) dates, with at least 90 days between them. The policy is at https://modelslab.com/api-deprecation-policy.

Full API reference: https://docs.modelslab.com/agents-api/overview.
