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
}
FieldDefaultNotes
query (or q)—Required. Up to 400 characters.
gl"us"Two-letter country code.
hl"en"Language code.
page11–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:

KeyType
query, gl, hlstring — the normalized request you were served
pagenumber — the page returned
resultsarray of { title, url, snippet, source, position }
peopleAlsoAskarray of { question, snippet, url }
relatedSearchesarray of strings
totalResultsnumber
resolvedCountrystring — the country actually used for the search
tookMsnumber — 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.