# Keys, kinds and scopes

Every request carries a key. The key decides which Site you are talking to, what you may do, and whether you are safe to embed in a browser.

## Sending a key

`Authorization: Bearer <key>`.

Two kinds of key, distinguished by their prefix:

- **`wv_pub_`, publishable.** Safe in a browser or a mobile app. Read only, and it can
  only see content at `status: published`. It cannot read drafts, cannot write, and
  cannot see keys, usage, media, the pipeline queue or webhooks. The operations it may
  reach are marked `x-publishable: true`; everything else is `403 INSUFFICIENT_SCOPE`
  for a publishable key regardless of its scopes.
- **`wv_sk_`, secret.** Server side only. Never ship one to a client, and never commit
  one. It can carry any scope.

A secret key's authority is its creator's live permissions intersected with the scopes
it was given. Narrowing or removing that person's access narrows every key they created,
on the next request. A key never outranks the person who made it.

Only a hash is stored. A lost key cannot be recovered, only rotated.

## The two kinds

`publishable` keys are prefixed `wv_pub_` and are safe in client code: read only, limited
to published content, and admitted only to the operations marked `x-publishable: true`.
Anything else returns `403 INSUFFICIENT_SCOPE`, whatever scopes the key carries.

`secret` keys are prefixed `wv_sk_` and are server side only.

**What a publishable key cannot do** (returns 403)

```bash

curl https://api.writavo.com/v1/usage \
  -H "Authorization: Bearer wv_pub_EXAMPLE000000000000000000000000000"

```

`GET /usage` returns the organisation's credit balance and plan, so a browser-safe key is refused whatever scopes it carries.

## What a publishable key may reach

The list is deny by default. An operation is reachable by a publishable key only when it has been explicitly reasoned about, which is why three read scopes are not available to one at all.

> `GET /usage` is the instructive exception. It sits under `meta:read`, which a publishable key may hold, but it returns the organisation credit balance and plan. Scope alone was not a fine enough gate, so each operation states its own answer.

## Scopes

What a key is permitted to do. A key carries a set of these, and every operation names
the one it needs.

Scope is the outer bound, not the whole answer. A request must also pass the creator's
live permissions, the plan entitlement, and, for a pipeline run, credits and the spend
cap. A scope you hold can still be refused by the gate behind it.

A publishable key may only carry `:read` scopes, and only ever sees published content.
Three of them are deliberately NOT available to a publishable key, because the endpoints
behind them return things a browser bundle must not carry: `media:read` lists the whole
library including assets attached only to drafts, `pipeline:read` returns the Site's
unpublished editorial queue, and `webhooks:read` is account configuration.

`meta:read` IS publishable, but `GET /usage` within it is not: it returns the
organisation's credit balance and plan, and a publishable key presenting it gets
`403 INSUFFICIENT_SCOPE`. Every operation states which side of that line it is on in
`x-publishable`.

## A key never outranks its creator

> A secret key's authority is **its creator's live permissions intersected with its scopes**. It is resolved on every request, not frozen at creation.

This has a consequence worth knowing before it surprises you rather than after. If the person who created a key is demoted, has a permission removed, or leaves the organisation, every key they created narrows or stops working on the next request. An integration that has been running for months can start returning `403 INSUFFICIENT_SCOPE` because of a change nobody connected to it.

- Create keys for long lived integrations under an account that will not be reorganised, not under whoever happened to be setting it up.
- When someone leaves, expect their keys to stop. Recreate them under a current member rather than trying to restore the old permissions.
- `GET /keys` shows the scopes actually granted, after the intersection. If a scope you asked for is missing, the creator did not have it.

**List the keys on this Site** (returns 200)

```bash

curl https://api.writavo.com/v1/keys \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"

```

Metadata only. A secret is shown once at creation and is never retrievable, because only a hash is stored.

## Expiry, rotation and revocation

- **Shown once.** Only a hash is stored. A lost key cannot be recovered, only rotated.
- **Expiry is optional.** Set `expires_at` when you create a key and it stops working at that moment with `API_KEY_EXPIRED`, which is a cleaner failure than a key that lives for ever.
- **Rotation issues a new secret** and returns it once. Update your integration, then let the old one go.
- **Revocation is immediate** on the next request, and answers `API_KEY_REVOKED`.
- **`key_prefix` is the only displayable fragment.** Use it to tell keys apart in your own logs and UI.

All four are operations on the [keys resource](/docs/api/api-keys), and all four are also available in the dashboard.

## When authentication fails

`INVALID_API_KEY`, `API_KEY_REVOKED` or `API_KEY_EXPIRED`. There is no key, or it is not
usable. This response never distinguishes "no such key" from "wrong key", so a caller
cannot probe for valid keys.

`INSUFFICIENT_SCOPE`. The key is valid but does not carry the scope this operation
requires, or its creator's permissions no longer cover it.

This is the **only** 403 in the API. In particular it is never returned for an object
belonging to another Site: that case is always 404, so this response never reveals that
something exists.
