Aandall.

Aandall API

Fact-check and localize documents from your own application — a CMS, a publishing pipeline, or a script.

Getting started

  1. Create an API key on your Account page, under API keys. Copy it — it's shown only once.
  2. Send it with every request as Authorization: Bearer aandall_live_…
  3. Usage draws on your normal token balance. No separate billing.

Keep keys server-side. A key spends your tokens, so never put one in a web page, a mobile app, or public source control. If a key leaks, revoke it on your Account page.

How work is submitted

Fact-checking and localization take from a few seconds to a couple of minutes, so the API is asynchronous. You create a job, get an id back immediately, then poll the job until it reports succeeded. Polling every 3–5 seconds is plenty. A job survives network problems on your side — the work continues on our servers, and the result waits for you.

Base URL

https://aandall.org/v1

Improve a document

Grammar, fact-checking against live sources, references, and your learned writing style.

POST /v1/documents/improve

{
  "content": { "format": "html", "body": "<p>The sun rises in the west.</p>" },
  "comments": [
    { "id": "c1", "note": "make this punchier" }
  ]
}

Responds 202 with a job:

{ "object": "job", "id": "AbC123…", "type": "improve",
  "status": "queued", "created": "2026-08-02T14:05:00Z",
  "result": null, "usage": null, "error": null }

Anchoring comments to specific passages

A comment is an instruction about a particular piece of text — the API equivalent of a margin note. Anchoring takes two parts that must match:

  1. In the HTML, wrap the passage the note is about in <span class="cmt" data-comment-id="your-id">…</span>
  2. In comments, send an entry whose id is that same your-id, with your instruction in note.

The ids are yours to choose — any short string works, as long as the two sides agree.

{
  "content": {
    "format": "html",
    "body": "<p>Our platform <span class=\"cmt\" data-comment-id=\"c1\">leverages
             synergistic paradigms</span> to deliver value. <span class=\"cmt\"
             data-comment-id=\"c2\">Founded in 2019.</span></p>"
  },
  "comments": [
    { "id": "c1", "note": "plain English please, this is jargon" },
    { "id": "c2", "note": "check this date, I think it was 2018" }
  ]
}

Each note is applied to its marked passage, and the <span> wrappers are stripped from the result — the text you get back is clean.

Document-wide instructions. A comment whose id matches no span is treated as guidance for the whole document. Useful for things like {"id": "g1", "note": "keep it under 400 words"} or {"id": "g2", "note": "British spelling throughout"}.

No comments at all? Omit the field. The document then gets a full editorial pass: grammar, fact-checking, references, and your learned writing style.

If you're pulling HTML from a CMS, add the comment spans after fetching the content and before sending it — don't store them in the CMS. They're a transport detail, not part of the author's document.

Localize a document

Send your document as segments — typically one per paragraph or heading. Segment ids are yours; we echo them back so you can put translations where they belong.

POST /v1/documents/localize

{
  "language": "Spanish",
  "region": "Mexico",
  "segments": [
    { "id": "p1", "content": { "format": "html", "body": "<h1>Welcome</h1>" } },
    { "id": "p2", "content": { "format": "html", "body": "<p>Hello there.</p>" },
      "translation": { "body": "<p>Hola.</p>" },
      "note": "too formal — make it warmer" }
  ]
}

Translator notes and human edits

Localize has no spans — each segment is already its own unit, so a note attaches directly to the segment it sits in. Three combinations, and each behaves differently:

What you sendWhat happens
content only Translated fresh.
content + translation Returned unchanged. A human approved this wording; we never silently overwrite it.
content + translation + note Revised — honouring both the human's wording and their instruction.

This is what makes iterative review work: send the whole document every time, and only the segments a translator has commented on get reworked. Everything they've settled stays put, and untouched segments are retranslated with the latest context.

Review a translation

Ask whether a translation says what the source says. This returns findings, never a rewrite — nobody's words are changed. Send each segment with both its source and its translation:

POST /v1/documents/review

{
  "language": "Tamil",
  "segments": [
    { "id": "p2",
      "content":     { "format": "html", "body": "<p>Founded in 2019.</p>" },
      "translation": { "body": "<p>…</p>" } }
  ]
}

A finished review job:

{ "object": "job", "type": "review", "status": "succeeded",
  "result": {
    "object": "review_result",
    "language": "Tamil",
    "segments_checked": 42,
    "findings": [
      { "object": "finding", "segment_id": "p2", "severity": "serious",
        "kind": "omission",
        "note": "The founding year is missing from the translation." }
    ] } }

severity is minor, moderate or serious, and findings come back most serious first. kind is one of meaning, omission, addition, tone, terminology, formatting, name, number. An empty findings array means the translation is sound — that is a result, not a failure.

note is written in English, for someone who does not read the target language. That is the point: it lets you judge a Tamil translation without reading Tamil.

Acting on findings

There is no separate "fix it" endpoint, because you already have one. Put a finding's note onto that segment in a localize call, alongside the translation you have:

{ "id": "p2",
  "content":     { "format": "html", "body": "<p>Founded in 2019.</p>" },
  "translation": { "body": "<p>…</p>" },
  "note": "The founding year is missing from the translation." }

That is the content + translation + note case above: revised, honouring the existing wording and the instruction. Segments with no finding are left alone.

To read a translation back in your own language, you don't need a separate endpoint either — send the translated segments to localize with language set to your source language. Reviewing tells you what is wrong; back-translating lets you see the whole thing for yourself.

Guides — rules for the whole document

A guide is what an author would tell a translator or editor before they start: rules that apply to every segment and that no model could infer from the text alone. Don't gloss Hindi words for Hindi readers. Keep proper names untranslated. British spelling. It is optional, and it works on both endpoints — writing presets for improve, translation presets for localize.

