Audio & Web Search
Extra services share the same authentication, rate-limit, and quota pipeline as chat. They're billed at published per-unit prices (see your plan). See also HTML to Markdown, Documents and Images.
Speech to text
POST /api/v1/audio/transcriptions — speech-to-text. Billed per audio minute,
rounded up to a hundredth of a minute.
Send the audio as the request body with its own content type — not as a multipart form field:
curl https://khabot.com/api/v1/audio/transcriptions \
-H "Authorization: Bearer kb_YOUR_KEY" \
-H "Content-Type: audio/mpeg" \
--data-binary @call.mp3
An optional ?model= query parameter selects a transcription model. The response is
the transcript JSON: metadata.duration carries the audio length that was billed, and
the text sits under results.channels[0].alternatives[0].transcript.
Text to speech
POST /api/v1/audio/speech — text-to-speech. Billed per 1,000 characters.
curl https://khabot.com/api/v1/audio/speech \
-H "Authorization: Bearer kb_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Hello world"}' --output speech.mp3
The field is text; an optional model selects the voice. The response
body is the audio itself (audio/mpeg), so write it to a file or stream it on.
Web search
POST /api/v1/web-search — search the live web and get back full structured
JSON: ranked results with title, URL, snippet and source, plus related questions and
searches. Render them straight into your own app's UI, or hand them to a model — the structure is
yours to use. Billed 1 unit per search.
Request
{
"query": "python asyncio event loop",
"gl": "us",
"hl": "en",
"page": 1
}
| Field | Default | Notes |
|---|---|---|
query (or q) | — | Required. Up to 400 characters. |
gl | "us" | Two-letter country code. |
hl | "en" | Language code. |
page | 1 | 1–10. Roughly ten results per page — paginate with this. |
Anything outside those rules returns 400 bad_request.
Response
200 with exactly these top-level keys:
| Key | Type |
|---|---|
query, gl, hl | string — the normalized request you were served |
page | number — the page returned |
results | array of { title, url, snippet, source, position } |
peopleAlsoAsk | array of { question, snippet, url } |
relatedSearches | array of strings |
totalResults | number |
resolvedCountry | string — the country actually used for the search |
tookMs | number — how long the search took |
A field with no value comes back as null rather than being dropped, so you can index
into results safely without guarding every key.
Example
curl https://khabot.com/api/v1/web-search \
-H "Authorization: Bearer kb_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"python asyncio event loop","gl":"us","hl":"en","page":1}'
{
"query": "python asyncio event loop",
"page": 1,
"gl": "us",
"hl": "en",
"results": [
{
"title": "Event loop — Python 3.14.7 documentation",
"url": "https://docs.python.org/3/library/asyncio-eventloop.html",
"snippet": "The event loop is the core of every asyncio application…",
"source": "Python documentation",
"position": 1
}
],
"peopleAlsoAsk": [],
"relatedSearches": [],
"totalResults": 124,
"resolvedCountry": "US",
"tookMs": 380
}
Trimmed for brevity — a page carries about ten results, and
peopleAlsoAsk / relatedSearches are populated when the query has them.
Each search bills one unit. If the search backend is unavailable you get
503 upstream_unavailable and are not billed.