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
| Field | Type | Notes |
|---|---|---|
voice | string | A voice id from /api/v1/voices. Default pg. |
text | string | Required. Up to 6000 words. Plain text; markdown in the input is fine, the output never has any. |
stream | boolean | Default false. true returns text/event-stream. Not available with mode: "markdown" (400). |
mode | string | "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-AfterTrial 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>"}.
| Status | error | When |
|---|---|---|
| 400 | bad_request | Body is not JSON, text is empty, mode is unknown, or stream was combined with mode: "markdown". |
| 401 | unauthorized | Missing, malformed, unknown or revoked key. |
| 402 | trial_exhausted | The trial key's lifetime quota is used up. upgrade_url points at /keys. |
| 404 | unknown_voice | No such voice id. |
| 413 | too_long | More than 6000 words. |
| 429 | quota_exceeded | Daily quota reached. Retry-After gives the seconds until 00:00 UTC. |
| 429 | rate_limited | Burst limit per IP. Retry-After: 60. |
| 503 | upstream, refused | The 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.