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. mode is the quality tier, live and humanizer are add-ons, 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 "mode": "fast", // "fast" quick pass, or "deep" native-quality "live": false, // true: cross-check the wording against live web usage "humanizer": false, // true: rewrite the output to read human "fallback": false, // true: if the tier can't serve right now, the other completes the call "webhook": "https://…" // optional, async
}
{ "req_id": "c4e21a9f-3b8d-4f50-9a17-1e6d0b2c7f48", "type": "translation", "mode": "fast", "from": "es", "to": "en", "text": "Hola, ¿cómo estás?", "translation": "Hello, how are you?", "words": 3, "billed": { "words": 30, "passes": 1, "cost": 0.0015 }
}
  • deep weighs context, idiom, register and tone, where fast may go word for word. Same shape, better translation.
  • Live cross-checks the wording against how people write on the web right now, so idioms and current terms match real usage and the result reads native, not literal. It only bills when it actually runs a check, so a call where the model is already confident carries no live: true marker and no Live charge.
  • humanizer rewrites the output so it reads like a person wrote it.
  • fallback trades tier for availability: off (default), the tier you asked for serves you or the call errors; on, a busy tier hands the request to the other one, the response's mode says which ran, and you are billed that tier, never above what you asked.

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 "mode": "fast", // "fast" quick pass, or "deep" native-quality "fallback": false, // true: if the tier can't serve right now, the other completes the call "webhook": "https://…" // optional, async
}
{ "req_id": "e8b31c5a-9d02-4f76-8a41-3c7e0d2b9f15", "type": "localization", "mode": "fast", "from": "en", "to": "de", "strings": ["Speichern", "Abbrechen", "Deine Änderungen wurden gespeichert."], "words": 7, "billed": { "words": 30, "passes": 1, "cost": 0.0015 }
}
  • Billing is translate's: per source word summed over the whole array, floored once per call, at your tier's rate. A thousand two-word labels bill as one job, not a thousand floors.
  • mode and fallback follow translate's rule: the response's mode is the tier that served and the tier billed, never above what you asked.
  • 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 mode, with a total. 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/fast": { "calls": 128, "cost": 0.352 }, "lookup/deep": { "calls": 16, "cost": 0.08 } }, "total": { "calls": 144, "failed": 2, "cost": 0.432 }
}

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, and the add-ons live (when it actually ran a check) and humanizer each bill the words once more, at the rate of the tier that actually served the request (the response's mode). 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...