Skip to navigation

Principals

Every Writ API key belongs to exactly one principal: an entry in your tenant’s identity directory — a team, a service, an agent. Keys carry the principal’s id, every check is enforced against it, and every receipt snapshots it. The portal’s Principals page is the directory UI; the /v1/principals endpoints are the API.

This is a different concept from the kill-switch “principal” (a sponsor/agent pair you revoke after an incident) — see the data dictionary.

Why principals exist

Without them, a key is just a string: revoking it is one click, but pausing a contractor’s access means hunting down every key they hold. Principals group keys by identity, so the human operations are one click:

  • Disable a principal and every one of its keys gets DENY on the next check — until you re-enable it. No key surgery.
  • Revoke all kills every key of the principal at once, each with its own REVOKE receipt carrying your reason. Offboarding in one click.
  • Delete tombstones the principal: its keys are revoked first (?force=1), the record stays resolvable by id forever, and the name becomes reusable.

Naming

Names are unique per tenant, case-insensitively: Billing and billing are the same name. 1–64 characters after trimming; letters, digits, spaces, hyphens, underscores, and periods; at least one letter or digit.

Every tenant has a default principal. If you never create one, Writ creates it the first time the directory is touched, and every key that predates the directory is attached to it — so the API keys table’s Principal column is never blank.

Issuing keys to a principal

Pass principal_id when you create a key. The id must belong to a live principal in your tenant — an unknown, deleted, or disabled principal is a 400, because a key must never be minted against an identity that can’t authorize.

curl -X POST https://api.withwrit.com/v1/keys \
-H "Content-Type: application/json" \
-d '{"email":"ops@example.com","principal_id":"prn_9d2f4a1b2c3d"}'
{
"apiKey": "writ_7hK2xQ9mZp4v...",
"tenantId": "ten_3fa8b91c2d44",
"principalId": "prn_9d2f4a1b2c3d",
"plan": "free"
}

Portal-created keys land on default unless your integration passes principal_id through the API.

Enforcement: fail closed

On every POST /v1/check, the gate resolves the key’s principal and:

  1. Deleted or missing principal → DENY ("principal deleted"), receipted.
  2. Disabled principal → DENY ("principal disabled"), receipted.
  3. Otherwise the check proceeds normally, and the receipt carries principalId / principalName — the principal as it was at write time.

Renames and deletes never rewrite history: an old receipt always shows the name the principal had when the decision was made, and the id in principalId still resolves — tombstoned principals stay fetchable forever.

Managing the directory

# create
curl -X POST https://api.withwrit.com/v1/principals \
-H "Authorization: Bearer writ_KEY" -H "Content-Type: application/json" \
-d '{"name":"billing"}'
# list (add ?include_deleted=1 to see tombstoned entries)
curl https://api.withwrit.com/v1/principals \
-H "Authorization: Bearer writ_KEY"
# rename / disable
curl -X PATCH https://api.withwrit.com/v1/principals/prn_9d2f4a1b2c3d \
-H "Authorization: Bearer writ_KEY" -H "Content-Type: application/json" \
-d '{"name":"invoicing","disabled":true}'
# revoke every key of the principal (each gets its own REVOKE receipt)
curl -X POST https://api.withwrit.com/v1/principals/prn_9d2f4a1b2c3d/revoke \
-H "Authorization: Bearer writ_KEY" -H "Content-Type: application/json" \
-d '{"reason":"contractor offboarded"}'
# delete (refuses with 409 while keys are live; ?force=1 revokes them first)
curl -X DELETE "https://api.withwrit.com/v1/principals/prn_9d2f4a1b2c3d?force=1" \
-H "Authorization: Bearer writ_KEY"

Directory mutations write their own receipts — PRINCIPAL_CREATE, PRINCIPAL_UPDATE, PRINCIPAL_DELETE — so the audit log shows not just what the agents did, but who was allowed to exist when they did it.

For agents

If your agent manages Writ tenants programmatically: resolve the principal once, store the principalId (stable across renames), and pass it as principal_id at key creation. When you offboard an identity, call POST /v1/principals/{id}/revoke — one call, every key dead, every revocation receipted with your reason.

Per-principal policy overrides

A principal can carry its own policy override — the top layer of the policy precedence: it beats the environment override, which beats the tenant default. Unset verbs inherit downward, and the dashboard’s Policy page shows, per verb, where the effective mode comes from. Only live principals can carry policy: PUT /v1/policies/principals/{id} on a tombstoned or disabled principal is 400.

Disabling or deleting a principal makes its override inert — it never supplies a mode while the principal can’t authorize checks, and re-enabling re-arms it. The override data is kept, not wiped.