Documentation menu

reference

Device sign-in

Device sign-in for command line tools and MCP clients: a person approves access in the dashboard instead of copying a key. The only endpoints that take no API key.

Device sign-in for command line tools and MCP clients: a person approves access in the dashboard instead of copying a key. The only endpoints that take no API key.

POST/auth/device

Start a device sign-in#

Scope
none
Rate limit
60/min (auth)
Publishable key
refused
Dashboard permission
none

Starts a sign-in for a command line tool or MCP client, modelled on the OAuth device authorization grant (RFC 8628). Takes no API key: this is how a client comes to have one.

The client generates the key. Create a secret the same shape as ours, wv_sk_ followed by 32 base64url characters (24 random bytes), keep it private, and send only its SHA-256 as key_hash plus its first 12 characters as key_prefix. The secret never leaves your machine; approval registers its hash as an ordinary secret key.

Show the person verification_uri_complete (or verification_uri and user_code). They sign in to the dashboard, or create an account first, choose a Site, and approve. Then poll POST /auth/device/token every interval seconds until it says approved, and start using your key. The request expires after expires_in seconds.

The key is created for the approving person, on the Site they choose, with the scopes you asked for that they are allowed to grant, and expires after 90 days. It appears under Settings > API keys, where it can be revoked like any other.

Request body

FieldTypeNotes
client_name*string

What the person sees on the approval screen, such as the name of your tool.

client_hoststring | null

The machine asking, such as its hostname, so the person can recognise it.

key_hash*string

SHA-256 of the full secret key you generated, as lower-case hex.

key_prefix*string

The first 12 characters of that key, shown in the dashboard to identify it.

scopes*Scope

The scopes you want. The key receives those the approving person may grant on the Site they pick; the rest are dropped.

Responses

  • 201

    The sign-in is waiting for approval.

    FieldTypeNotes
    device_code*string

    Opaque. Keep it private and send it when you poll.

    user_code*string

    What the person confirms on the approval screen.

    verification_uri*uri
    verification_uri_complete*uri

    The approval page with the code filled in. Show this one when you can.

    expires_in*1800
    interval*5

    Seconds to wait between polls.

  • 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.

  • 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. The two device sign-in endpoints take no key, so they are limited per IP address instead.

    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
    authPOST /auth/device and POST /auth/device/token, per IP address60 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/auth/device \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
  "client_name": "<client_name>",
  "key_hash": "<key_hash>",
  "key_prefix": "<key_prefix>",
  "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/auth/device/token

Poll a device sign-in#

Scope
none
Rate limit
60/min (auth)
Publishable key
refused
Dashboard permission
none

Reports whether the person has approved the sign-in. It returns an outcome, never a credential: on approved, the key you generated is live, and the response says which Site it belongs to, which scopes it carries and when it expires.

pending means keep waiting. slow_down means you polled faster than interval; wait the interval it returns before the next poll. denied and expired are final: start again with a new key.

Request body

FieldTypeNotes
device_code*string

Responses

  • 200

    The current outcome.

    FieldTypeNotes
    status*string

    one of pending, slow_down, approved, denied, expired

    intervalinteger

    Present on slow_down. Seconds to wait before polling again.

    keyobject

    Present on approved.

    websiteobject

    Present on approved. The Site the key belongs to.

  • 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.

  • 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.

  • 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. The two device sign-in endpoints take no key, so they are limited per IP address instead.

    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
    authPOST /auth/device and POST /auth/device/token, per IP address60 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/auth/device/token \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
  "device_code": "<device_code>"
}'

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.

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