Reference
API
Two endpoints. The request shape mirrors what most agent code already speaks, so adopting it is usually a base URL change.
Authentication
Send your key either way. Both work, on every endpoint.
authorization: Bearer twl_...
x-api-key: twl_...
Keys carry one of two scopes. A search key can spend quota and nothing
else, which is what you deploy into agents and containers. An admin key
can also mint and revoke keys and read usage. The raw key is shown once, at creation;
only its hash is stored, so it cannot be recovered.
POST /v1/search
{
"query": "what changed in the EU AI act", // required
"search_depth": "basic", // "basic" | "advanced"
"topic": "general", // "general" | "news"
"max_results": 5,
"include_answer": false,
"include_raw_content": false,
"include_domains": [],
"exclude_domains": [],
"time_range": "week" // day | week | month | year
} Depth is the cost dial. basic returns provider snippets
and fetches no pages. advanced fetches every result, extracts clean
Markdown, and ranks passages. That is roughly an order of magnitude in both latency
and cost. Asking for an answer implies the full pipeline, because there is nothing to
answer from otherwise.
Topic selects the providers. general uses Bing and
DuckDuckGo; news uses Google News and Yahoo News.
Response
{
"query": "...",
"answer": "The Act entered into force ...", // null unless include_answer
"answer_citations": ["https://..."],
"results": [
{
"title": "...",
"url": "https://example.com/article",
"content": "# Heading\n\nClean markdown ...",
"score": 0.03175,
"published_date": "Tue, 12 Aug 2026 09:14:00 GMT",
"providers": ["bing", "duckduckgo"],
"fetched": true,
"fetch_error": null
}
],
"providers": [
{ "provider": "bing", "status": "ok", "resultCount": 10, "elapsedMs": 304 },
{ "provider": "duckduckgo", "status": "circuit_open", "resultCount": 0,
"error": "breaker open until ...", "elapsedMs": 0 }
],
"response_time": 3.1
} Always read providers
Three of the four sources are HTML scrapers. This array is how you tell a complete answer from a degraded one.
| status | Meaning |
|---|---|
ok | Returned results. |
failed | Tried and failed. error says why. |
circuit_open | Out of rotation after repeated failures. |
disabled | Switched off for your org. |
skipped | Cannot serve this topic. |
An empty results array only ever means the web had nothing to say. If
every provider failed you get a 502, never a 200 with nothing in it.
The providers field on each result is a quality signal in its own right:
a URL listed by two engines was ranked independently by both. Results are fused by
rank, so agreement lifts a page above any single provider's favourite.
POST /v1/extract
The same pipeline without the retrieval stage, for URLs you already have.
{ "urls": ["https://example.com/a", "https://example.com/b"] } {
"results": [
{ "url": "...", "title": "...", "content": "# ...", "source": "fetch" }
],
"failed": [
{ "url": "...", "error": "disallowed by robots.txt" }
],
"response_time": 1.2
} URLs are canonicalised and deduplicated before anything is charged, so the same page with three different tracking parameters costs one extraction. Billing is per URL, and URLs that produce nothing are refunded. If nothing at all could be extracted you get a 502, because an empty array would read as "these pages are blank".
Errors
| Status | When |
|---|---|
| 400 | Malformed request. details carries the field errors. Fail loud; this is a bug in the caller, not an empty result. |
| 401 | Missing, unknown, or revoked key. |
| 403 | Valid key, insufficient scope. |
| 429 | Rate limit or quota. Rate limits carry retry-after. |
| 502 | Every provider failed, or nothing could be extracted. |
| 503 | A required capability is not configured. |
A missing capability returns 503 naming it rather than quietly omitting it. A response that looks successful while leaving out the thing you asked for sends you debugging the wrong layer.
Notes for agent authors
- Start with
basicdepth. Most agent queries are answered by titles and snippets, and it is roughly ten times cheaper and faster. include_answeris not free. It spends the answer meter and adds a model round trip. If you are already running a model, passresultsto it instead.- Retrieved content is untrusted. We fence it and constrain the answer schema, but if you feed
contentinto your own prompt, treat it as hostile input. A search API is an attractive injection target because the attacker only has to rank for a query you run. - Cite from
answer_citations. Those URLs are validated against the sources actually supplied, so an injected or hallucinated index is dropped before it reaches you.
Base URL
https://api.trawlia.co