API reference

Rewrite API

One endpoint that rewrites text in a named voice, one that lists voices, and a key dashboard. JSON in, JSON or server-sent events out.

Authentication

Every request to /api/v1/rewrite carries a key in the Authorization header. Keys look like bw_live_ followed by 43 URL-safe characters. Mint and revoke them at /keys after signing in with Google. We store only a keyed hash of each key, so a key is shown exactly once, when minted.

Authorization: Bearer bw_live_...

POST /api/v1/rewrite

FieldTypeNotes
voicestringA voice id from /api/v1/voices. Default pg.
textstringRequired. Up to 6000 words. Plain text; markdown in the input is fine, the output never has any.
streambooleanDefault false. true returns text/event-stream. Not available with mode: "markdown" (400).
modestring"plain" (default) rewrites the whole text as one piece. "markdown" keeps structure: front matter, code fences, tables, headers, link-only lines, list and blockquote markers, and any line under 12 words stay byte for byte; each paragraph or list item of 12 words or more is rewritten on its own, in parallel. The response adds blocks_rewritten.
curl https://betterwriting.ai/api/v1/rewrite \
  -H "Authorization: Bearer $BW_KEY" \
  -H "Content-Type: application/json" \
  -d '{"voice":"pg","text":"We are thrilled to announce our new pricing."}'

Response, 200:

{
  "id": "7a1f...",
  "voice": "pg",
  "text": "We changed our prices. Here is why.",
  "words_in": 8,
  "words_out": 7,
  "model": "claude-fable-5-1",
  "prompt_version": "v1",
  "latency_ms": 2140
}

Headers x-quota-remaining-rewrites and x-quota-remaining-words report what is left today for this key.

Streaming

With "stream": true the response is text/event-stream. Each event is a data: line of JSON. Text arrives as {"delta":"..."} events; the last event is {"done":true, ...} with the same fields as the non-streaming response, including the final cleaned text. Replace what you displayed with that final text: it has markdown stripped and em dashes removed, which the deltas cannot guarantee mid-stream. An error after the stream opened arrives as {"error":"...","message":"..."}.

curl -N https://betterwriting.ai/api/v1/rewrite \
  -H "Authorization: Bearer $BW_KEY" \
  -H "Content-Type: application/json" \
  -d '{"voice":"pg","text":"...","stream":true}'

data: {"delta":"We"}
data: {"delta":" changed our prices."}
data: {"done":true,"id":"...","voice":"pg","text":"We changed our prices. ...","words_in":8,"words_out":7,"model":"...","prompt_version":"v1","latency_ms":2140}

POST /api/v1/trial

No account. Send a stable device identifier (the plugin uses the sha256 of hostname, user and machine id) and get a trial key with a lifetime quota of 15,000 words and 300 rewrites. The same device gets the same key back while it has quota. Three new trials per network per day, 300 per day overall.

curl https://betterwriting.ai/api/v1/trial \
  -H "Content-Type: application/json" \
  -d '{"device":"<sha256 of hostname+user+machine-id>"}'

201 {"api_key":"bw_trial_...","quota":{"words":15000,"rewrites":300},"remaining":{"words":15000,"rewrites":300}}
200 same device, same key, quota left
409 {"error":"trial_exhausted","upgrade_url":"https://betterwriting.ai/keys"}
429 {"error":"rate_limited"} or {"error":"trials_capped"} with Retry-After

Trial keys work on /api/v1/rewrite exactly like account keys; the quota headers report what is left for life rather than for the day. When it is used up, rewrites answer 402 with {"error":"trial_exhausted","upgrade_url":"https://betterwriting.ai/keys"}. Sign in on /keys and paste the trial key to attach its history to your account. Coding-agent setup lives on the agents page.

GET /api/v1/voices

No auth. Returns the roster with what is serving each voice right now.

{
  "voices": [
    {
      "id": "pg",
      "name": "Paul Graham",
      "handle": "paulg",
      "status": "live",
      "prompt_version": "v1",
      "model": "claude-fable-5-1"
    }
  ]
}

Errors

Errors are JSON: {"error":"<code>","message":"<human text>"}.

StatuserrorWhen
400bad_requestBody is not JSON, text is empty, mode is unknown, or stream was combined with mode: "markdown".
401unauthorizedMissing, malformed, unknown or revoked key.
402trial_exhaustedThe trial key's lifetime quota is used up. upgrade_url points at /keys.
404unknown_voiceNo such voice id.
413too_longMore than 6000 words.
429quota_exceededDaily quota reached. Retry-After gives the seconds until 00:00 UTC.
429rate_limitedBurst limit per IP. Retry-After: 60.
503upstream, refusedThe model backend failed or declined the text. Quota is refunded.

Quotas

Per key, per UTC day: 50 rewrites and 20,000 input words. A request that would cross either cap gets 429 and is not counted. Keys are free; ask if you need more.

Keys

The dashboard at /keys uses the same JSON routes with a session cookie: GET /api/keys lists keys with today's usage, POST /api/keys {"name":"..."} mints one (at most five active), DELETE /api/keys/:id revokes.

What we log

For each call: the key id, voice, word counts, latency, backend, model and prompt version. Never the text, in or out. See privacy.