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 hello@writersapi.com 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 the reopening of the Lekki toll bridge after 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). 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".
languagestringnoneAny language name or tag: "French", "pt-BR", "Igbo". Default: the language of your prompt.
stylestringnone"neutral", "professional", "journalistic", "conversational", "friendly", "persuasive", "formal", "academic", "witty", "technical", "storytelling", "simple"
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. "small business owners in Kenya".
keywordsstring[]noneUp to 20 keywords to work in naturally.
formatstring"markdown""markdown", "html", "text", "json". json returns the parsed object in data.json too.
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": "# Lekki toll bridge reopens...",
    "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

Templates add structure and editorial rules for common jobs. They never replace your prompt. GET /v1/templates returns this list as JSON.

templateForWords (medium)
articleA news or feature article with a headline, a strong lead and clear sections.700
blog-postAn engaging blog post with a hook, scannable sections and a takeaway.900
listicleA numbered list article: an intro, numbered items with explanations, a wrap-up.900
reviewA balanced product or service review with pros, cons and a verdict.900
press-releaseA standard press release: headline, dateline, lead, quotes and boilerplate.500
copyShort persuasive copy for ads, landing pages and calls to action.200
product-descriptionA product page description: benefits first, then features and specifications.250
how-toStep-by-step instructions with what you need and tips.800
newsletterAn email newsletter with a subject line, a short intro and sections.500
social-postA social media post, sized for the platform named in the prompt.120
emailA clear, purposeful email with a subject line.200
summaryA faithful summary of the text supplied in the prompt.200
rewriteRewrites the supplied text in a new style while keeping its meaning.as needed
seo-metaAn SEO title (up to 60 characters) and meta description (up to 155).as needed

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. Models that are currently broken go last and are re-tested every 30 minutes, so a recovered model comes back on its own.

What happens when a model fails:

outcomeMeaningWhat the router does
unavailableTimeout, network error, 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 six 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.

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-flashgeminiWritingWith your own keyweb search, JSON, thinking
gpt-6.1-solchatgptWritingWith your own keyweb search, JSON, thinking
deepseek-v4-prodeepseekWritingWith your own keyJSON, thinking
gemini-4-argongeminiWritingWith your own keyweb search, JSON, thinking
gpt-6-astrachatgptWritingWith your own keyweb search, JSON, thinking
deepseek-flashdeepseekWritingWith your own keyJSON, thinking
gpt-6-lunachatgptWritingWith your own keyweb search, JSON, thinking
gemini-3.1-flash-litegeminiWritingWith your own keyweb search, JSON, thinking
flux-2-profluxImageWith your own keyn/a
nano-banana-2-litenano-bananaImageWith your own keyn/a
gpt-image-2chatgptImageWith your own keyn/a
flux-3fluxImageWith your own keyn/a
nano-banana-2.1nano-bananaImageWith your own keyn/a
gpt-image-minichatgptImageWith your own keyn/a
flux-2-kleinfluxImageWith your own keyn/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.
  • Body size: 512 KB. Prompt: 100,000 characters.
  • 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 Accra.",
    "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"]))