Documentation menu

authentication

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.

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.

wv_pub_

Publishable

Safe in a browser bundle, a mobile app or a static site build. Read only, published content only, and admitted to 11 of the 50 operations. Everything else is a 403 whatever scopes it carries.

wv_sk_

Secret

Server side only. Can carry any scope, can read drafts, can write, can spend. Treat it like a database password: never in a repository, never in client code, never in a log line.

What a publishable key cannot do

returns 403
curl
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.

articles:readarticles:writetaxonomy:readtaxonomy:writeauthors:readauthors:writemedia:readmedia:writepipeline:readpipeline:runkeys:readkeys:writewebhooks:readwebhooks:writemeta:read

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
curl
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, 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.