Toli

Help

Help & support

A reference, not a tutorial: you arrive here with one question rather than reading it top to bottom. Every route, field name and number on this page is the one the code uses. The gateway answers at https://toli.essabu.com.

1. Getting a key

There is no self-service sign-up. Keys are issued by hand, so write to toli@essabu.com and say what you are building. Saying so beats a form that leads nowhere.

  • A key looks like toli_ followed by 32 random characters. It is shown once, at issue. Only its SHA-256 is stored, so we cannot read it back to you: a lost key is replaced, never recovered.
  • It travels in the Authorization header, as Bearer toli_... Nothing else authenticates a call: no cookie, no session, no CSRF token. The gateway enables none of them.
  • A key carries scopes, and there are exactly four: ask, batch, usage, status. POST /v1/ask asks for ask, POST /v1/batch for batch, GET /v1/usage and GET /v1/models for usage, and the agent routes of a status page for status. A key issued for reporting cannot spend a reading.
  • A key carrying status is bound to one status page, and the agent routes never name a page in the path: the page is read from the key. That is why a status agent key cannot reach another tenant's page by editing a URL.
  • OAuth 2.0 client credentials exists beside the keys, never instead of them. POST /v1/oauth/token mints a bearer token of the form toli_at_ followed by 40 characters, valid one hour (3600 seconds); POST /v1/oauth/revoke kills it on the next request. Both endpoints are limited to 60 requests per minute per IP address.

2. A first call

POST /v1/ask, with your key in Authorization. One state, every question at once, one reading back. Asking six questions in one call costs roughly what asking one does; asking them in six calls costs six times.

  • The body has three fields. state: the thing to judge, text or an object, up to 128 000 characters once serialised. model: the version you are pinning, a string of at most 64 characters. questions: an object of 1 to 32 entries, keyed by names you choose.
  • Each question carries kind, one of choice, score or noul; instructions, up to 4000 characters; and optionally criteria. The gateway validates the shape and nothing else: the SDKs already refuse a Choice with fewer than two options before the network, and a second copy of that rule here would drift from the first.
  • A 200 carries model, answers and usage. answers is keyed by your own question names. model is the version the engine actually served, never an echo of what you asked for: that is what warns you the day the provider ships an update.
  • usage carries input_tokens and output_tokens. Note the snake_case: it is the convention of /v1/ask and /v1/batch. The status surface answers in camelCase, and that difference is deliberate rather than an oversight.
  • Every response, errors included, carries an x-request-id header. Send it with any question about a call and the call is findable; without it, it is a guessing game.
  • The response never carries a route. The gateway does not decide on your behalf, and one test in the suite has no other job than keeping it that way.

3. Reading an answer

The shape of the answer follows the shape of the question. Three kinds, and the third behaves differently enough to deserve its own paragraph.

  • Choice returns the winning key in choice, a probability for every option you offered in probabilities, and a calibrated confidence in confidence.
  • Score returns a weighted float in score, the wording of the level it lands on in legend, and a calibrated confidence in confidence.
  • Noul returns noul alone: the probability that the statement holds. There is no confidence field, and its absence is the contract, not a gap.
  • The winning probability and the confidence are two different numbers. The largest entry in probabilities is the share the model gave that option; confidence is the calibrated figure, the one that matches how often an answer at that level turns out to be right. Threshold confidence. Thresholding the largest probability gives you a number that moves with how many options you offered.
  • For a Noul there is no confidence to read, so the certainty is derived: |noul - 0.5| x 2. A noul of 0.5 gives a certainty of 0; a noul of 0.02 and a noul of 0.98 both give 0.96. Half is ignorance, not a no, and a confident no deserves to be acted on exactly like a confident yes. Comparing noul itself against a threshold sends every certain negative to a human, and it is the single most misread number in the contract.

4. Thresholds and the three routes

One certainty, two cuts, three routes. Your code reads the route, never the probability.

  • certainty >= 0.85 gives ACT: your code acts alone. certainty >= 0.60 gives CONFIRM: it proposes and a person approves. Below 0.60, ESCALATE: a human queue, or a larger model. Both cuts are inclusive.
  • 0.85 and 0.60 are the defaults the SDKs ship. They are a starting point and were not measured on your data. A threshold holds for one model, one set of questions, one corpus.
  • So the SDKs make you date them: Thresholds.measured(act: 0.85, confirm: 0.60, measuredOn: ..., measuredAt: ...). Whoever reads act: 0.9 six months from now has to be able to tell what that 0.9 was established on.
  • The route is computed in the SDK, from your thresholds, on your side. The gateway returns answers and confidences and nothing else: moving a threshold is a deployment of yours, not a ticket opened with us.

