API reference

WritersAPI docs

One JSON API for AI writing and images. Every request is routed to a working model and falls back on its own when a provider fails.

On this page
  1. Quickstart
  2. Authentication
  3. Write text
  4. Templates and options
  5. Generate images
  6. Routing and fallback
  7. Models
  8. Usage
  9. Errors
  10. Limits
  11. Billing
  12. Code examples

Quickstart

Every request needs an API key. Keys are issued by the WritersAPI team: email [email protected] with your site's name and whether you want hosted keys or to bring your own. Then:

Write an article
curl https://writersapi.com/v1/write \
  -H "Authorization: Bearer $WRITERSAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Write about a city reopening its main bridge after six weeks of repairs.",
    "template": "article",
    "style": "journalistic",
    "length": "medium"
  }'

The answer is in data.content. Which model wrote it, whether a fallback was needed and what it cost are returned alongside.

Authentication

Send your key in the Authorization header as a bearer token. Keys start with wapi_.

Header
Authorization: Bearer wapi_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Call the API from your server only. A key in browser code can be copied and spent by anyone; the API sends no CORS headers, so browsers can't call it directly anyway.

Write text

POST/v1/write

Your prompt is passed to the model exactly as sent. Everything else is optional: templates and options add instructions, and your own system text is added last so it wins over them.

FieldTypeDefaultWhat it does
promptstringrequiredWhat to write, or the text to work on. Up to 100,000 characters.
systemstringnoneYour site's own instructions (voice, rules, house style), up to 20,000 characters. Applied after templates and options, so it takes precedence.
modelstring"auto""auto", a family ("deepseek", "gemini", "chatgpt", "flux", "nano-banana"), or an exact model id from Models.
fallbackbooleantrueTry other models when the first fails (up to 4). false: exactly one model is tried.
preferencestring"balanced""balanced" follows our priority order, "quality" tries flagship models first, "cost" the cheapest first.
web_searchbooleanfalseGround the answer in live web results. Only models that can search are used, and their sources come back in citations.
templatestringnoneOne of the templates, e.g. "press-release". Sets the structure (parts and their order), not the tone.
languagestringnoneAny language name or tag: "French", "pt-BR", "Tagalog". Default: the language of your prompt.
stylestringnone"neutral", "professional", "journalistic", "conversational", "friendly", "persuasive", "formal", "academic", "witty", "technical", "storytelling", "simple". Sets the tone; works with any template.
lengthstringnone"short", "medium", "long". Scales the template's usual length.
wordsintegernoneTarget word count (20 to 10,000). Overrides length.
audiencestringnoneWho it is for, e.g. "owners of small online shops".
keywordsstring[]noneUp to 20 keywords to work in naturally.
formatstring"markdown""markdown", "html", "text", "json". json returns the parsed object in data.json too; with a template and no schema, each part is its own key.
json_schemaobjectnoneA JSON Schema the answer must follow, with format "json".
temperaturenumbernone0 to 2. Ignored by models that don't take it (thinking mode).
max_tokensinteger8192Output cap, 16 to 64,000. 32,000 by default with thinking on, since thinking counts toward it.
thinkingbooleanfalseLet models that support it reason before answering. Slower and costs more; better for complex briefs.

Response

200 OK
{
  "success": true,
  "data": {
    "id": "req_8213",
    "content": "# Main bridge reopens after six weeks of repairs...",
    "format": "markdown",
    "truncated": false,
    "citations": [],
    "model": { "id": "gpt-6.1-sol", "family": "chatgpt", "provider": "openrouter" },
    "requested_model": "auto",
    "fallback_used": true,
    "upgraded_from": null,
    "attempts": [
      { "model": "gemini-3.8-flash", "outcome": "unavailable", "latency_ms": 30012 },
      { "model": "gpt-6.1-sol", "outcome": "ok", "latency_ms": 6240 }
    ],
    "usage": {
      "input_tokens": 412,
      "output_tokens": 903,
      "cost_usd": 0.010341,
      "provider_cost_usd": 0.009849,
      "billing": "hosted"
    }
  }
}
  • truncated is true when the answer hit max_tokens; raise it and retry.
  • upgraded_from names the model you asked for when it was retired and its successor answered instead.
  • cost_usd is what this request costs you: 0 with your own keys.

Templates and options

