API reference

One POST per method, a bearer key, JSON in and out.

Endpoint

Each method is its own path. POST a JSON body with your bearer key and an API-Version header. Every call answers with a req_id, inline by default, or delivered to a webhook if you pass one.

POST https://api.linguin.xyz/lookup
Authorization: Bearer YOUR_API_KEY
API-Version: 2026-06-01
Content-Type: application/json

{ "text": "bonjour", "lang": "fr", "locale": "en", "mode": "fast" }

Authentication

Pass your key as a bearer token. Keys are per account: a sandbox key for evaluation, a live key on a paid plan. Never ship a key in client-side code.

Authorization: Bearer YOUR_API_KEY
API-Version: 2026-06-01

Dictionary

Two ways into the dictionary. lookup takes a term and returns its entry, fast (just the definition) or deep (the complete entry). reverse takes a meaning and returns the words that fit, as bare terms or expanded to the same fast or deep detail. Both deliver async with a webhook.

lookup

The dictionary entry for a term. fast returns just the definition, deep returns the complete entry.

POST /lookup

{
  "text":    "bonjour",    // word or phrase to look up
  "lang":    "fr",         // language of the word, or "auto"
  "locale":  "en",         // localization to return the entry in, defaults to en
  "mode":    "fast",       // "fast" just the definition $0.003, or "deep" the complete entry $0.005
  "limit":   1,            // max entries to return, top first
  "webhook": "https://…"   // optional, async
}
{
  "req_id": "9f1c7b3e-2a40-4d8e-b5c1-6e0a2d9f4c87",
  "items": [
    {
      "type": "word",
      "term": "bonjour",
      "lang": "fr",
      "locale": "en",
      "definition": "The standard French daytime greeting, equivalent to 'hello' or 'good day'."
    }
  ],
  "billed": { "cost": 0.003 }
}

deep returns the same fields plus pron, pos, forms, meanings, etymology and categories. Word-language fields stay in lang, only the prose follows locale.

{
  "req_id": "9f1c7b3e-2a40-4d8e-b5c1-6e0a2d9f4c87",
  "items": [
    {
      "type": "word",
      "term": "bonjour",
      "lang": "fr",
      "locale": "en",
      "pron": "/bɔ̃.ʒuʁ/",
      "pos": ["interjection", "noun"],
      "forms": [],
      "meanings": [
        {
          "pos": "interjection",
          "domain": "everyday",
          "register": "neutral",
          "name": "Daytime greeting",
          "definition": "The standard polite way to say hello in French, used from morning until early evening.",
          "examples": [
            { "phrase": "**Bonjour**, comment allez-vous ?", "translation": "**Hello**, how are you?" }
          ],
          "synonyms": ["salut", "coucou", "bonsoir"],
          "antonyms": ["au revoir", "adieu"],
          "related_terms": ["bonsoir", "bonne journée", "salut"],
          "translations": ["hello", "good day", "hi"]
        }
      ],
      "definition": "The standard French daytime greeting, equivalent to 'hello' or 'good day'.",
      "etymology": "From Old French *bon jour*, 'good day', a conventional greeting since the Middle Ages.",
      "categories": ["everyday life", "social interaction", "communication"]
    }
  ],
  "billed": { "cost": 0.005 }
}

Lookup always returns an items array, one entry per hit, so the shape never changes with limit. Omit lang to autodetect the word's language: a spelling can live in several languages (English and Spanish both have molar), so when it is ambiguous limit returns the top entries across them, each billed as its own lookup. limit: 1 (default) returns one.

reverse

A reverse dictionary: meaning in, word out. Describe a sense and get the words that fit. mode sets how much each match carries, limit how many. It returns one top match by default.

POST /reverse

{
  "query": "a doctor for animals",  // a meaning or description
  "lang":   "en",          // language of the words, defaults to locale
  "locale": "en",          // localization to return definitions in, defaults to en
  "mode":   "fast",        // "terms" the word, "fast" with a definition, "deep" the whole entry
  "limit":  2              // max matches to return, top first
}
{
  "req_id": "2c9d41a7-6b0e-4f13-9a28-7d5e0c3b1f64",
  "query": "a doctor for animals",
  "locale": "en",
  "matches": [
    { "type": "word", "term": "veterinarian", "lang": "en", "locale": "en", "definition": "A doctor who treats animals." },
    { "type": "word", "term": "vet", "lang": "en", "locale": "en", "definition": "An informal short form of veterinarian, a doctor who treats animals." }
  ],
  "billed": { "matches": 2, "cost": 0.008 }
}

terms is $0.002 a call. fast and deep bill per match, $0.004 and $0.006, so limit caps their cost. terms returns just the word, deep the complete entry. To open one match as its own entry, pass its term to lookup.

Translate