5. Advisory domains

Some domains cannot have an ACT route at all. Clinical orientation and screening are the clearest case: a mis-routed support ticket is re-routed the next morning, a person sent home is not.

  • Thresholds.advisory(confirm: ..., measuredOn: ..., measuredAt: ...) is a different constructor, not a higher number. Under it, route() cannot return ACT, whatever confidence the engine reported.
  • A certainty of 0.99 in an advisory domain routes to CONFIRM. Not clamped, not warned about: CONFIRM is the best a reading can earn there.
  • No configuration overrides this, because there is nothing to override: ACT is absent from what the function is able to return. A threshold can be edited in passing by someone who does not know what it guarded; a missing branch cannot.

6. Batches

POST /v1/batch, scope batch. Several readings in one request, each idempotent on a reference you supply. Unlike /v1/ask, a batch is written synchronously: being idempotent means handing back what was decided the first time, which is impossible while the record is still sitting in a queue.

  • items holds 1 to 100 entries. Each one is an /v1/ask body plus client_ref: a required string of at most 128 characters, distinct within the batch. Two entries sharing one would make your own reconciliation ambiguous long before it reached ours.
  • A batch always answers 200, even when entries failed. The status of a batch is not the status of its worst entry, and a 500 would tell you to replay forty successful readings to recover one.
  • Each entry of results carries client_ref, status (ok or failed) and replayed. A judged entry adds model, answers and usage; a failed one adds error. summary counts judged, replayed and failed.
  • A client_ref is unique per key, and forever. Resubmit one and you get the first judgement back with replayed: true, at no charge, even if the state you sent this time is different. That is the whole point of it, and it is also the trap: reuse a reference and you will never see the new answer.
  • client_ref is stored exactly as you send it, in clear, and nothing deletes it. Put your own record identifier in it, not somebody's e-mail address or national id.

7. Usage, quota and models

Two read routes, both under the usage scope.

  • GET /v1/usage answers period (as YYYY-MM), readings, input_tokens, output_tokens, and quota with monthly and remaining. On an unlimited key both quota figures are null. It is read from the readings table, which is the ledger, and not from the counter that gates the quota.
  • This route is deliberately outside the quota gate. A key that has spent its month must still be able to see why.
  • GET /v1/models answers models, a list of id and status. The SDKs pin a version; this is how an integrator learns that the one they pinned is on its way out, before it stops answering rather than after.
  • The quota is monthly and counted per key. Spending it gives a 429 on /v1/ask and /v1/batch, carrying a retry-after header set to the seconds left in the month.

8. The status page and its JSON