A template sets the structure of the piece: which parts it has and in what order, like the subject line and signature of an email or the dateline and media contact of a press release. Tone is separate, set with style. Templates never replace your prompt. GET /v1/templates returns this list as JSON.

templateStructureWords (medium)
articleheadline, standfirst, lead, body, what happens next700
blog-posttitle, intro, sections, takeaway, call to action900
listicleheadline, intro, items, conclusion900
reviewtitle, summary, what it is, in use, pros, cons, alternatives, verdict900
press-releaserelease line, headline, subheadline, dateline, lead, body, quotes, about, media contact, end mark500
copyheadline, subheadline, body, proof points, call to action200
product-descriptionproduct name, opening, who it's for, key features, specifications250
how-totitle, outcome, what you need, steps, tips and common mistakes800
newslettersubject line, preview text, opening, sections, call to action, sign-off500
social-posthook, body, call to action, hashtags120
emailsubject line, greeting, body, next step, sign-off, signature200
summaryone-line summary, key points, details200
rewritetextas needed
seo-metatitles, meta descriptions, url slugas needed

Parts in italics are included only when the brief has the material for them. Without web search, every template tells the model not to invent facts, quotes or figures, and to leave a placeholder in square brackets where your brief lacks a detail.

Generate images

POST/v1/images

