Documentation menu

reference

API keys

Key management. Specified here, implemented in API-2.

GET/keys

List API keys#

Scope
keys:read
Rate limit
600/min (read)
Publishable key
refused
Dashboard permission
api_keys.manage

Metadata only. The secret itself is shown once, at creation, and is never retrievable afterwards because only a hash is stored. key_prefix is the displayable fragment you use to tell keys apart.

Requires a secret key carrying keys:read. A publishable key can never read this.

Parameters

NameInTypeNotes
cursorquerystring

The opaque cursor from data.next_cursor on the previous page. Do not parse it or construct one; its encoding is not part of this contract and will change.

limitqueryinteger

Page size. Values above the maximum are clamped rather than rejected, so a client asking for a thousand rows gets a hundred and a next_cursor.

default 20

Responses

  • 200a page of ApiKey

    A page of keys.

  • 401

    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.

  • 403

    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.

  • 429

    RATE_LIMIT_EXCEEDED. Back off and honour Retry-After.

    Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 500

    INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against the request_id in the body, so quote it if you contact support.

    Safe to retry, and safer still with the same Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded.

  • 503

    MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in Retry-After.

    • Retry-After Seconds to wait before retrying.

Request shape

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

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

POST/keys

Create an API key#

Scope
keys:write
Rate limit
120/min (write)
Publishable key
refused
Dashboard permission
api_keys.manage

Returns the secret exactly once, in data.secret. Store it immediately. There is no way to recover it later, and support cannot retrieve it for you.

A key can never be created with more authority than its creator has. The scopes you request are intersected with the creator's own permissions, and the granted set comes back in data.scopes. If you asked for something you do not hold, the key is still created, without it. Compare what you asked for against what you got.

A secret key's authority is also re-evaluated on every request against its creator's live permissions, so revoking a person's access immediately narrows every key they made.

A retry does not give you the secret again. Idempotency-Key guarantees you created one key rather than two, and a replayed response returns the key's id, kind, key_prefix and scopes with secret: null and secret_replayable: false. The secret is never stored anywhere, including in the idempotency record, because storing it would put a live credential in a database backup. If you lost it, rotate the key.

Parameters

NameInTypeNotes
Idempotency-Key*headerstring

A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice.

Retry with the same key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out.

Same key with a different body is 409 IDEMPOTENCY_KEY_CONFLICT, because reusing a key for different work is a bug in your client rather than a retry. Same key while the first request is still running is 409 IDEMPOTENCY_KEY_IN_FLIGHT; wait and retry.

Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation.

Request body

FieldTypeNotes
name*string

A label for humans. Say where the key lives, so you know what breaks if you revoke it.

kind*KeyKind

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.

scopes*Scope
expires_atdate-time | null

Optional expiry. After it passes the key returns 401 API_KEY_EXPIRED.

Responses

  • The key was created. This is the only time the secret is returned.

  • 400

    INVALID_REQUEST. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header.

  • 401

    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.

  • 403

    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.

  • 409

    SLUG_CONFLICT, IDEMPOTENCY_KEY_CONFLICT, IDEMPOTENCY_KEY_IN_FLIGHT or CONFLICT. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Read code to tell which, then re-read and retry.

  • 422

    VALIDATION_FAILED. The request parsed but the values are not acceptable. error.fields maps each offending field to a message you can put next to the input.

    Common causes: publishing without a title, slug or content; scheduling in the past; and sending status on a create or update, which is how the API refuses to let a client push content into the AI pipeline.

  • 429

    RATE_LIMIT_EXCEEDED. Back off and honour Retry-After.

    Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 500

    INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against the request_id in the body, so quote it if you contact support.

    Safe to retry, and safer still with the same Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded.

  • 503

    MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in Retry-After.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
curl -X POST https://api.writavo.com/v1/keys \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "<name>",
  "kind": "publishable",
  "scopes": "<scopes>"
}'

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

POST/keys/{id}/rotate

Rotate an API key#

Scope
keys:write
Rate limit
120/min (write)
Publishable key
refused
Another Site's id
404
Dashboard permission
api_keys.manage

Issues a new secret for the same key record, keeping its name and scopes, and returns it once.

