# Hreinskrift for AI agents

Everything an agent needs to use Hreinskrift correctly, on one page.

Hreinskrift turns English, or Icelandic that sounds translated or machine-written,
into Icelandic that reads as if a native speaker wrote it. It checks that the
meaning survived, and it refuses to return a rewrite it has found to be broken.

- Endpoint: `POST https://hreinskrift.is/api/v1/refine`
- Full reference: https://hreinskrift.is/llms-full.txt
- OpenAPI: https://hreinskrift.is/api/openapi.json

## When to call it, and when not to

Call it for any Icelandic a person will read: web and app copy, UI strings,
emails, product text, marketing, children's stories, and anything you
translated into Icelandic yourself.

Don't call it for code, identifiers, data that must stay byte-for-byte literal,
or quotations that must not be reworded. It may rephrase, so check the diff.

**The person's setting decides whether you ask first (Háttur).** Their account
says `agentMode` (`GET /api/v1/me`, or `npx hreinskrift mode`). Their own choice
is at https://hreinskrift.is/stjornbord/stillingar#gervigreind.

- `ask` („Spyrja fyrst“, the default): ask them once per task before you send
  anything to Hreinskrift. Say what you will send and roughly what it costs. Then:
  - the CLI needs `--approved` (without it, it exits 5 and sends nothing);
  - the MCP tools need `approved: true`.

  One yes covers the task (a folder, a site, a set of strings). Never set it
  without their yes.
- `auto` („Sjálfvirkt“): call it when the task calls for it.
- `always` („Alltaf“): also run the Icelandic you write for them through it,
  unasked.

## 1. Connect (once)

### On a person's computer (Claude Code, Codex, Cursor, a terminal): do this

**Do not ask the person for an API key, and do not tell them to set
`HREINSKRIFT_API_KEY`.** The login below gets a key onto this computer without it
ever appearing in the chat, a file you write, or the person's clipboard.

1. Run `npx hreinskrift login` **in the background** (it waits up to 10 minutes
   for the person). It never asks a question on the terminal.
2. Read its output and show the person **the link and the code** it prints. It
   also tries to open the link in their browser. They sign in or create an
   account (new accounts get 6,500 free characters a day, and 500 kr. of trial
   credit once they confirm their email), check that the code matches, and click **Tengja**.
3. Wait for the command to exit `0`. The key is now stored for this user
   (`%APPDATA%\hreinskrift\credentials.json` on Windows,
   `~/.config/hreinskrift/credentials.json` elsewhere), and, if Claude Code is set
   up, the `/hreinskrift` skill is installed in `~/.claude/skills/hreinskrift/`.
4. Add the MCP tool. It reads the stored login, so **no key goes in any config**:
   - Claude Code: `claude mcp add hreinskrift -s user -- npx -y hreinskrift-mcp@latest`
   - Codex: `codex mcp add hreinskrift -- npx -y hreinskrift-mcp@latest`
   - Cursor and other MCP clients: the JSON under "MCP" below.
5. Check it: `npx hreinskrift whoami` prints the account and balance.

For your own work, use the MCP tool (`refine_icelandic`) or the CLI with
`--review`. Both return one Markdown block made to be shown as it is: the
verdict, the text before and after, every change with its reason, what the call
cost and the balance left. Paste it to the person unchanged, then act on the
outcome. A new MCP server is usually available from the next session.

### On a server or in CI

Create an account at https://hreinskrift.is/minar-sidur, click **Búa til lykil**
("create key"), and put the key (`rtv_<id>_<secret>`, shown once) in the
environment as `HREINSKRIFT_API_KEY` from a secret manager. This is also how code
you write into a project (the HTTP examples below) gets its key.

Never put the key in a prompt, a chat message, a command-line argument or a
committed file. Command-line arguments leak through shell history and process
lists. Existing keys keep working; there is no rotation schedule.

## 2. Call it — three ways

### HTTP (works from anything)

```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"}'
```

### CLI (from an agent: `--review`)

```bash
npx -y hreinskrift@latest --review --file in.txt --out out.txt
```

Write the text to a UTF-8 file first; never pass it on the command line. stdout
is the block to show the person as it is (verdict, before, after, each change
with its reason, cost and balance); `out.txt` holds the refined text to apply.
Exit codes: `0` rewritten and verified, or no change needed; `3` the rewrite was
refused or the meaning could not be verified (keep the original); `4` out of
credit; `2` not logged in or no input; `1` request failed. `--json` prints the
full API response instead, for code that parses it.

