Skip to navigation

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 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:

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:

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.

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:

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.