The hosted status product. Everything public is read without a key, names the page in the path, and reads a table rather than a health check: a page that says operational because the machine serving it is up has never told anyone anything. Field names on this surface are camelCase.

  • GET /v1/status/{page} answers state, updatedAt, page, services and incidents. state is the worst state among the services, derived on every read and stored nowhere; it is one of operational, degraded, partial, major, maintenance. The incidents returned here are the ones still running.
  • Each service carries id, slug, name and description (each an object with fr and en), position, state, then its strip: uptime, recordedDays, windowDays and days. windowDays is 90.
  • Read recordedDays before quoting uptime. The window is ninety days wide, but a page opened yesterday has eighty-nine days of silence, and an empty window reports 100. The figure is a record of declared states, not a measurement, and it is not a service-level commitment.
  • GET /v1/status/{page}/services/{slug} returns one service and its strip. GET /v1/status/{page}/incidents is the archive: resolved incidents, newest first, cursor paginated, with limit defaulting to 20 and capped at 100. GET /v1/status/{page}/incidents/{slug} returns one incident and its updates.
  • POST /v1/status/{page}/subscribers takes email and an optional locale (fr or en) and answers 202 whatever happens. An address already subscribed gets the identical answer: a 409 on a duplicate would turn a public form into an oracle for who is watching this company's outages. So a 202 tells you the request was accepted, and nothing about whether a mail went out.
  • The confirmation mail carries a one-shot link, GET /v1/status/confirm/{token}, which answers 204 and is then spent. GET /v1/status/unsubscribe/{token} also answers 204 and deletes the row. Neither names a page: the token identifies the subscription, and asking somebody to name the page they are leaving is hostile.
  • Only the confirmation mail carries an unsubscribe link. The incident mails that follow do not. It is a known gap, named here rather than hidden.
  • A page can answer on the customer's own host. There the page segment is gone rather than optional: /, /services/{slug}, /incidents, /incidents/{slug} and POST /subscribers. A host nobody provisioned is a 404, never a default page. The slug routes keep working while the vanity DNS is the thing that is broken, which is exactly the day it matters.
  • The agent routes take a key carrying status and never name the page: PUT /v1/status/services/{slug} with state; POST /v1/status/incidents with title, impact (none, minor, major or critical), status (investigating, identified or monitoring; resolved is refused on opening), serviceIds and body; POST /v1/status/incidents/{slug}/updates with status and body; POST /v1/status/probes/{slug} with state and an optional uptime between 0 and 100. title and body are objects with fr and en, and one of the two languages is enough.
  • Posting an update with status: resolved stamps the incident resolved and returns every affected service to operational, unless it was under maintenance. It is the one piece of automation here, and it exists because forgetting it is how a page stays red for a week after everything came back.

9. The embeddable widget

Two lines, in any page, framework or none: <script src='https://toli.essabu.com/v1/status/embed.js' data-toli-status data-page='acme' defer></script>. The script takes no key and names no page: it reads the slug from the tag, then calls the same public GET /v1/status/{page} as everybody else. Here are the attributes exactly as it reads them.

  • data-page, required. The page slug. Missing, the widget writes [toli-status] missing data-page in the console and renders nothing.
  • data-toli-status. A marker, needed whenever the tag is injected rather than parsed, which is every single-page application and every tag manager: document.currentScript is null there, and this is how the script finds its own tag. Without it the console reads [toli-status] cannot find its own tag; add data-toli-status.
  • data-variant. banner gives a full-width bar. Anything else, absence included, gives the inline badge.
  • data-locale. fr or en; only the first two letters are looked at. Absent, the widget falls back to the document's lang attribute, then to English.
  • data-target. A CSS selector of the element to render into. Absent, the widget renders where the tag sits. A selector that matches nothing warns [toli-status] target not found.
  • data-href. Where the badge links. The order is data-href first, then the page's customDomain if one is provisioned, then no link at all. A badge with nowhere honest to send you carries no href rather than sending you to a wall of JSON.
  • data-toli-ready is written by the script onto its own tag and is not for callers: it is what stops a second injection from adopting the first tag.
  • Everything renders inside a shadow root: the host page cannot restyle the widget by accident, and our styles cannot leak into it. The state is carried by the words as much as by the colour of the dot.
  • It reads once on load, then every 60 seconds, and again the moment the tab comes back. Each call is abandoned after 5 seconds. A failure paints Status unavailable rather than disappearing: the one component whose job is to say that something is wrong must not go quiet exactly when it is.
  • The script is served as application/javascript, to any origin, with Cache-Control public, max-age=300, stale-while-revalidate=3600 and an ETag over its contents. Five minutes is the window during which a bad release keeps running on pages we cannot reach.
  • Point data-target at an empty div. An element that cannot host a shadow root, or that already hosts one, makes it write [toli-status] cannot render into that target, and nothing appears.

10. The messenger in the corner

Plainly: the messenger panel is indeed in the corner of these pages, and the gateway serves no /v1/support routes yet. Opening a conversation fails, and the panel says so rather than pretending. Write to toli@essabu.com: a person answers, usually within a day.

  • The panel mounts on every page even so, and mounting it sets one first-party cookie: toli-visitor, a uuid, path /, SameSite Lax, one year. That cookie is the whole of the identity the messenger was designed around, and it is set before any message is written.
  • Two other keys stay in your browser and never leave it: toli-locale, the language you picked, held both as a cookie and in localStorage, and toli-theme. There is no analytics, no tag manager and no third-party script on this site.
  • The state of the hosted products is a separate surface, and that one does work: see the status page and its JSON above.

