> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# Authentication

> API keys, sponsor tokens, tenants, and team invites — the two-credential model.

Writ has two credentials, held by two different parties. The **API key**
says *which tenant is asking*. The **sponsor token** says *a human
approved this*. Most integrations only ever touch the API key; the
sponsor token belongs to the human in the loop.

## API keys

An API key looks like `writ_...` and belongs to a tenant. It authorizes:

* `POST /v1/check` — the point-of-action decision
* `POST /v1/tokens/verify` — auth token validation
* `GET /v1/policy`, `PUT /v1/policy` — the tenant's verb policy
* `GET /v1/me` — tenant and key identity
* `GET /v1/receipts*` — reading, streaming, and verifying the audit log
* `POST /v1/tenant/invites` — team invite codes

Issue one with no auth at all:

```bash
curl -X POST https://api.withwrit.com/v1/keys \
  -H 'Content-Type: application/json' \
  -d '{"email": "ops@example.com", "tenantName": "Acme support"}'
```

The first key for an email **creates the tenant** and returns the
tenant's `sponsorToken` — shown exactly once, never recoverable. Save it
immediately. Lost it? Sign in to the portal and rotate it (API keys →
Rotate sponsor token): the old token dies the moment the new one is
issued. Later keys for the same email (or keys issued via an invite code)
join the existing tenant and do not return a sponsor token.

Then call the API:

```bash
curl https://api.withwrit.com/v1/me \
  -H 'Authorization: Bearer writ_KEY'
```

Key issuance is rate-limited to 10 keys per hour per client IP.

Creating a key from the signed-in portal additionally requires TOTP
step-up (below): the request body carries the 6-digit code as `totpCode`.

## Authenticator app (TOTP step-up)

A portal session is powerful — it can mint keys, rotate the sponsor
token, and rewrite policy — and a session is one magic link away for
anything that can read your email, including your own agents. So issuing
**new credentials** requires a second factor: a TOTP code from an
authenticator app. This gates the portal's `POST /v1/keys` and
`POST /v1/sponsor-token/rotate` only — every other portal action works
with the session alone.

Enroll once, from the portal (API keys → Security):

1. `POST /v1/totp/enroll` (signed-in session) returns an `otpauth://` URI.
   The portal renders it as a QR code — scan it with any authenticator
   app (Google Authenticator, 1Password, …), or enter the secret manually.
2. `POST /v1/totp/confirm` with `{"code": "123456"}` verifies one code
   and activates enrollment. It returns **10 backup codes**, shown once:
   each works in place of an authenticator code, exactly once, if you
   lose your device. Only their hashes are stored.

It is fail-closed: until enrollment, both issuance endpoints return
`403 totp_required` ("no new keys until you're enrolled"). Once enrolled,
the code (or a backup code) goes in the request body's `totpCode` field.
Verification accepts ±30 seconds of clock skew; attempts are
rate-limited to 10 per 5 minutes per tenant. Enrollment writes a
`TOTP_ENROLL` receipt to the audit log.

TOTP is the RFC 6238 standard — no vendor, no per-code cost. The secret
is stored reversibly (it has to be, to verify codes); everything else
in Writ remains hashes-only.

## Sponsor tokens

A sponsor token looks like `writ_sp_...` and belongs to the tenant's
human. It authorizes the human-approval half of the product:

* `POST /v1/grants` — approve one write (the STEP_UP flow)
* `POST /v1/revoke` — the kill switch
* `POST /v1/reinstate` — undo a revocation
* `GET /v1/revoked` — the kill-switch roster

It goes in the same `Authorization: Bearer` header, on those endpoints
only. API keys are rejected there and sponsor tokens are rejected
everywhere else — the two credentials do not substitute for each other.

Treat the sponsor token like a password: it can approve money movement
and revoke agents. Store it in a secret manager, never in code or chat
logs.

Rotation is self-serve and session-gated: in the portal (API keys →
Rotate sponsor token), a signed-in session mints a new token and kills
the old one immediately, writing a `ROTATE` receipt to the audit log.
The API key alone can never rotate it — the agent's credential must not
be able to lock out the human. Rotations are rate-limited to 5 per hour
per tenant. Once you've enrolled an authenticator app, rotation also
requires your TOTP code (see above).

## Signing up on the web

The portal sign-in page doubles as signup. Enter any email:

1. Writ emails a one-time link (15 minutes, single use). If the address
   already has an account it is a sign-in link; otherwise it is a
   **create-your-account** link. The page shows the same "check your
   email" either way — it never reveals whether an address has an
   account, so addresses can't be probed.
2. Clicking the link is the confirmation. For a new address, the tenant
   is created at click time — typing an address alone never creates an
   account, so nobody can mint accounts in someone else's name.
3. You land on a **Welcome to Writ** screen showing the API key and
   sponsor token exactly once. Save them (a password manager is the
   right home), then enter the portal.

The welcome credentials are issued by a single-use server-side grant: a
second visit, or a second tab racing the first, gets "already issued"
instead of a second copy. Closed the tab before saving? Sign in again —
the portal routes you back to the Welcome screen until the credentials
are issued.

## Tenants and teams

A tenant is the isolation boundary: one audit log, one policy, one set
of revocations. A key only ever sees its own tenant's receipts — there is
no cross-tenant read path, and a foreign receipt ID is indistinguishable
from a missing one.

To add teammates, create an invite code with your API key:

```bash
curl -X POST https://api.withwrit.com/v1/tenant/invites \
  -H 'Authorization: Bearer writ_KEY'
```

Teammates pass the code as `inviteCode` to `POST /v1/keys` and land in
your tenant. Creating a new code rotates the old one. Joining members
never receive the sponsor token — there is one human credential per
tenant, issued at creation.

## The keyless sandbox

No signup, no key: `POST /v1/sandbox` mints a 90-second grant, but only
for `verb: "demo_write"` on targets starting with `demo-`. It's the
trial loop — prove the intercept works, then issue a real key for real
verbs.