Authentication
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 decisionPOST /v1/tokens/verify— auth token validationGET /v1/policy,PUT /v1/policy— the tenant’s verb policyGET /v1/me— tenant and key identityGET /v1/receipts*— reading, streaming, and verifying the audit logPOST /v1/tenant/invites— team invite codes
Issue one with no auth at all:
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:
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):
POST /v1/totp/enroll(signed-in session) returns anotpauth://URI. The portal renders it as a QR code — scan it with any authenticator app (Google Authenticator, 1Password, …), or enter the secret manually.POST /v1/totp/confirmwith{"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 switchPOST /v1/reinstate— undo a revocationGET /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:
- 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.
- 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.
- 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:
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.