11. Error responses

One envelope for the whole surface, RFC 7807, served as application/problem+json. The body carries type (https://toli.essabu.com/errors/ followed by the title), title, status, detail and request_id. A 422 adds errors, which names the fields. The same envelope is served on a customer's own domain, so an SDK never meets a second error shape there.

  • 401, invalid_key. No bearer token was presented, or it is unknown, revoked or expired. The engine is never reached: the refusal happens before it.
  • 403, request_failed. The credential is good and does not carry the scope this route asks for. The detail names the scope, for example: This key does not carry the 'batch' scope. 401 is about the credential, 403 is about what it may do.
  • 404, not_found. No such status page, service, incident or subscription. It is 404 and never 403 for all five cases, deliberately: a 403 would confirm that somebody runs a page under that name, which is the question a crawler is asking when it tries one.
  • 409, invalid_transition. An incident status change the contract refuses.
  • 422, invalid_request. The shape is wrong. errors names each field: errors.state for a state that is neither text nor an object or that exceeds 128 000 characters, errors.items for a malformed batch.
  • 429, quota_exceeded, on /v1/ask and /v1/batch: the key has spent its monthly quota. The response carries a retry-after header holding the seconds left in the month. A 429 from /v1/oauth/token or /v1/oauth/revoke is a different thing: it is the per-IP rate limit, and its title is request_failed.
  • 503, engine_unavailable. The judgement engine could not answer. The gateway fails closed and never invents a default judgement: one that answered anyway would be worse than one that is down, because nobody would find out.
  • 500, internal_error, with the detail The gateway failed to answer. Send us the request_id.

12. Troubleshooting

Failures somebody actually hits, and what each one means.

  • 401 on a key that worked yesterday: it was revoked, or it is an OAuth token past its hour. A token lives 3600 seconds, and revocation takes effect on the next request rather than at an expiry.
  • 403 naming a scope: a reading key carries ask, batch and usage by default, and a status agent key carries status alone. No scope covers everything.
  • 404 on an agent status route while the key authenticates fine: the key is bound to no page, or to a page that no longer exists. The agent path never names a page, so the key is the only thing deciding where the write lands.
  • 429 on /v1/ask while GET /v1/usage still answers: that is by design. The usage route is not behind the quota gate, precisely so you can read why you are being refused.
  • 429 on /v1/oauth/token from a server that mints one token an hour: the limit is 60 requests per minute per IP, and the counter is keyed on the address the gateway sees. Behind a proxy that is the proxy's address, so every client behind it shares one bucket.
  • 422 with errors.state: the state is neither text nor an object, or it is over 128 000 characters once serialised. Measure what you serialise, not what you typed.
  • A batch entry comes back with replayed: true and an answer that does not match the state you just sent: that client_ref was judged before. A reference is judged once per key, permanently, and resubmitting never re-judges. Take a new reference.
  • 422 on a batch before anything is judged: two entries share a client_ref. They have to be distinct within one request.
  • The widget shows Status unavailable while everything is up: open the browser console. The script names which mistake it is, and a 404 from it is an integration mistake, not an outage. [toli-status] no status page called "acme" means the slug is wrong or the page was never provisioned.
  • The widget renders nothing at all and warns that it cannot find its own tag: the tag was injected rather than parsed, so document.currentScript is null. Add data-toli-status to it.
  • A service shows 100 per cent and an empty strip: nothing has been reported for it yet. recordedDays is 0, and an empty window averages to 100. Read recordedDays before quoting the figure.
  • The status page says operational while your service is down: nothing here probes anything. A state is declared, by a person through the agent routes or by your own prober posting to POST /v1/status/probes/{slug}. With nobody posting, the page keeps showing the last thing declared.
  • POST /subscribers answered 202 and no mail arrived: 202 is the same answer for a new address, for one already subscribed and for one already confirmed, on purpose. Check the spam folder, then write to us with the page slug.

13. Contact

Write to toli@essabu.com. Toli is published by Essabu Intelligence Provider, and the gateway runs on a dedicated server in Germany. Include the x-request-id of the response you are asking about, and the page slug if it concerns a status page.

© 2026 Toli. Essabu Intelligence Provider. All rights reserved.