A whole folder, i18n catalog or website in one run: `npx -y hreinskrift@latest
./docs --dry-run` prints the estimate; the same command with `--yes` writes the
results to `./docs-is` (only changes whose meaning check passed) and one report.
Code, placeholders, tags and JSON keys come back byte for byte.

Keep the CLI current with `npx hreinskrift update`. It asks before changing
anything; in a non-interactive run it only reports.

### Test keys

A key made with environment **test** (dashboard → API lyklar og skjölun, with the
switch on Prófun) never runs a model and is never charged. The response has the
same shape, `output` is the input unchanged, `test: true`, and `meaningPreserved`
is `null`. Use it to wire up and test an integration; switch to a live key for real
text.

### Showing the result

After every call, show the person what changed: the text before and after, and the
numbered `diffs` (before, after, reason). `hreinskrift --review` and the MCP tool
print that block for you, with the cost and the balance; `hreinskrift --diff` prints
it on stderr for a person at a terminal; `hreinskrift --report` (or `HREINSKRIFT_REPORT=1`) also writes a Markdown
report to `./hreinskrift-reports/`. The MCP tool takes `report: true` for the same.

### ChatGPT

In ChatGPT and Claude.ai, Hreinskrift is 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 for ChatGPT is coming soon. Do not tell people to
  turn on ChatGPT's Developer mode; point them to Claude.ai or hreinskrift.is for now.
- Claude.ai: Settings, Connectors, Add custom connector, the same URL, then
  Connect, sign in and approve.

Use is billed to the person's account like any other. Send one `tools/call` per
request: batches with more than one refine are refused. (The older per-account
secret link from https://hreinskrift.is/minar-sidur#chatgpt still works; treat it
as a secret.)

### MCP (Claude Code, Cursor, any MCP client)

```json
{ "mcpServers": { "hreinskrift": { "command": "npx", "args": ["-y", "hreinskrift-mcp@latest"] } } }
```

This exposes the `refine_icelandic` tool. The server finds its key by itself:
`HREINSKRIFT_API_KEY` if the environment has it, otherwise the key stored by
`npx hreinskrift login`. Never put a key in this JSON. From `hreinskrift-mcp` 0.1.2 the tool states the outcome on its
first line and reports a refused or unverified result as an error. Older
versions lead with "meaning preserved" even when a rewrite was refused, so read
their warnings.

## 3. The request

Only `text` is required.

| field | values | default | use it when |
|---|---|---|---|
| `text` | string, up to 20,000 characters | required | always |
| `domain` | `general` · `marketing` · `data-analysis` · `childrens-fiction` | `general` | you know what kind of text it is |
| `register` | `formal` · `neutral` · `warm` | `neutral` | tone matters |
| `sourceLang` | `auto` · `en` · `is` | `auto` | detection could guess wrong, e.g. very short text |
| `glossary` | `[{ "en": "...", "is": "...", "note": "..." }]` | `[]` | a term must always be translated one way (max 50) |

Leave everything else at its default. In particular, don't switch off
`preserveMeaning` or `scoreNativeness` for text a person will read: that removes
the safety checks described below.

## 4. Read the result — this is the part that matters

Don't look at one field. Work out which of four outcomes happened, **in this
order**:

| # | check | outcome | what to do |
|---|---|---|---|
| 1 | `quality.encodingArtifacts` is not empty | **unusable** | discard the output and report an error |
| 2 | `rejectionReason` is set, or `meaningPreserved` is `false` | **refused** | the original came back unchanged; tell the user it was *not* rewritten, and why |
| 3 | `rewriteAttempted` is `false` | **already fine** | nothing needed changing; that's a success |
| 4 | `meaningPreserved` is not `true` | **unverified** | the meaning could not be checked; don't present the output as verified |
| – | otherwise | **rewritten** | use `output`, and show the user the `diffs` |

Why `meaningPreserved` alone is not enough: when Hreinskrift refuses a broken
rewrite (an invented word, say), the meaning check has *already passed*. The
response then has `meaningPreserved: true` and returns your original text. Only
`rejectionReason` shows that the rewrite was thrown away.