FieldTypeDefaultWhat it does
promptstringrequiredWhat to draw. Up to 4,000 characters.
modelstring"auto""auto", "flux", "nano-banana", "chatgpt", or an exact model id.
fallbackbooleantrueTry other image models when the first fails.
preferencestring"balanced""balanced", "quality" or "cost".
aspect_ratiostring"1:1""1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"
stylestringnone"photo", "editorial", "illustration", "flat-vector", "3d", "watercolor", "line-art", "cinematic"
output_formatstring"png""png", "jpeg" or "webp". FLUX honours it; other models return their own format, named in mime_type.
200 OK
{
  "success": true,
  "data": {
    "id": "req_8214",
    "image": { "b64_json": "iVBORw0KGgo...", "mime_type": "image/png" },
    "aspect_ratio": "16:9",
    "model": { "id": "flux-2-pro", "family": "flux", "provider": "bfl" },
    "fallback_used": false,
    "attempts": [{ "model": "flux-2-pro", "outcome": "ok", "latency_ms": 9120 }],
    "usage": { "input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0315, "provider_cost_usd": 0.03, "billing": "hosted" }
  }
}

One image per request, returned as base64. Save it to your own storage; nothing is kept on our side.

Routing and fallback

For every request the router builds an ordered list of models, then tries them until one answers:

  1. Only models for the task, enabled, and able to do what you asked (web search, for example) are considered.
  2. A model you named goes first. If it was retired, its successor (or its family's best working model) takes its place.
  3. Then the rest of the same family, then every other model, ordered by your preference.
  4. Providers that were overloaded in the last 5 minutes go after every other working model. Models that are currently broken go last and are re-tested every 30 minutes, so a recovered model comes back on its own.

Answers stream from the provider. If a model sends nothing within 15 seconds (60 with thinking, plus 15 with web search), or stops for 20 seconds mid-answer (45 with thinking), the next model takes over. A long article that keeps arriving is never cut off.

What happens when a model fails:

outcomeMeaningWhat the router does
overloadedThe provider is too busy, or queued the request without answeringTries the next model at once, puts that provider last for 5 minutes and alerts the team.
unavailableTimeout, network error, other provider 5xx, unreadable answerTries the next model. Three in a row mark the model broken and alert the team.
rate-limitThe provider is throttlingTries the next model. The model keeps its standing.
model-goneRetired, renamed or removed by the providerMarks it broken at once, alerts the team, tries the next model.
authThe provider rejected the keySkips that provider for the request and alerts the team.
billingThe provider account is out of creditSame as auth.
rejectedThe model refused or couldn't process this requestTries the next model. Says nothing about the model's health.

Every 3 hours, WritersAPI also asks each provider which models it still offers. A model missing from two checks in a row is retired and its traffic moves to its successor, before any request fails on it. A newer stable release of a model already in the lineup is added on its own and tried just before the version it replaces, which stays as its fallback.

Models

GET/v1/models

The live lineup with status, features and hosted prices. No key needed. Use a model's id as model to pin it, or a family name to always get that family's best working model.

Model idFamilyTaskStatusFeatures
gemini-3.8-flashgeminiWritingAvailableweb search, JSON, thinking
gpt-6.1-solchatgptWritingAvailableweb search, JSON, thinking
deepseek-v4-prodeepseekWritingAvailableJSON, thinking
gemini-4-argongeminiWritingAvailableweb search, JSON, thinking
gpt-6-astrachatgptWritingAvailableweb search, JSON, thinking
deepseek-flashdeepseekWritingAvailableJSON, thinking
gpt-6-lunachatgptWritingAvailableweb search, JSON, thinking
gemini-3.5-flash-litegeminiWritingAvailableweb search, JSON, thinking
gemini-3.1-flash-litegeminiWritingAvailableweb search, JSON, thinking
flux-2-profluxImageAvailablen/a
nano-banana-2-litenano-bananaImageAvailablen/a
gpt-image-2chatgptImageAvailablen/a
flux-3fluxImageAvailablen/a
nano-banana-2.1nano-bananaImageAvailablen/a
gpt-image-minichatgptImageAvailablen/a
flux-2-kleinfluxImageAvailablen/a

Usage

GET/v1/usage

This calendar month's totals (UTC) for the key you send: requests, errors, tokens, images, cost and budget.

200 OK
{
  "success": true,
  "data": {
    "key": "Example News", "billing": "hosted", "month": "2026-10",
    "requests": 1840, "errors": 3, "input_tokens": 905112, "output_tokens": 1630442,
    "images": 212, "cost_usd": 24.18, "budget_usd": 100, "rate_per_minute": 60
  }
}

Errors

Errors use the same envelope with success: false, a stable code and a readable message. Failed routing also returns the attempts.

StatuscodeMeaning
400invalid_request, invalid_json, unknown_model, unsupportedFix the request. The message names the field.
401missing_key, invalid_keyNo key, a wrong key, or a disabled key.
402budget_exceededThe key's monthly budget is used up.
413too_largeThe body is over 512 KB.
415unsupported_media_typeSend Content-Type: application/json.
422rejectedEvery model refused the request. Check the prompt.
429rate_limitedSlow down; wait for the Retry-After seconds.
503all_models_failed, no_model_available, unavailableNothing could answer right now. Retry after the Retry-After seconds.
503
{
  "success": false,
  "code": "all_models_failed",
  "message": "No model could complete this request right now. Retry shortly.",
  "attempts": [
    { "model": "gemini-3.8-flash", "outcome": "unavailable", "latency_ms": 30010 },
    { "model": "gpt-6.1-sol", "outcome": "rate-limit", "latency_ms": 412 }
  ]
}

Limits

  • Requests per minute: set per key, 60 by default. Over it you get 429 with Retry-After.
  • Per address: 600 requests a minute to /v1 from one IP, across all keys. Over it you get 429.
  • Body size: 512 KB. Prompt: 100,000 characters; system: 20,000; image prompt: 4,000.
  • Time: a writing request may take up to 6 minutes across all fallbacks (4 per model); an image request up to 4. Set your HTTP client's timeout above that.

Billing

Hosted keys pay the provider's cost plus 5%; keys with their own provider keys pay nothing to us. Only the model that answered is billed. See pricing.

Code examples

Node.js (18+)
const res = await fetch("https://writersapi.com/v1/write", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WRITERSAPI_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "Five ways small shops can cut energy bills this winter.",
    template: "listicle",
    language: "English",
    format: "html",
  }),
  signal: AbortSignal.timeout(7 * 60 * 1000),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.code}: ${body.message}`);
console.log(body.data.model.id, body.data.content);
PHP
$ch = curl_init("https://writersapi.com/v1/write");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 420,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer " . getenv("WRITERSAPI_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "prompt" => "Announce our new office in Toronto.",
    "template" => "press-release",
  ]),
]);
$body = json_decode(curl_exec($ch), true);
echo $body["success"] ? $body["data"]["content"] : $body["message"];
Python
import os, requests

r = requests.post(
    "https://writersapi.com/v1/images",
    headers={"Authorization": f"Bearer {os.environ['WRITERSAPI_KEY']}"},
    json={"prompt": "A quiet harbour at dawn", "aspect_ratio": "16:9", "style": "editorial"},
    timeout=300,
)
body = r.json()
if body["success"]:
    import base64
    open("harbour.png", "wb").write(base64.b64decode(body["data"]["image"]["b64_json"]))