# Hreinskrift — 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://hreinskrift.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://hreinskrift.is/api # v1 endpoints under /v1/... Authorization: Bearer # required on /v1/* ``` Mint a key in the dev portal at https://hreinskrift.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 6,500 free characters a day on the standard writer (about 1,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 characters (or unlimited use) for partners. Free use (calls covered by the free characters) 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; text through the hosted ChatGPT and Claude connectors is never kept in the bank. Separately, every call from an account keeps its text and result, until the person deletes it, in the account's own run history (all surfaces, ChatGPT included; never used to improve Hreinskrift): the result's `runUrl` (https://hreinskrift.is/saga/) opens it with the checks and an approve/edit form. `"contribute": false` keeps a call's text nowhere; history can be switched off in Settings. A key with environment "test" never runs a model and is never charged: same response shape, output = input, `test: true`, `meaningPreserved: null`. CLI: `hreinskrift --diff` prints the before/after and every change; `--report` (or HREINSKRIFT_REPORT=1) writes a Markdown report to ./hreinskrift-reports/. Store the one-time plaintext in a password manager. Load `HREINSKRIFT_API_KEY` via a masked prompt or secret manager; do not paste it into shell commands or committed config. Bash: ```bash read -rsp "Hreinskrift API key: " HREINSKRIFT_API_KEY; echo export HREINSKRIFT_API_KEY ``` PowerShell 7: ```powershell $env:HREINSKRIFT_API_KEY = Read-Host "Hreinskrift 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 billable characters. | | 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, Hreinskrift 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 is billed per character: billable characters × weight × tariff, paid from the account's prepaid credit. The standard tariff is 1.00 kr per 100 characters (`billingUnit: "character"` and `iskPer100Chars` per tier in GET /v1/pricing have the live values), applied when the call runs: a later price change never alters credit already held. Icelandic rewriting counts a MINIMUM of 250 characters before the weight (`minimumBillableChars`); translation from English counts at least 800 (`minimumBillableCharsEn`), because a refine runs the same guarded pipeline whether the text is one word or fifty. A proofread (`task: "proofread"`) counts a minimum of 200 (`minimumBillableCharsProofread`). The most a call can cost is held before it runs and settled to what it actually cost; a failed call costs nothing. Responses carry `characters` and `billing.characters` (what was billed) and `billing.freeCharsLeftToday`; `units` / `billing.units` stay for one release as a word equivalent (characters ÷ `charsPerWord`, 6.5, rounded up) and are deprecated, as are the word-unit fields of GET /v1/pricing. 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://hreinskrift.is when the person is signed in: texts up to 20,000 characters and a choice of the standard or max writer. The count is every character (Unicode code point) of `text` after trimming, spaces, punctuation and newlines included, plus every glossary field once. A translation counts the characters sent, not the Icelandic returned. The weights: | writer \ scoring | none | fast | rigorous | |------------------|------|------|----------| | standard | 1× | 1× | 3× | | max | 2.5× | 2.5× | 5× | A `max` + `rigorous` call therefore bills 5× its billable characters; 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 for a paying account, the standard writer for free use), 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://hreinskrift.is/api/v1/refine \ -H "Authorization: Bearer $HREINSKRIFT_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://hreinskrift.is/api/v1/refine", headers={"Authorization": f"Bearer {os.environ['HREINSKRIFT_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://hreinskrift.is/api/v1/refine", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${process.env.HREINSKRIFT_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 hreinskrift login` — opens https://hreinskrift.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 hreinskrift whoami` shows the account and credit (`GET /v1/me`). - CLI: `npx hreinskrift --domain marketing "Make sure to reach out to us today."` (agents: `npx hreinskrift --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 hreinskrift@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 hreinskrift update` checks for a newer version and asks before updating. - MCP server (Claude Code and any MCP client): ```json { "mcpServers": { "hreinskrift": { "command": "npx", "args": ["-y", "hreinskrift-mcp@latest"] } } } ``` Claude Code: `claude mcp add hreinskrift -s user -- npx -y hreinskrift-mcp@latest`; Codex: `codex mcp add hreinskrift -- npx -y hreinskrift-mcp@latest`. Exposes the `refine_icelandic` tool. The server uses `HREINSKRIFT_API_KEY` if its environment has it, otherwise the key stored by `npx hreinskrift login`. Never put a key in MCP JSON. - ChatGPT and Claude.ai: a remote MCP connector with sign-in (OAuth) at `https://hreinskrift.is/api/mcp`; no secret link and no key to copy. ChatGPT: an Hreinskrift plugin is coming soon; do not suggest Developer mode. Claude.ai: Settings, Connectors, Add custom connector, the URL, Connect, sign in and approve. Use is billed to the account like any other. - Agents on a person's computer: run `npx hreinskrift 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://hreinskrift.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://hreinskrift.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": "hreinskrift_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://hreinskrift.is/api/v1/refine` with the tool input as the body and the Bearer header. Or just hand the agent the OpenAPI at `https://hreinskrift.is/api/openapi.json`. ## Notes - Past the account's free daily characters, each portal-key call is paid from the account's prepaid credit (krónur). A call costs its billable characters, 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`). - Háttur: the account's `agentMode` (`GET /v1/me`; `POST /v1/me/agent-mode` `{"mode": "ask"|"auto"|"always"}`; the person chooses at https://hreinskrift.is/stjornbord/stillingar#gervigreind). `ask` (default): ask once per task before sending; the CLI needs `--approved` (else exit 5, nothing sent), the MCP tools `approved: true`. `auto`: as needed. `always`: also refine Icelandic you write, unasked. `HREINSKRIFT_MODE` overrides it on one computer. - Never join separate UI strings (buttons, menu items, headings, labels) into one text: they are rewritten as prose. Send each as a batch item, or run the CLI on the catalog folder (it classifies every string and looks buttons up first). - Icelandic labels: `POST /v1/hnappasafn` with `"sourceLang": "is"` checks each one; `status` is `established` (keep), `minority` (use `best`) or `unknown` (not charged). `POST /v1/ordakista/search` `{"q", "limit"?, "drawers"?}` searches the whole Orðakista in English or Icelandic (free, 30/min, 200/day per account; MCP `ordakista_search`). - Batch: many short strings (UI strings, i18n catalogs) go together to `POST /v1/refine/batch` with `{"items": [{"id", "text"}], "sourceLang", ...}` (up to 500 items, 2,000 characters each, 20,000 in total; no `task: "proofread"`). Each string runs the full single-text pipeline and returns under its id in `items[]` (an item with `error` came back unchanged and was not billed). Billing: each item at least 130 characters (`batchItemMinimumChars` in GET /v1/pricing; 800 for an English item, `batchItemMinimumCharsEn`: the request's sourceLang, else each item's detected language), the 250-character minimum once per request. An item that needed no change bills `keptBilledChars` (65). Keep a request to about 40 strings. The CLI batches folder, site and JSON-catalog runs. (`/refine/batch` without /v1 is internal/self-hosted only.) - Hnappasafn (UI-string library): `POST /v1/hnappasafn` with `{"strings": [...], "minConfidence": "medium"|"high"}` (up to 200 strings, 80 characters each). Returns `results[]` with `en`, `found`, and for a hit `is`, `confidence`, `alternatives` (forms at least two sources use) and `evidence` (sources and licences), plus `hits`, `billedHits`, `misses`, `chargedIsk` and `attribution`. A string the Hnappasafn lacks is tried in the Orðakista (Icelandic naming bank): English names of countries, languages, currencies, units, months, weekdays, cities, regions and EU bodies, and Icelandic place names by their Icelandic name; such a result has `source: "ordakista"`, `kind`, `forms` ({"sg"|"pl": {nf, thf, thgf, ef}} where known), `attribution`, and for a place `preposition` (á/í), `phrases` (in/to/from) and `uncertain`. No model runs. Billing: 1 kr per string found (`hnappasafn.iskPerHit` in GET /v1/pricing), the account's free characters first at 100 a hit; a string not found costs nothing, nor does a place marked `uncertain`; a test key looks nothing up. At most 2,000 hits a day and 10,000 in 30 days per account (429 `hnappasafn_limit`; nothing charged). MCP tool (hosted and hreinskrift-mcp): `lookup_icelandic_ui_strings`. Sources: Icelandic websites with IS and EN versions, and the Icelandic localisations of Firefox, Thunderbird, LibreOffice (MPL-2.0) and Android (Apache-2.0); for the Orðakista, BÍN (case forms, CC BY-SA 4.0), Unicode CLDR, Wikidata (CC0) and Hagstofa Íslands (CC BY 4.0). - Data: free-use text and its output are kept in Hreinskrift'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; text through the hosted ChatGPT and Claude connectors never. Separately, every call from an account keeps its text and result in the account's own run history (`runUrl`, https://hreinskrift.is/saga/) until the person deletes them, on every surface; only the account and its team see it, and it is never used to train or improve Hreinskrift. History can be switched off in Settings, and `"contribute": false` keeps a call's text nowhere. 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. Pricing update (2026-10-17): a single English translation bills at least 800 characters (`minimumBillableCharsEn`), each English batch item at least 800, and proofreading at least 200. Icelandic rewriting keeps its 250-character minimum; a kept-text check stays at 65. These are before tier weights; GET /api/v1/pricing is authoritative.