`rejectionReason` starts with one of:

- `MEANING DRIFT` — the rewrite changed what the text says
- `HARD INVARIANT LOST` — a number, date, time, negation, name or obligation
  (`skal`, `óheimilt` …) did not survive
- `UNVERIFIED` — the meaning check could not complete, so nothing was risked
- `CANDIDATE VETO` — the rewrite itself was broken; `candidateVetoSignal` says
  how: `morphology` (a word that isn't Icelandic), `concrete_nativeness_tell`
  (a specific quoted error) or `encoding_artifacts`

**English input works a little differently.** Returning English to someone who
asked for Icelandic would be useless, so a translation whose meaning can't be
verified comes back with `meaningPreserved: null` and a warning, and counts as
unverified (row 4). A translation that clearly changed the meaning is retried
once, with the stronger writer for a paying account and the standard writer for
free use; if that also fails, the API returns `502` with no text.

### A correct wrapper

```python
import os, requests

def refine_icelandic(text: str, **options) -> dict:
    r = requests.post(
        "https://hreinskrift.is/api/v1/refine",
        headers={"Authorization": f"Bearer {os.environ['HREINSKRIFT_API_KEY']}"},
        json={"text": text, **options},
        timeout=300,
    )
    r.raise_for_status()
    data = r.json()

    if data["quality"].get("encodingArtifacts"):
        outcome = "unusable"
    elif data.get("rejectionReason") or data.get("meaningPreserved") is False:
        outcome = "refused"
    elif data.get("rewriteAttempted") is False:
        outcome = "already_fine"
    elif data.get("meaningPreserved") is not True:
        outcome = "unverified"
    else:
        outcome = "rewritten"

    return {
        "outcome": outcome,
        "text": data["output"],                 # the original when refused or already fine
        "reason": data.get("rejectionReason", ""),
        "changes": data.get("diffs", []),
        "warnings": data.get("warnings", []),
        "id": data["id"],                        # quote this when reporting a problem
    }
```

```ts
type Outcome = "unusable" | "refused" | "already_fine" | "unverified" | "rewritten";

export async function refineIcelandic(text: string, options: Record<string, unknown> = {}) {
  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, ...options }),
    signal: AbortSignal.timeout(300_000),
  });
  if (!res.ok) throw new Error(`Hreinskrift ${res.status}: ${(await res.json()).detail}`);
  const data = await res.json();

  const outcome: Outcome =
    data.quality?.encodingArtifacts?.length ? "unusable"
    : data.rejectionReason || data.meaningPreserved === false ? "refused"
    : data.rewriteAttempted === false ? "already_fine"
    : data.meaningPreserved !== true ? "unverified"
    : "rewritten";

  return { outcome, text: data.output, reason: data.rejectionReason ?? "",
           changes: data.diffs ?? [], warnings: data.warnings ?? [], id: data.id };
}
```

## 5. Expose it as a tool

Return the **outcome** to the model, not the raw JSON. A model given raw JSON
tends to read `meaningPreserved: true` and report success.

```json
{
  "name": "refine_icelandic",
  "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", "description": "The text to refine." },
      "domain": { "type": "string", "enum": ["general", "marketing", "data-analysis", "childrens-fiction"] },
      "register": { "type": "string", "enum": ["formal", "neutral", "warm"] }
    },
    "required": ["text"]
  }
}
```

## 6. Signals worth knowing

- **Treat the nativeness score as a hint.** `quality.nativenessScore` (0–100)
  and `translationeseTells` point at likely problems, but the score can be
  confidently wrong in both directions. A tell that quotes a specific phrase is
  worth far more than the number. Never use a low score to override a refusal.
- **`presentationDrift`** lists formatting-only changes that kept the value
  (for example a date written differently). They are safe, but a user may want
  to know.
- **`invariantChecks`** shows each number, date, name and obligation that was
  checked, and whether it survived.

## 7. Limits, errors and cost

| status | meaning | what to do |
|---|---|---|
| `400` | empty `text` | send text |
| `401` | missing or invalid key | check `HREINSKRIFT_API_KEY`; don't retry |
| `402` | `insufficient_balance`: account funding is insufficient for this call | stop and tell the person; waiting won't help. They add credit in the portal. If the message says an automatic top-up has started, try once more in a minute |
| `413` | text too long | split at paragraph boundaries, then call once per part |
| `422` | invalid request, or glossary over its limits | fix the request; don't retry |
| `429` | a usage limit was reached: calls at once or per minute per account (until an account has paid: 2 at once, 10 a minute; after: 4 and 60), or an operator key's daily grant | wait for the `Retry-After` header, then retry |
| `502` | a model provider failed, or an English translation could not be verified | retry once later |
| `503` | the service is not configured | retry later |

- **Cost:** past the free characters (below), calls are paid from the account's
  prepaid credit, held in krónur. Billing is per character (every character sent,
  spaces included): 1.00 kr per 100 characters at the standard tier
  (`GET /api/v1/pricing` has the current figures). Every call counts at least 250
  characters (65 for a proofread), however short the text, because the full set
  of checks runs either way. The `max` writer and `rigorous` scoring multiply the count (see
  `billedTier`).
- **UI strings: look them up first.** `POST /api/v1/hnappasafn` with
  `{"strings": ["Search", "Save changes", "Contact us"]}` (up to 200, 80 characters
  each) answers from the Hnappasafn: the Icelandic that Icelandic websites and
  open-source software (Firefox, Thunderbird, LibreOffice, Android) use, served only
  where the sources agree. Each result has `found`, `is`, `confidence`
  (`high`/`medium`) and `alternatives`. A string the Hnappasafn lacks is tried in the
  Orðakista: English names of countries, languages, currencies, units, months, weekdays
  and cities (`"Germany"` → `"Þýskaland"`, `"km"` with its case forms), and Icelandic
  place names (`"Akureyri"` → `preposition: "á"`, `phrases.in: "á Akureyri"`). Those
  results have `source: "ordakista"`, a `kind`, `forms` (`nf`/`thf`/`thgf`/`ef`,
  singular and plural where known), `attribution`, and for a place `preposition`,
  `phrases` and `uncertain`. No model runs, so the answer is instant and
  consistent across a project. 1 kr per string found, from the free characters first
  (100 a hit); a string not found costs nothing, nor does a place marked
  `uncertain: true`. Send the misses to the batch call below. In the hosted MCP (`https://hreinskrift.is/api/mcp`) and in `hreinskrift-mcp` it is the tool
  `lookup_icelandic_ui_strings`; the CLI looks folder and catalog strings up first by itself.
  An account can look up 2,000 strings a day and 10,000 in 30 days (`429 hnappasafn_limit`:
  translate the rest with the batch call).
  With `"sourceLang": "is"` the strings are Icelandic labels to check: each result has
  `status` `established` (the form Icelandic software uses: keep it), `minority` (use
  `best`: „Hvernig það virkar“ → „Svona virkar þetta“) or `unknown` (not charged), with
  `evidence`.
- **Search the Orðakista:** `POST /api/v1/ordakista/search` with `{"q": "save", "limit": 10}`
  (English or Icelandic, 1–80 characters; optional `drawers`) finds UI text, names, places
  in any case form, computing terms and stiff phrasings. Exact matches come first, then
  prefixes and one-letter typos. Free: 30 a minute, 200 a day per account. MCP tool:
  `ordakista_search`.
- **Never join separate UI strings into one text.** Buttons, menu items,
  headings and labels glued into one text with prose are read as prose and
  rewritten as sentences. Send each string as its own item (below), or run the
  CLI on the catalog folder, which classifies every string and looks buttons and
  headings up first. This holds for Icelandic catalogs too.
- **Headings and FAQ questions name the thing:** „Svona virkar Hreinskrift“, „Hvað
  kostar þýðingin?“. Never „Svona virkar það“ or „Hvað kostar það?“: on a website
  „það“ points at nothing; it belongs in conversation. A plural name takes a plural
  verb („Svona virka Opin gögn“).
- **Many short strings** (UI strings, an i18n catalog, labels): never one call
  per string. Send them together to `POST /api/v1/refine/batch` with
  `{"items": [{"id": "save", "text": "Save changes"}, …], "sourceLang": "en"}`
  (up to 500 items, 2,000 characters each, 20,000 in total). Each string still
  gets the full checks and comes back under its `id` in `items[]`; each bills
  at least 130 characters (`batchItemMinimumChars`; 800 for an English string,
  `batchItemMinimumCharsEn`) and the 250-character minimum applies once per call,
  so short Icelandic strings cost about half. A string that needed no change bills
  `keptBilledChars` (65). Pass `"sourceLang": "is"` for Icelandic strings with no
  Icelandic letter (such as "Vista"), or they are detected as English. Keep a request to about 40
  strings so it answers within a minute or two. The CLI does this for you:
  put the strings in a `{"key": "text"}` JSON file in a folder of its own and run
  `npx -y hreinskrift@latest <folder>` (add `--dry-run` first for the estimate).
- **Free tier:** every account gets 6,500 free characters a day on the standard
  writer (about 1,000 words, roughly two A4 pages), shared by its keys, the
  signed-in web tool and its ChatGPT and Claude connections. The `max` writer is always paid from credit. Until the account
  has paid, it is limited to 10 calls a minute and 2 at once. `GET /v1/pricing`
  returns the live numbers.
- **Codes:** a person may redeem a code in the portal (Kóðar og stillingar). A
  code can make use free within its own limits, discount everything the credit
  pays, or add credit once. Nothing changes in how you call the API.
- **Credit:** past the free characters, calls are paid from credit. A new account
  also gets 500 kr of trial credit once, when its email is confirmed. More credit is bought in the
  portal (1,000 to 100,000 kr; larger top-ups earn +5% to +15% extra credit)
  and can top itself up from a saved card when it runs low, up to a monthly
  maximum the account sets. See usage and credit in the portal.
- **Free use is kept to improve the service.** A call covered by the free daily
  characters is kept with its output in Hreinskrift's text bank, with personal data
  (kennitölur, emails, phone, card and account numbers, API keys) replaced by
  placeholders, unless it sends `"contribute": false` or the account has opted
  out in the portal. Paid calls (from credit) are kept only with
  `"contribute": true` or account-wide consent. Send `contribute: false` when
  the person asks that their text not be kept, or when it is confidential; never
  send `contribute: true` without their explicit consent. Text sent through the
  hosted ChatGPT and Claude connectors is never kept in the bank.
- **Many texts:** short strings go together to `POST /api/v1/refine/batch`
  (above). Longer texts go one per call to `/v1/refine`, at most 4 in parallel.
  To group them as one job in the person's history (a folder, a site, a long
  text in parts), send the same `"job": {"id": "j_…", "title": "…", "part": "…"}`
  on each call; `POST /api/v1/jobs` with a `sample` first (free) classifies the
  kind of text and its specialist fields for the whole job, and the result's
  `jobUrl` opens it. Stop at the first `402`; on `429`, wait for `Retry-After`.