As with creation, a replayed retry returns secret: null and secret_replayable: false rather than handing out the credential a second time. Rotate again if you lost it.

Set grace_seconds to keep the old secret working while you deploy the new one. During the grace window both work. After it, the old one returns 401 API_KEY_REVOKED. A grace of 0 cuts the old secret off immediately, which is the right choice if you are rotating because it leaked.

Parameters

NameInTypeNotes
id*pathuuid
Idempotency-Key*headerstring

A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice.

Retry with the same key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out.

Same key with a different body is 409 IDEMPOTENCY_KEY_CONFLICT, because reusing a key for different work is a bug in your client rather than a retry. Same key while the first request is still running is 409 IDEMPOTENCY_KEY_IN_FLIGHT; wait and retry.

Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation.

Request body (optional)

FieldTypeNotes
grace_secondsinteger

Responses

  • Rotated. The new secret is returned once.

  • 400

    INVALID_REQUEST. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header.

  • 401

    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.

  • 403

    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.

  • 404

    NOT_FOUND. Either no such object exists, or it exists and belongs to a different Site.

    These two cases are deliberately indistinguishable, and neither ever returns 403. A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site.

  • 409

    SLUG_CONFLICT, IDEMPOTENCY_KEY_CONFLICT, IDEMPOTENCY_KEY_IN_FLIGHT or CONFLICT. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Read code to tell which, then re-read and retry.

  • 429

    RATE_LIMIT_EXCEEDED. Back off and honour Retry-After.

    Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 500

    INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against the request_id in the body, so quote it if you contact support.

    Safe to retry, and safer still with the same Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded.

  • 503

    MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in Retry-After.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
curl -X POST https://api.writavo.com/v1/keys/3f1b0c7a-0000-4000-8000-000000000001/rotate \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

DELETE/keys/{id}

Revoke an API key#

Scope
keys:write
Rate limit
120/min (write)
Publishable key
refused
Another Site's id
404
Dashboard permission
api_keys.manage

Immediate and permanent. The key record is kept, marked revoked, so the audit trail survives, but the secret stops working at once and returns 401 API_KEY_REVOKED.

Revoking the key you are calling with is allowed. It is the last request that key makes.

Parameters

NameInTypeNotes
id*pathuuid

Responses

  • 204

    Revoked. No body.

  • 401

    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.

  • 403

    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.

  • 404

    NOT_FOUND. Either no such object exists, or it exists and belongs to a different Site.

    These two cases are deliberately indistinguishable, and neither ever returns 403. A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site.

  • 429

    RATE_LIMIT_EXCEEDED. Back off and honour Retry-After.

    Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.

    ClassEndpointsLimit
    readevery GET600 per minute
    writePOST, PATCH and DELETE on content, taxonomy, keys and webhooks120 per minute
    uploadPOST /media/upload-url60 per minute
    pipelinePOST /pipeline/runs10 per minute

    Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you are refused rather than after.

    • RateLimit-Limit Requests permitted in the current window for this endpoint class.
    • RateLimit-Remaining Requests left in the current window.
    • RateLimit-Reset Unix seconds at which the window resets.
    • Retry-After Seconds to wait before retrying.
  • 500

    INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against the request_id in the body, so quote it if you contact support.

    Safe to retry, and safer still with the same Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded.

  • 503

    MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given in Retry-After.

    • Retry-After Seconds to wait before retrying.

Request shape

curl
curl -X DELETE https://api.writavo.com/v1/keys/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"

Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.

Schemas

The shapes this resource sends and returns. Unknown fields may be added within /v1, so tolerate them.

ApiKey#

Key metadata. The secret is never included except in the response that created or rotated it.

FieldTypeNotes
id*uuidread only
name*string
kind*KeyKind

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.

key_prefix*stringread only

The only displayable fragment of the secret. Use it to identify a key in your own UI.

scopes*Scope

The scopes actually granted, after intersection with the creator's permissions.

last_used_atdate-time | nullread only
expires_atdate-time | null
revoked_atdate-time | nullread only
created_atdate-timeread only

KeyKind#

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.

publishablesecret

Scope#

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