One POST per method, a bearer key, JSON in and out.
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" }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-01Two 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.
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.
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.
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 }
}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.live, since real-usage counts skew formal and can cost you register.proofread, a toggle of its own now.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 }
}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.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" }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 }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 }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 }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 }
}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"] }Every generating method takes an optional webhook. You always know which response you get from whether you passed one.
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 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." }| Code | Meaning |
|---|---|
unauthorized | Missing or invalid API key. |
invalid_method | Unknown method, or one your plan cannot call. |
invalid_version | Unknown API-Version header. Omit it or send a version this API has. |
invalid_params | A required field is missing or malformed. |
rate_limited | Plan rate limit exceeded. Back off and retry. |
insufficient_balance | Prepaid balance exhausted. Top up to continue. |
last_key | Refusing to revoke your last active key. Mint a replacement first. |
not_found | No entry for that term, or no request for that req_id. |
translation_failed | The engine could not produce a translation. Nothing was charged, retry. |
nothing_to_refund | No refundable balance to send back. |
stripe_error | The payment could not be opened or refunded. Retry. |
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.