# Altalandi — full API reference Turn English or stiff/AI-translated Icelandic into text that reads as if a native Icelander wrote it — and prove the meaning didn't change. One call: `POST /v1/refine`. It's a guarded pipeline, not a single model call: a frontier LLM rewrites to native register (L1), a rule proofreader cleans up (L2), then a validation gate scores translationese, flags calques, checks meaning (a back-translation judge AND a deterministic check that numbers, dates, times, negation, names and obligations survived), and refuses any rewrite the pipeline itself has found to be broken (L3). Every change comes back as a diff with the reason. **AI agents: read https://altalandi.is/agents.md first.** It is the short version of this page, with the result-reading procedure, a wrapper and a tool definition. ## Base URL & auth ``` https://altalandi.is/api # v1 endpoints under /v1/... Authorization: Bearer # required on /v1/* ``` Mint a key in the dev portal at https://altalandi.is/minar-sidur (create an account, then "Búa til lykil"). A missing/invalid key returns 401; when the dev API is disabled it returns 404. Every account gets 2,000 free units a day on the standard writer (about 2,000 words), shared by its keys; the max writer is always paid from credit. Until an account has paid, it is limited to 10 calls a minute and 2 at once. Codes redeemed in the portal can make use free within their own limits, give a discount, or add credit. Past that, calls are paid from the account's prepaid credit, held in krónur: a new account also gets 500 kr of trial credit once, when its email is confirmed (at once for a Google sign-up). Operators can grant a key free daily units (or unlimited use) for partners. Free use (calls covered by the free units) is kept in the text bank with personal data scrubbed, unless the request sends `"contribute": false` or the account opted out in the portal; paid calls are kept only with `"contribute": true` (only with the person's consent) or account-wide consent; ChatGPT text is never kept. A key with environment "test" never runs a model and is never charged: same response shape, output = input, `test: true`, `meaningPreserved: null`. CLI: `altalandi --diff` prints the before/after and every change; `--report` (or ALTALANDI_REPORT=1) writes a Markdown report to ./altalandi-reports/. Store the one-time plaintext in a password manager. Load `ALTALANDI_API_KEY` via a masked prompt or secret manager; do not paste it into shell commands or committed config. Bash: ```bash read -rsp "Altalandi API key: " ALTALANDI_API_KEY; echo export ALTALANDI_API_KEY ``` PowerShell 7: ```powershell $env:ALTALANDI_API_KEY = Read-Host "Altalandi API key" -MaskInput ``` ## POST /v1/refine ### Request fields | field | type | default | meaning | |-------|------|---------|---------| | text | string | (required) | The text to refine (EN or IS). Hosted limit: 20,000 characters. | | sourceLang | "auto" \| "en" \| "is" | "auto" | Force the input language or auto-detect. | | domain | "general" \| "marketing" \| "data-analysis" \| "childrens-fiction" | "general" | Tunes register/idiom. | | register | "formal" \| "neutral" \| "warm" | "neutral" | Tone. | | preserveMeaning | boolean | true | Run the back-translation meaning gate. | | scoreNativeness | boolean | true | Return a 0–100 nativeness score + tells. | | onlyImprove | boolean | false | IS-refine guarantee, **opt-in**: keep the original unless a trustworthy judge establishes the rewrite is more native. Off by default because the pairwise judge mis-ranks stiff vs native often enough to revert good rewrites (reads as "it did nothing"). When true it fails closed — an unavailable, disabled or conflicted judge returns the original. | | polishPass | boolean | false | L1.5 monolingual polish on EN→IS. Off by default: measured to add latency with no median nativeness gain. | | glossary | [{en, is, note?}] | [] | Force specific term translations. Up to 50 terms; `en`/`is` max 200 characters and `note` max 500. All glossary fields count toward the daily billable input units. | | returnDiffs | boolean | true | Return the per-change diff. | | writer | "standard" \| "max" | "standard" | Writer quality tier (see Quality tiers). | | scoring | "none" \| "fast" \| "rigorous" | "fast" | Nativeness-judging tier (see Quality tiers). | ### Quality tiers — `writer` and `scoring` Two optional fields trade cost for quality, per call: | field | values | default | meaning | |-------|--------|---------|---------| | writer | "standard" \| "max" | "standard" | Which curated writer lane produces the Icelandic. The operator resolves each lane to a model; `max` is intended for long, legal, or formal text. | | scoring | "none" \| "fast" \| "rigorous" | "fast" | How hard the output is judged for nativeness. `none` = skip; `fast` = one judge sample; `rigorous` = three samples for a less noisy score. Both judge models are operator-resolved and must be in a different model family from every writer/reviser used by the request. | Each key has an entitlement ceiling. Requesting a tier above your ceiling does NOT error — the call is downgraded to your ceiling, a warning is added, and the response reflects the downgrade in both `tier` and `billedTier`. Keys default to `writer:"standard"`, `scoring:"fast"`; ask the owner to raise the ceiling for `max` / `rigorous`. `tier` describes the effective execution selection. Its writer value follows an engine-initiated escalation, but its scoring value is the selected scoring behaviour — it is not proof that a judge completed. `billedTier` describes what was charged. If the engine escalates the writer to repair structure or meaning, Altalandi absorbs that escalation (`tier.writer` can be `max` while `billedTier.writer` stays `standard`). Failed or disabled scoring is charged as `none`; if only part of a three-sample `rigorous` run succeeds, the useful one-pass result is returned and charged as `fast`. A warning explains either degradation. Billing — a call costs (billable input units × weight × tariff) krónur, paid from the account's prepaid credit. The tariff is 0.08 kr per unit by default (GET /v1/pricing has the live value), applied when the call runs: a later price change never alters credit already held. Each call counts a MINIMUM of 40 input units before the weight (`minimumBillableUnits` in GET /v1/pricing), because a refine runs the same guarded pipeline whether the text is one word or fifty. A proofread (`task: "proofread"`) costs a fraction of that and counts a minimum of 10 (`minimumBillableUnitsProofread`). The most a call can cost is held before it runs and settled to what it actually cost; a failed call costs nothing. Top-ups earn a volume bonus of extra credit: +5% from 10,000 kr, +10% from 25,000 kr, +15% from 50,000 kr (`topupBonuses` in GET /v1/pricing has the live tiers). The same credit also pays for the web tool at https://altalandi.is when the person is signed in: texts up to 20,000 characters and a choice of the standard or max writer. The count includes `text` and every glossary field. Each whitespace-delimited token is at least one unit; an unbroken token uses one unit per 20 characters, rounded up (so minified code, base64, and similar payloads cannot count as one cheap word): | writer \ scoring | none | fast | rigorous | |------------------|------|------|----------| | standard | 1× | 1× | 3× | | max | 2.5× | 2.5× | 5× | A `max` + `rigorous` call therefore costs 8 units per billable input unit; the default (`standard`/`fast`) spends 1×. Long, structured/legal `standard` input may be auto-routed to `max` for correctness — `tier`, `billedTier`, and a warning make the absorbed escalation explicit. Every glossary field is included in the metered input count. ### Response (200) ```jsonc { "id": "r_a1b2c3d4e5", // per-refine id — quote it in bug reports "output": "Hafðu samband við okkur í dag.", // native IS; on IS-input meaning drift, the original unchanged "sourceLang": "en", "diffs": [ { "before": "ná út til okkar", "after": "hafa samband", "layer": "L1", "reason": "calque of 'reach out' — use 'hafa samband'" } ], "quality": { "nativenessScore": 96, // 0–100 (judge tops out ~98); null if not scored "translationeseTells": [], "calqueFlags": [], "englishRemnants": [], "encodingArtifacts": [] // non-empty ⇒ output is mojibake, do not use }, "meaningPreserved": true, // semantic AND invariants; null = could not be established "meaningCheck": "passed", // passed | changed | reverted | skipped | unavailable (only "passed" is verified) "diffStatus": "listed", // listed | none | reverted | translation | unavailable (why diffs is or isn't empty) "semanticMeaningPreserved": true, // back-translation judge only "invariantsPreserved": true, // numbers, dates, negation, names, obligations survived "invariantChecks": [], // per-invariant evidence "presentationDrift": [], // formatting-only changes that kept the value "rewriteAttempted": true, // false = the text needed no change "rejectionReason": "", // set when a rewrite was refused; output is then your original "candidateVetoFired": false, // true = the rewrite itself was broken and refused "candidateVetoReason": "", "candidateVetoSignal": "", // morphology | concrete_nativeness_tell | encoding_artifacts "meaningNotes": "", "modelUsed": "provider:model-id", // models used for the attempted rewrite; a gate may still return the original "tier": { "writer": "standard", "scoring": "fast" }, // effective execution selection "billedTier": { "writer": "standard", "scoring": "fast" }, // what was charged "warnings": [] } ``` The selected scoring label in `tier` is not proof that a judge completed. Check `quality.nativenessScore`, `warnings`, and `billedTier`. A family conflict or failed judge can leave the score `null` and billing at `none`; a partial `rigorous` result is labelled `rigorous` in `tier`, warned, and billed as `fast`. ### Reading the result (the important one) The engine states the outcome for you: `meaningCheck` is the meaning verdict as a state (`reverted` = `output` is your original), and `diffStatus` says why `diffs` is or isn't empty. Prefer them. The table below is how they are derived, and what to do against an older response that lacks them. Do not decide from `meaningPreserved` alone. Check these in order: | # | check | outcome | |---|-------|---------| | 1 | `quality.encodingArtifacts` is not empty | unusable — discard | | 2 | `rejectionReason` is set, or `meaningPreserved` is `false` | refused — `output` is your original, unchanged | | 3 | `rewriteAttempted` is `false` | already fine — nothing needed changing | | 4 | `meaningPreserved` is not `true` | unverified — do not present as checked | | – | otherwise | rewritten — use `output`, show the `diffs` | Row 2 is why one field is not enough: when the candidate veto refuses a broken rewrite, the meaning check has already passed, so `meaningPreserved` is `true` while `output` is your original. `rejectionReason` begins with `MEANING DRIFT`, `HARD INVARIANT LOST`, `UNVERIFIED` or `CANDIDATE VETO`. For Icelandic input, any refusal returns the original. For English input, returning English would not be an Icelandic result: a translation whose meaning could not be verified comes back with `meaningPreserved:null` and a warning (row 4), and one that clearly changed meaning is retried once with the max writer, returning `502` and no text if that also fails. ### Status codes | code | meaning | |------|---------| | 200 | OK | | 400 | empty `text` | | 401 | missing/invalid API key | | 402 | `insufficient_balance`: account funding is insufficient for this call; add credit (waiting won't help) | | 404 | dev API disabled | | 413 | text too long | | 422 | invalid request shape or glossary limits | | 429 | a technical or administrative usage limit was reached (calls at once, calls per minute, or an operator key's daily grant); retry after `Retry-After` | | 502 | upstream model error | | 503 | L1 model not configured | Errors are `{ "detail": "" }`. ## Examples ### curl ```bash curl -s https://altalandi.is/api/v1/refine \ -H "Authorization: Bearer $ALTALANDI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text":"Make sure to reach out to us today.","domain":"marketing"}' ``` ### Python ```python import os, requests r = requests.post( "https://altalandi.is/api/v1/refine", headers={"Authorization": f"Bearer {os.environ['ALTALANDI_API_KEY']}"}, json={"text": "Make sure to reach out to us today.", "domain": "marketing"}, timeout=300, ) r.raise_for_status() data = r.json() if data["meaningPreserved"] is not True: raise RuntimeError(f"meaning unverified: {data['meaningNotes']}") print(data["output"], "·", data["quality"]["nativenessScore"], "/100") ``` ### TypeScript (fetch) ```ts const res = await fetch("https://altalandi.is/api/v1/refine", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${process.env.ALTALANDI_API_KEY}` }, body: JSON.stringify({ text: "Make sure to reach out to us today.", domain: "marketing" }), }); const data = await res.json(); if (data.meaningPreserved !== true) throw new Error(`meaning unverified: ${data.meaningNotes}`); ``` ## Terminal clients (published npm packages) - Connect once: `npx altalandi login` — opens https://altalandi.is/tengja to sign in or sign up and approve a code; the CLI stores the key in the user's config directory and installs the Claude Code skill. No environment variable needed afterwards. `npx altalandi whoami` shows the account and credit (`GET /v1/me`). - CLI: `npx altalandi --domain marketing "Make sure to reach out to us today."` (agents: `npx altalandi --review --file in.txt --out out.txt`) - stdout = the refined text; stderr = nativeness score, meaning verdict, diff. - From an agent, use `--review`: stdout is one Markdown block to show the person as it is (verdict, before, after, each change with its reason, cost, balance), and `--out` gets the refined text. Exit 0 = rewritten and verified or no change needed, 3 = refused or unverified (keep the original), 4 = out of credit. `--json` prints the full response for code that parses it. - A whole folder, i18n catalog or website: `npx -y altalandi@latest ./docs --dry-run` prints the estimate; with `--yes` it writes ./docs-is (only verified changes; code, placeholders, tags and JSON keys byte for byte) and one report. - `npx altalandi update` checks for a newer version and asks before updating. - MCP server (Claude Code and any MCP client): ```json { "mcpServers": { "altalandi": { "command": "npx", "args": ["-y", "altalandi-mcp@latest"] } } } ``` Claude Code: `claude mcp add altalandi -s user -- npx -y altalandi-mcp@latest`; Codex: `codex mcp add altalandi -- npx -y altalandi-mcp@latest`. Exposes the `refine_icelandic` tool. The server uses `ALTALANDI_API_KEY` if its environment has it, otherwise the key stored by `npx altalandi login`. Never put a key in MCP JSON. - Agents on a person's computer: run `npx altalandi login` in the background, show the person the link and code it prints, wait for exit 0, then add the MCP server. Do not ask the person for a key. Full steps: https://altalandi.is/agents.md ## Using it from an AI agent The complete agent guide — result procedure, Python and TypeScript wrappers, tool definition, limits — is at https://altalandi.is/agents.md. Return the OUTCOME to the model rather than the raw JSON: a model given raw JSON tends to read `meaningPreserved: true` and report success on a refused rewrite. Anthropic (Claude) tool-use schema: ```json { "name": "altalandi_refine", "description": "Rewrite English or unnatural Icelandic into native-sounding Icelandic without changing its meaning. Returns an outcome: 'rewritten' (use the text), 'already_fine' (no change needed), 'refused' (the original is returned because a rewrite would have been wrong — tell the user why), 'unverified' (do not present as checked) or 'unusable'.", "input_schema": { "type": "object", "properties": { "text": { "type": "string" }, "domain": { "type": "string", "enum": ["general","marketing","data-analysis","childrens-fiction"] }, "register": { "type": "string", "enum": ["formal","neutral","warm"] } }, "required": ["text"] } } ``` Then POST to `https://altalandi.is/api/v1/refine` with the tool input as the body and the Bearer header. Or just hand the agent the OpenAPI at `https://altalandi.is/api/openapi.json`. ## Notes - Past the account's free daily units, each portal-key call is paid from the account's prepaid credit (krónur). A call costs its billable input units, defined above, × its `billedTier` weight (1× for the default standard/fast, up to 5× for max/rigorous) × the tariff. When the credit cannot cover a call it returns 402 `insufficient_balance` without charging anything. Credit is bought in the portal and can top up automatically from a saved card, within a monthly maximum. 429 means a technical or administrative usage limit was reached: more than 4 calls running at once or 60 per minute per account (2 and 10 until the account has paid; see GET /v1/pricing), or, for operator-issued keys without an account, the key's daily grant. `Retry-After` says when retrying makes sense. See usage in the dev portal. - Latency: a fully-gated call makes several model calls; independent L3 gates run concurrently when the candidate is fixed. For an unverified low-stakes path, set `preserveMeaning:false`, `scoreNativeness:false`, `onlyImprove:false`, and `scoring:"none"` (or use CLI `--fast`). - Batch: the hosted v1 contract is one document per request. Use bounded parallel `/v1/refine` calls for catalogs; `/refine/batch` is internal/self-hosted only. - Data: free-use text and its output are kept in Altalandi's text bank, with kennitölur, emails, phone, card and account numbers and API keys replaced by placeholders, for up to three years, and may be used to improve the service, including training and tuning models. Opt out per call (`"contribute": false`) or account-wide in the portal. Paid text is kept only with consent; ChatGPT text never. Request text is also sent to the configured model providers for processing, under each provider's terms. - Logging: for usage accounting, cost and abuse prevention, each call records metadata only — time, key id, account, client IP address, text length, models, tokens, estimated provider cost and outcome. Sign-ups, logins (including failed attempts), key creation and revocation are recorded with the IP address. Client IP addresses are deleted after 90 days. Passwords, session tokens and key secrets are never stored; text only as described under Data.