Terrior API
Rewrite any text toward how a specific region writes its language, and get told what could not safely be changed.
Base URL: https://terrior.app. Requests and responses are JSON (UTF-8). Version: v1.
Getting started
Subscribe at https://terrior.app/signup. Checkout returns you to a page that shows your API key once; we store only a hash of it. Then:
curl https://terrior.app/v1/convert \
-H 'Authorization: Bearer tk_live_…' \
-H 'Content-Type: application/json' \
-d '{"region":"eng-in-05-chandigarh","text":"The forthcoming update comprises fixes for everyone impacted by last week'\''s outage, and our colleagues will subsequently bolster support for anyone attempting to move their account."}'
On Starter and Growth, choose your regions first (see Choosing regions); Scale includes every region. Regions available now: cat-es-56-barcelona, cat-es-56-manresa, cat-es-56-sabadell, deu-ch-bs-basel, deu-de-04-hamburg, deu-de-05-bebra, deu-de-07-bergisch-gladbach, deu-de-07-leverkusen, deu-de-13-chemnitz, deu-de-13-dresden, eng-gb-sct, eng-in-05-chandigarh, eng-in-07-delhi, eng-in-09-ahmedabad, eng-in-13, eng-in-16-mumbai, eng-in-16-pune, eng-in-19, eng-in-19-bengaluru, eng-in-21, eng-in-25, eng-in-25-chennai, eng-in-28-kolkata, eng-in-40, eng-ke-05-nairobi, eng-ng-05-lagos, eng-us-ga-atlanta, eng-us-ks-smith-center, eng-us-mo-saint-joseph, eng-us-nc-salisbury, eng-us-pa-bloomsburg, hin-in-05-chandigarh, hin-in-07-delhi, hin-in-10, hin-in-16-mumbai, hin-in-23, hin-in-24, hin-in-24-jaipur, hin-in-34, hin-in-34-patna, hin-in-35, hin-in-35-bhopal, hin-in-36, hin-in-36-lucknow, hin-in-37-raipur, hin-in-38, hin-in-38-ranchi, hin-in-39, hin-in-39-dehradun, hun-hu-01-kiskunfelegyhaza, hun-hu-05-budapest, hun-ro-09-viscri, mar-in-16, mar-in-16-mumbai, mar-in-16-nagpur, mar-in-16-pune, pol-pl-72-wrocaw, pol-pl-77-krakow, pol-pl-78-warsaw, pol-pl-86-poznan, por-br-04, por-br-05, por-br-05-salvador, por-br-15-belo-horizonte, por-br-17, por-br-18, por-br-18-apucarana, por-br-18-arapongas, por-br-18-curitiba, por-br-21-rio-de-janeiro, por-br-22, por-br-23, por-br-26, por-br-26-lages, por-br-27-sao-paulo, por-br-29, ron-ro-03-pitesti, ron-ro-07-botosani, ron-ro-09-brasov, ron-ro-11-buzau, ron-ro-13-cluj-napoca, ron-ro-14-constanta, ron-ro-16-targoviste, ron-ro-18-galati, ron-ro-21-hunedoara, ron-ro-23-iasi, ron-ro-28-piatra-neamt, ron-ro-29-slatina, ron-ro-30-ploiesti, ron-ro-36-timisoara, ron-ro-39-ramnicu-valcea, rus-by-05-stowbtsy, rus-ru-48-moscow, rus-ru-67-saratov, spa-ar-07-buenos-aires, spa-co-34-bogota, spa-es-29-madrid, spa-es-34, spa-es-34-aviles, spa-es-34-gijon, spa-es-34-oviedo, spa-es-51-sevilla, spa-es-57, spa-es-58, spa-es-58-pontevedra, spa-es-58-vigo, spa-es-60-valencia, spa-mx-07-saltillo, spa-mx-15, spa-mx-24-los-zacatones, tam-in-25-chennai, tam-in-25-coimbatore, tam-in-25-cuddalore, tam-in-25-dharmapuri, tam-in-25-erode, tam-in-25-madurai, tam-in-25-nagapattinam, tam-in-25-namakkal, tam-in-25-thiruvarur, tam-in-25-vellore, zho-cn-04-suzhou, zho-cn-10, zho-cn-11, zho-cn-12, zho-cn-23-shanghai, zho-cn-25-weihai.
Authentication
Send your key as a bearer token on every /v1/ request: Authorization: Bearer tk_live_<id>_<secret>.
Keys beginning tk_test_ belong to the staging service and are refused by the live one, and the reverse.
Any problem with the key itself (missing, malformed, unknown, revoked) is the same 401, deliberately.
A key that is valid on an account suspended for non-payment gets 402; a closed account, or one waiting to be enabled after signup, gets 403.
Lost a key? Contact support to have it revoked and a new one issued.
Plans and limits
| Plan | Regions | Rate | Concurrent requests |
|---|---|---|---|
| Starter | 3 regions you choose | 60 requests / minute | 4 |
| Growth | 15 regions you choose | 600 requests / minute | 16 |
| Scale | Every region, now and later | No per-minute limit | 64 |
Every plan also has: conversions that are not counted or billed; at most 5,000 words per request (past that it is a batch job, and the request is refused with 403); a request body of at most 1 MiB (413).
Prices are shown at checkout. Scale has no per-minute rate. On Starter and Growth the per-minute rate protects the service, not the bill, and is enforced and answers 429 with Retry-After.
Rate-limit headers
Every admitted /v1/convert and /v1/profiles response on a plan with a per-minute rate carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute on your plan. |
X-RateLimit-Remaining | Requests left in the current sliding minute. |
X-RateLimit-Reset | Unix time (seconds) at which the window is full again. |
A 429 carries Retry-After in seconds. Back off for at least that long; the window slides, so retrying immediately is refused again.
Errors
Every error is {"error": "<a sentence saying what to do>"} with one of these statuses:
| Status | When | Retry? |
|---|---|---|
| 400 | The body is not JSON, or text / region is missing or empty. | No: fix the request. |
| 401 | No key, or a key that is malformed, unknown or revoked. | No. |
| 402 | The account is suspended for non-payment. POST /v1/billing/portal still works, so you can pay. | After paying. |
| 403 | A region your plan does not hold (the message lists those you do); no profile for that region yet (the message lists those available); over 5,000 words in one request; or the account is closed or waiting to be enabled. | No. |
| 413 | Request body over 1 MiB. | No. |
| 429 | Over your per-minute rate, or over your plan's concurrent requests. | Yes, after Retry-After. |
| 503 | The service could not reach its own storage or payment provider. The detail is logged on our side, never returned. | Yes, with backoff. |
Choosing regions
Starter and Growth cover a number of regions that you choose. A new account holds none, and /v1/convert answers 403 for a region you have not chosen, naming the ones you hold. Choose with POST /v1/account/regions:
curl https://terrior.app/v1/account/regions \
-H 'Authorization: Bearer tk_live_...' \
-H 'Content-Type: application/json' \
-d '{"add": ["cat-es-56-barcelona"]}'
A region you choose is held for 30 days before it can be released, so a swap is one call with both remove and add once the hold has passed. Moving to a smaller plan keeps the regions you have held longest. Scale includes every region, including the ones added after you subscribe, and has nothing to choose.
What is billed
Your plan's monthly fee, and nothing else. The same text for the same region always converts the same way, so convert a text once and keep the result; conversions are not counted against the fee and there is no per-call charge. Invoices, payment method and cancellation are in the billing portal (POST /v1/billing/portal).
Endpoints
POST /v1/convert
Rewrite text toward how region writes it. Only swaps that are provably safe are applied; the rest are returned as flagged for a person to decide. Syntax and structure are never changed.
| Request field | Type | |
|---|---|---|
region | string | Required. One of the regions in /v1/profiles. |
text | string | Required, non-empty, at most 5,000 words. |
{
"applied": [
{
"concept": "upcoming~forthcoming",
"offset": 4,
"original": "forthcoming",
"replacement": "upcoming"
},
{
"concept": "includes~comprises",
"offset": 23,
"original": "comprises",
"replacement": "includes"
},
{
"concept": "affected~impacted",
"offset": 52,
"original": "impacted",
"replacement": "affected"
},
{
"concept": "later~subsequently",
"offset": 108,
"original": "subsequently",
"replacement": "later"
},
{
"concept": "strengthen~bolster",
"offset": 121,
"original": "bolster",
"replacement": "strengthen"
},
{
"concept": "trying~attempting",
"offset": 148,
"original": "attempting",
"replacement": "trying"
}
],
"convert_ms": 0.0332,
"flagged": [
{
"concept": "members~colleagues",
"offset": 92,
"original": "colleagues",
"reason": "'members' and 'colleagues' may differ in meaning here, not only in which one the region writes; check before using; the two words are not written beside the same neighbouring words, so the swap could leave a nearby word that no longer fits; check before using",
"replacement": "members"
}
],
"region": "eng-in-05-chandigarh",
"text": "The upcoming update includes fixes for everyone affected by last week's outage, and our colleagues will later strengthen support for anyone trying to move their account."
}
| Response field | |
|---|---|
text | The input with every applied swap made. |
applied[] | Swaps made. offset is a Unicode code-point offset into your input, not into the returned text. |
flagged[] | Swaps the profile suggests but did not make, each with a reason. Review these before publishing. |
caveats[] | What this region's profile does not yet know. Show them to whoever approves the copy. |
convert_ms | Server-side conversion time in milliseconds, excluding network and queueing. |
GET /v1/profiles
The regions your key can use, each with the number of rules, a content digest (changes when the profile is rebuilt), and its caveats.
{ "profiles": [ { "region": "lko", "n_rules": 83, "digest": "c8626095…", "caveats": ["…"] } ] }
GET /v1/account
Your plan, the regions you hold and when each can be released, subscription status and this billing period's usage.
{ "tenant": "t_…", "key_id": "…", "tier": "basic",
"plan": "growth", "region_allowance": 15,
"regions_held": [{ "region": "cat-es-56-barcelona", "since": 1791590400, "releasable_at": 1794182400 }],
"requests_per_minute": 600, "max_concurrency": 16,
"subscription": { "status": "active", "period_start": 1791590400, "period_end": 1794268800 },
"usage_this_period": { "requests": 1250, "words": 52310 } }
POST /v1/account/regions
Choose or release regions on a plan that covers a number of them. Both lists are optional; removals apply first. Answers with the same body as GET /v1/account, or 400 saying what would succeed: over the plan's count, a region still inside its 30-day hold, a region with no profile, or a plan that includes every region.
{ "add": ["cat-es-56-barcelona"], "remove": [] }
POST /v1/billing/portal
Returns a one-time link to the billing portal: invoices, payment method, cancellation. Works for an account suspended for non-payment.
{ "url": "https://billing.stripe.com/p/session/…" }
GET /health no key
{ "status": "ok", "regions": ["del", "lko"] }
POST /v1/score not yet available
Scoring (which region a piece of copy reads as, and which words carry that reading) is not served by this API yet and answers 501. It is not billed.