One endpoint, the response shape the same across every combination. Three independent toggles, no tiers: live resolves the source before translating, proofread checks the finished wording against real usage, humanizer rewrites it to read human. webhook delivers async.

POST /translate

{
  "text":      "Hola, ¿cómo estás?",  // text to translate
  "from":      "es",       // source language, or "auto"
  "to":        "en",       // language to translate into
  "live":      false,      // true: look the source's expressions up before translating
  "proofread": false,      // true: check the finished wording against real usage
  "humanizer": false,      // true: rewrite the output to read human
  "fallback":  false,      // true: if the model can't serve right now, another completes the call
  "webhook":   "https://…" // optional, async
}
{
  "req_id": "c4e21a9f-3b8d-4f50-9a17-1e6d0b2c7f48",
  "type": "translation",
  "from": "es",
  "to": "en",
  "text": "Hola, ¿cómo estás?",
  "translation": "Hello, how are you?",
  "words": 3,
  "passes": ["translate"],
  "billed": { "words": 30, "rate": 0.05, "cost": 0.0015 }
}
  • The base call is a single translating pass, resolving idiom, register and tone in one go.
  • live grounds the way in. One stage reads the source and returns the expressions whose sense a translator would check rather than assume, the idioms, the slang, the regional usage, anything whose current sense has moved. Not set phrases or officialese: those are stiff rather than opaque, and they are proofread's job. Each is searched, a second stage states what it means against the results it cites, and those senses go to the translating pass alongside the text. A sense that cites nothing is discarded before it gets there. A translation can be fluent, grammatical and idiomatic and still be built on a misread expression, and nothing after that mistake can see it, so the looking up happens before the wording is chosen.
  • proofread grounds the way out. A stage picks out the wording worth questioning, the openers, the set phrases, the collocations, anything whose shape followed the source language, and puts each against the alternative it beat. Both go to a live search as bare phrases, and what comes back is how often each actually occurs in real writing. A third stage applies only what the counts overruled. A translation can be perfect by every rule of grammar and still be phrasing nobody reaches for, and only counting separates the two. It earns its keep most on formal and professional prose, and on distant language pairs; on casual writing prefer live, since real-usage counts skew formal and can cost you register.
  • humanizer rewrites the output so it reads like a person wrote it. Bills the words once more.
  • fallback trades exactness for availability: off (default), the call errors if the model is unavailable; on, another model completes it.
  • passes is what the request actually did, in order, and what it was charged for. A live pass that found nothing uncertain in the source, or a proofread that found nothing worth checking, is not in the list and is not billed.
  • mode was the old fast/deep tier. Still accepted, now does nothing. Deep's usage check is proofread, a toggle of its own now.

localize

Bulk translation of a string array into one language, built for UI and app localization. The array comes back translated and aligned one to one, same length, same order, so it maps straight back onto your keys. However big the array (up to 1000 strings per call), it is one logical job: chunking and parallelism happen inside. Multiple target languages are your loop, one call each.

POST /localize

{
  "strings":  ["Save", "Cancel", "Your changes were saved."],
  "from":     "en",        // source language, or "auto"
  "to":       "de",        // language to localize into
  "fallback": false,       // true: if the model can't serve right now, another completes the call
  "webhook":  "https://…"  // optional, async
}
{
  "req_id": "e8b31c5a-9d02-4f76-8a41-3c7e0d2b9f15",
  "type": "localization",
  "from": "en",
  "to": "de",
  "strings": ["Speichern", "Abbrechen", "Deine Änderungen wurden gespeichert."],
  "words": 7,
  "passes": ["translate"],
  "billed": { "words": 30, "rate": 0.05, "cost": 0.0015 }
}
  • Billing is translate's: per source word summed over the whole array, floored once per call, at the base rate. A thousand two-word labels bill as one job, not a thousand floors.
  • No toggles here. A UI label has no source idiom for live to resolve, no calqued collocation for proofread to catch and no machine cadence for humanizer to loosen, so localize is the base pass and nothing else.
  • Keyboard-plain output: curly quotes and invisible space variants the model may drift into are normalized, unless your source string itself carries them.

get

Fetch a result by its req_id, free. It returns exactly what the request produced, with its created and delivered times and the billed it charged, or pending if it is not ready yet. Only your own requests are readable.

POST /get

{ "req_id": "9f1c7b3e-2a40-4d8e-b5c1-6e0a2d9f4c87" }
{
  "req_id": "9f1c7b3e-2a40-4d8e-b5c1-6e0a2d9f4c87",
  "created_at": "2026-06-01T09:24:01Z",
  "delivered_at": "2026-06-01T09:24:03Z",
  "items": [
    {
      "type": "word",
      "term": "bonjour",
      "lang": "fr",
      "locale": "en",
      "definition": "The standard French daytime greeting, equivalent to 'hello' or 'good day'."
    }
  ],
  "billed": { "cost": 0.003 }
}
{ "req_id": "9f1c7b3e-2a40-4d8e-b5c1-6e0a2d9f4c87", "status": "pending" }