- **Speed:** a fully checked call makes several model calls and can take several
  seconds; allow up to 300 seconds. Don't disable the checks to go faster for
  text people will read.

## 8. What Hreinskrift records

**Text in the text bank** (used to improve Hreinskrift): free use is kept,
scrubbed of personal data, for up to three years (see "Free use is kept"
above); paid use only with consent; text through the hosted ChatGPT and Claude
connectors never.

**Run history** (only for the account): every call from an account keeps the
text sent and the result until the person deletes it, on every surface including ChatGPT and
Claude, so the run page (`runUrl` in the result, `https://hreinskrift.is/saga/<id>`)
can show it with the checks and the person's approval or edits. Only the
account and its team see it; it is never used to train or improve Hreinskrift.
`"contribute": false` (CLI `--no-store`) keeps that call's text nowhere; the
account can switch history off in Settings or delete a run's text on its page.
Anonymous homepage tries have no run page.

**Never stored:** passwords, session tokens and key secrets.

**Recorded, per call, so usage and cost can be accounted for and abuse
spotted:** time, key id, account, client IP address, text length, the models
used, tokens, estimated provider cost, and the outcome. Client IP addresses are
deleted after 90 days; the rest of the record is kept for cost history.

Sign-ups, logins (including failed attempts), key creation and revocation are
also recorded, with the IP address.

Your text is sent to the model providers that do the rewriting, under their
terms. Banked text may be used to review and correct translations, test new
versions, and train or tune the models Hreinskrift uses. Full policy:
https://hreinskrift.is/personuvernd#textabanki

## 9. When something looks wrong

Every response has an `id` like `r_1a2b3c`. Quote it when reporting a problem;
it lets the operator find the record without you sending the text again.

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.