POST /v1/documents/localize

{
  "language": "Tamil",
  "segments": [ … ],
  "guide": {
    "presets": ["no_gloss", "keep_names"],
    "notes": "The author is Tamil. Do not explain Tamil terms back to them."
  }
}

presets are named rules we maintain; notes is your own free text, up to 2,000 characters. Use either, or both.

Finding out which presets exist

Don't hard-code the list — ask for it. GET /v1/account returns every preset, grouped, with a label and a description for each:

{ "object": "account",
  …
  "guide_presets": {
    "object": "guide_presets",
    "translation": [ { "key": "no_gloss", "label": "…", "group": "…" }, … ],
    "writing":     [ … ]
  } }

A preset we don't recognise is reported, not silently dropped

If you send a preset we don't know — a typo, or a writing preset on a translation job — the rest of your guide still applies, and the ones we ignored come back on the job as guide_ignored:

{ "object": "job", "id": "AbC123…", "type": "localize", "status": "succeeded",
  "guide": { "object": "guide", "presets": ["no_gloss"], "notes": "…" },
  "guide_ignored": ["no_glos", "spelling_uk"],
  … }

Check guide_ignored in development. A misspelled preset is not an error — your document still translates. It just translates without the rule you thought you'd set, which is a much harder thing to notice later.

Poll a job

GET /v1/jobs/{id}

status is one of queued, running, succeeded, failed. A finished improve job:

{ "object": "job", "id": "AbC123…", "type": "improve", "status": "succeeded",
  "result": {
    "object": "improve_result",
    "content":  { "format": "html",     "body": "<p>The sun rises in the east…</p>" },
    "analysis": { "format": "markdown", "body": "## Fact-checking…" }
  },
  "usage": { "object": "usage", "tokens_in": 4210, "tokens_out": 3050,
             "tokens_thinking": 1840, "tokens_charged": 9100 },
  "error": null }

A finished localize job returns result.segments, each with your original id and the translated content.body.

Reading the usage figures

tokens_in + tokens_out + tokens_thinking = tokens_charged. It always adds up — if it ever doesn't, tell us, because that's a bug.

tokens_thinking is work the model does before it answers. Newer models reason through a problem first, and that reasoning costs tokens even though you never see it. It is billed, so we show it rather than folding it invisibly into your total. On a model that doesn't reason it is 0.

Check your balance

GET /v1/account
{ "object": "account", "email": "you@example.com",
  "tokens": { "object": "token_balance", "balance": 412000, "used_total": 88000 },
  "limits": { "object": "limits", "requests_per_minute": 60,
              "concurrent_jobs": 10, "max_words_per_document": 15000 },
  "guide_presets": { "object": "guide_presets",
                     "translation": [ … ], "writing": [ … ] } }

Use guide_presets to populate a menu in your own interface rather than shipping a copy of our list that goes stale.

Errors

(Available only to select developers, reach out to the.sri.ss@gmail.com if you need access)

Every error shares one shape. Branch on type, never on the message — messages may be reworded, types will not.

{ "object": "error", "type": "insufficient_tokens",
  "message": "Not enough tokens for this job.",
  "details": { "tokens_required": 180000, "tokens_available": 40000 } }
typeHTTPMeaning
unauthorized401Missing, malformed, or revoked API key.
invalid_request400Something's wrong with the request body.
insufficient_tokens402Top up at aandall.org/account.
not_found404No such job, or it isn't yours.
too_large413Document exceeds the word limit.
rate_limited429Slow down; see details.retry_after_seconds.
server_error500Our fault. Retry; you aren't charged for failed jobs.

A complete example

curl -X POST https://aandall.org/v1/documents/improve \
  -H "Authorization: Bearer $AANDALL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":{"format":"html","body":"<p>Draft text.</p>"}}'

# → {"object":"job","id":"AbC123","status":"queued", …}

curl https://aandall.org/v1/jobs/AbC123 \
  -H "Authorization: Bearer $AANDALL_KEY"

On Windows: PowerShell's curl is an alias for a different tool with different syntax. Use curl.exe explicitly, or Invoke-RestMethod.

Stability — read this before you build

v1 is in preview and is still changing. We are actively adding capabilities to it — see below — and while we are, we may rename a field or change a shape rather than carry a design mistake forever. We would rather get this right than be politely wrong for years.

What we will do: tell you before anything breaking lands, and never change a shape without a reason we can explain. What we ask: write your integration to ignore fields it doesn't recognise, and email us so we know you exist. Anyone building against this gets told first.

Once the surface settles, this section becomes the ordinary promise — additions only, breaking changes at /v2, and /v1 kept running.

What's changing next

So you can build in the right direction rather than around us:

ComingWhat it means for you
A choice of model Translation will be able to run on more than one engine, traded off for cost and quality. Today the choice is ours and the API says nothing about it. If that becomes yours to make, it will be a new optional field — never a change to an existing one.
Multi-turn translation Today every call is self-contained: you resend the whole document with your notes, and we rework only the segments you commented on. That works, but it means you carry the state. We are looking at letting a document persist across calls so you can send just the notes. The current shape will keep working.

Changelog

DateChange
6 Sep 2026 New: POST /v1/documents/review — check a translation against its source and get findings back.
6 Sep 2026 Added tokens_thinking to usage, so the figures reconcile with what is charged.
6 Sep 2026 Added guide to improve and localize; guide and guide_ignored echoed on jobs; guide_presets added to /v1/account.

Questions

Building a CMS plugin or an integration? Email the.sri.ss@gmail.com — we'd like to hear what you're making.