balance

Your current prepaid balance in USD, free. Every call deducts its billed.cost the moment the result is delivered, so this reading is live to the last request.

POST /balance

{}
{ "balance": 24.9955 }

topup

Puts money in, free to call. Pass the amount in USD, $25 minimum, and you get a Stripe payment link back. Open it, pay, and the balance is credited within moments of the payment landing.

POST /topup

{ "amount": 50 }
{ "url": "https://checkout.stripe.com/c/pay/cs_a1B2…", "amount": 50 }

refund

Sends your unused balance back to the cards that paid it, free to call. The body is empty: everything refundable goes back, split across your payments newest-first. Refundable is what you paid in and have not spent: promotional credit is spendable but never withdrawable, so a refund leaves any credit you still hold in the wallet, ready to use. The cards see the money within a few business days.

POST /refund

{}
{ "refunded": 24.99, "balance": 0.002 }

report

Your usage, free: how many calls and what they cost, per method and variant, with a total. A translate variant is the toggles the call asked for, a dictionary one is its mode. Both dates are optional ISO 8601, since inclusive, until exclusive. Failed calls appear in the total's failed count and cost nothing.

POST /report

{ "since": "2026-07-01", "until": "2026-08-01" }
{
  "methods": {
    "translate/base":      { "calls": 128, "cost": 0.352 },
    "translate/live":      { "calls": 12,  "cost": 0.054 },
    "lookup/deep":         { "calls": 16,  "cost": 0.08 }
  },
  "total": { "calls": 156, "failed": 2, "cost": 0.486 }
}

Keys

Manage your own bearer keys, free. key mints an additional key for your account, in the environment of the key you call with: a dev key mints dev keys, a live key live ones. The plaintext is in the response and nowhere else, store it before anything.

POST /key

{}
{ "key": "live_9f2c8a1d…", "prefix": "live_9f2c8a1", "env": "live" }

revoke kills one of your keys, named by its full value or its prefix. Your last active key is refused, so rotation is: mint the new key, move your traffic, then revoke the old one.

POST /revoke

{ "prefix": "live_9f2c8a1" }
{ "revoked": ["live_9f2c8a1"] }

Sync or async

Every generating method takes an optional webhook. You always know which response you get from whether you passed one.

  • Absent the call blocks until the result is ready and returns it inline.
  • Present the call returns at once with a req_id, then POSTs the result to your webhook when ready.

On submit you get the ticket:

{ "req_id": "9f1c7b3e-2a40-4d8e-b5c1-6e0a2d9f4c87", "status": "queued" }

The webhook receives the finished result, the same body a sync call returns, carrying its req_id. If the callback is missed, get(req_id) is always there to pull it, so a dropped delivery is recoverable.

Errors

Errors return a non-2xx status with a stable error code you can switch on, and a message that says what went wrong.

{ "ok": false, "error": "invalid_params", "message": "Field 'text' is required." }
CodeMeaning
unauthorizedMissing or invalid API key.
invalid_methodUnknown method, or one your plan cannot call.
invalid_versionUnknown API-Version header. Omit it or send a version this API has.
invalid_paramsA required field is missing or malformed.
rate_limitedPlan rate limit exceeded. Back off and retry.
insufficient_balancePrepaid balance exhausted. Top up to continue.
last_keyRefusing to revoke your last active key. Mint a replacement first.
not_foundNo entry for that term, or no request for that req_id.
translation_failedThe engine could not produce a translation. Nothing was charged, retry.
nothing_to_refundNo refundable balance to send back.
stripe_errorThe payment could not be opened or refunded. Retry.

Billing

Translate bills one unit only, the source word: no base fees, no per-call charges. A request bills at least 30 words. Each pass in the response's passes bills those words once at its own rate, so the charge is the base plus whatever you switched on, and the response's rate is the total per 1K words. You are only charged for a pass that ran: a live pass that found nothing to look up, or a proofread that found nothing worth checking, bills as though the toggle had been off. Localize is translate's billing over an array: the words summed across every string, floored once per call. Words are whitespace-delimited, so punctuation and formatting are never charged; languages written without spaces count each character as a word. Dictionary methods bill per call or per match. get is free.

Every priced response carries a billed object stating the exact charge, so the math is on the response itself. Billing is prepaid: each call deducts its billed.cost from your balance as it is delivered, and calls are refused with insufficient_balance once it hits zero. Check it anytime with balance, refill with topup, and send unused balance back to your card with refund, all free. The full price table is on Dictionary API Access.

Look up word or phrase...