Quickstart
Four steps from nothing to a first result. Every endpoint lives under https://api.kaer.ai/v1.
1. Create an account
Sign up at api.kaer.ai. New accounts get $10 of free credit, valid for 90 days.
2. Open search with a top-up
Search is locked on the Free tier and answers 403 tier_required there. Your first top-up of $5 or more moves you to Starter at once: search opens, and the free credit pays for it too.
3. Make a key with the search scope
In the dashboard, open Keys, create a key and tick search. Copy it right away: it starts with kaer_live_ and is shown only once. Keep it on your server.
4. Search
curl https://api.kaer.ai/v1/search \
-H "Authorization: Bearer $KAER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "eu packaging regulation importers",
"lane": "deep",
"num_results": 3
}'The answer names the lane that ran and what it cost:
{
"object": "search.result",
"lane": "deep",
"served_lane": "deep",
"results": [
{
"title": "The PPWR: EU Regulation for a More Circular Economy and Less Packaging Waste",
"url": "https://everwave.de/en/2026/07/13/the-ppwr-eu-regulati…",
"snippet": "The PPWR—Packaging and Packaging Waste Regulation; EU Regulation 2025/40—will take effect on August 12,…",
"source": ["kaer_web"],
"text": "The PPWR (Packaging and Packaging Waste Regulation, EU Regulation 2025/40) will take effect on 12 August 2026 and introd…",
"text_source": "page",
"text_truncated": false
},
{ "title": "Regulation (EU) 2025/40 on packaging and packa…", … },
{ "title": "EU Packaging Regulation Training – Become PPWR…", … }
],
"kaer": { "request_id": "req_…", "cost_usd": 0.003 }
}Authentication
Every call except GET /v1/models needs a key. Send it as a bearer token:
Authorization: Bearer kaer_live_...or in an x-api-key header. When Authorization holds a bearer token, it wins.
Scopes
| Scope | Endpoints | Opens at |
|---|---|---|
models | POST /v1/chat/completions, GET /v1/me | Free |
search | POST /v1/search, POST /v1/search/batch, POST /v1/images | Starter |
maps | GET /v1/maps | Starter |
mail | POST /v1/mail/send, GET /v1/mail/messages/{id} | Build |
What is checked, in order
- The key exists and is not revoked or expired: else 401
invalid_api_key(no key at all:missing_api_key). - The account is active: else 403
account_inactive. - The caller's IP is on the key's allow-list, when it has one: else 403
ip_not_allowed. - The key has the endpoint's scope: else 403
insufficient_scope. - Your tier includes the service: else 403
tier_required. - Rate limits: else 429
rate_limit_exceeded. - A hold for the call's maximum cost: else 402 (
insufficient_credits,monthly_limit_reachedorkey_budget_exceeded).
Allow-lists, budgets, expiry and rotation are in Key security.
Search
api.kaer.ai/v1/search- Scope
search- Tier
- Starter and up
- Price
- $0.0005 to $0.003 per query, see Search pricing
- Free credit
- Yes, once Starter opens it
Kaer's own search index, alone or racing the live web, with ranked results.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
queryRequired | string | · | 1 to 512 characters after trimming. |
lane | string | auto | web, fast, deep or auto, case-insensitive. Anything else is a 400. |
num_results | integer | 5 | Results per query, 1 to 25. Numbers outside are clamped; anything that is not a number is a 400. Also read as numResults. |
include_domains | string[] | · | Only results from these hosts or their subdomains, up to 20. The web lane applies it before ranking; the others filter after retrieval. Also includeDomains. |
exclude_domains | string[] | · | Never return results from these hosts or their subdomains, up to 20. Also excludeDomains. |
Any other field is not applied, and the response lists it in kaer.ignored_params, so a filter you expected never fails silently. The body is limited to 16 KB.
Lanes
| Lane | What it does | Per query | Timeout |
|---|---|---|---|
web | Kaer's index alone. Fastest and cheapest. | $0.0005 | 6 s |
fast | The index racing the live web. | $0.001 | 12 s |
deep | Every lane, re-ranked. Results carry the page's text, up to 4,000 characters, when it could be read. | $0.003 | 40 s |
auto | Kaer picks a lane per query. Billed at the lane that served it. | As served | 40 s |
A query that runs past its lane's timeout fails with 502 search_unavailable and is not charged.
Response
| Field | Type | Description |
|---|---|---|
object | string | search.result |
query | string | The query, trimmed. |
lane | string | The lane you asked for. |
served_lane | string | The lane that ran, and the one billed. |
results[].title | string | Page title. |
results[].url | string | Page address. |
results[].snippet | string | Text from the page that matches the query. |
results[].source | string[] | Which engines returned the page. |
results[].published_date | string | Publication time (RFC 3339), when known. |
results[].text | string | Deep only: the page's main text, up to 4,000 characters. |
results[].text_truncated | boolean | Deep only: the text was cut. |
results[].text_source | string | Deep only: page, or snippet when the site would not serve us the page. |
kaer.request_id | string | Same as x-request-id. |
kaer.cost_usd | number | What the query cost. |
kaer.ignored_params | string[] | Fields that were not applied, when there are any. |
{
"object": "search.result",
"query": "latest ai research",
"lane": "fast",
"served_lane": "fast",
"results": [
{
"title": "…",
"url": "https://…",
"snippet": "…",
"source": ["kaer_web"]
}
],
"kaer": { "request_id": "req_…", "cost_usd": 0.001 }
}Rows can carry more fields, such as scores on the deep lane; rely on the ones above.
Headers: x-request-id and the x-ratelimit-* headers. Search sends no x-kaer-cost-usd header: the cost is kaer.cost_usd in the body.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. Images also needs a JSON object. |
| 400 | query_required | query is missing or blank. |
| 400 | query_too_long | query is over 512 characters (search) or 300 (images). |
| 400 | invalid_lane | lane is not web, fast, deep or auto. |
| 400 | invalid_num_results | num_results is not a number. |
| 400 | invalid_domains | include_domains or exclude_domains is not an array of up to 20 host names. |
| 403 | tier_required | Your tier does not include this service yet. |
| 413 | body_too_large | The body is over the endpoint's limit: chat 1 MB, search 16 KB, batch 64 KB, images 8 KB, mail 512 KB. |
| 502 | search_failed | The search service failed. Nothing was charged. |
| 503 | search_busy | Search is at capacity. Nothing was charged. |
| 502 or 503 | search_unavailable | Search is unreachable, timed out or paused. Nothing was charged. |
curl https://api.kaer.ai/v1/search \
-H "Authorization: Bearer $KAER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "latest ai research",
"lane": "fast",
"num_results": 5
}'Batch search
api.kaer.ai/v1/search/batch- Scope
search- Tier
- Starter and up
- Price
- Per served row, at search prices
- Free credit
- Yes, once Starter opens it
Many queries in one round trip. Every row shares the lane, the result count and the domain filters.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
queriesRequired | string[] | · | 1 to your tier's batch size. Each query is 1 to 512 characters after trimming. |
lane | string | auto | web, fast, deep or auto, case-insensitive. Anything else is a 400. |
num_results | integer | 5 | Results per query, 1 to 25. Numbers outside are clamped; anything that is not a number is a 400. Also read as numResults. |
include_domains | string[] | · | Only results from these hosts or their subdomains, up to 20. The web lane applies it before ranking; the others filter after retrieval. Also includeDomains. |
exclude_domains | string[] | · | Never return results from these hosts or their subdomains, up to 20. Also excludeDomains. |
Unread fields come back in kaer.ignored_params. The body is limited to 64 KB, and the whole batch to 40 seconds.
| Tier | Queries per batch |
|---|---|
| Starter | 10 |
| Build | 50 |
| Scale | 100 |
| Pro | 100 |
Billing
- Each row that comes back with
ok: trueis billed like a single search, at the lane that served it. - Identical queries run once, but every row is billed, duplicates included.
- Rows that fail carry
charged: falseand cost nothing. - While the batch runs, the gateway holds the dearest possible price for every row (
autoholds the deep price). - If the whole batch fails, nothing is charged.
Response
| Field | Type | Description |
|---|---|---|
object | string | search.batch |
lane | string | The lane you asked for. |
count | integer | Rows requested. |
served | integer | Rows served and billed. |
results[].index | integer | Position in queries. |
results[].query | string | The query. |
results[].ok | boolean | Whether the row was served. |
results[].served_lane | string | Served rows: the lane billed. |
results[].results | array | Served rows: the hits, shaped like Search results. |
results[].charged | boolean | false on rows that failed. |
results[].error | string | Failed rows: why. |
kaer.cost_usd | number | What the batch cost. |
Rows can carry more diagnostic fields; read only the ones above.
Headers: x-request-id and the x-ratelimit-* headers. The cost is kaer.cost_usd in the body; there is no x-kaer-cost-usd header.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. Images also needs a JSON object. |
| 400 | queries_required | queries is missing or empty. |
| 400 | batch_too_large | More queries than your tier's batch size. |
| 400 | invalid_query | A batch query is blank or over 512 characters. |
| 400 | invalid_lane | lane is not web, fast, deep or auto. |
| 400 | invalid_num_results | num_results is not a number. |
| 400 | invalid_domains | include_domains or exclude_domains is not an array of up to 20 host names. |
| 403 | tier_required | Your tier does not include this service yet. |
| 413 | body_too_large | The body is over the endpoint's limit: chat 1 MB, search 16 KB, batch 64 KB, images 8 KB, mail 512 KB. |
| 502 | search_failed | The search service failed. Nothing was charged. |
| 502 or 503 | search_unavailable | Search is unreachable, timed out or paused. Nothing was charged. |
curl https://api.kaer.ai/v1/search/batch \
-H "Authorization: Bearer $KAER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"queries": ["solar panel efficiency 2026", "heat pump grants uk"],
"lane": "web",
"num_results": 3
}'Images
api.kaer.ai/v1/images- Scope
search- Tier
- Starter and up
- Price
- $0.002 per call
- Free credit
- Yes, once Starter opens it
Real photos, diagrams and illustrations for a subject, from Kaer's picture index with live Wikimedia Commons and Pexels results when the index is thin. Every picture is checked and comes with its licence, a ready-made credit line and whether a business website may reuse it.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
queryRequired | string | · | What to picture, 1 to 300 characters ("sourdough bread", "water cycle diagram"). |
count | integer | 12 | 1 to 30 pictures. Anything else, a numeric string included, is a 400. |
reusable | boolean | false | Only pictures a business website may use with the returned credit. |
Other fields come back in kaer.ignored_params. The body is limited to 8 KB; a call times out after 15 seconds.
Price
One call costs $0.002 whatever count is, even when it finds no pictures. A call that fails is not charged.
Response
| Field | Type | Description |
|---|---|---|
object | string | images.result |
query | string | The query, trimmed. |
subject | string | The subject the index searched for. |
count | integer | Pictures returned. |
reusable_only | boolean | Echoes reusable. |
withheld | object | Present when some pictures were held back. |
results[].id | string | Stable picture id. |
results[].kind | string | photo, diagram or illustration. |
results[].title | string | Picture title. |
results[].image_url | string | Full-size image (https). |
results[].thumb_url | string | Thumbnail (https). |
results[].page_url | string | The page the picture comes from. |
results[].host | string | That page's host. |
results[].width, height | integer | Pixel size, when known. |
results[].source | string | wikimedia_commons, pexels or web. |
results[].license | object | name, code, label and url. |
results[].credit | string | Author or owner. |
results[].attribution | string | Attribution text from the source. |
results[].credit_line | string | A ready-made credit to publish with the picture. |
results[].attribution_required | boolean | Publish credit_line next to the picture when true. |
results[].share_alike | boolean | Derivatives must keep the same licence. |
results[].commercial_ok | boolean | A business website may use it. |
{
"object": "images.result",
"query": "sourdough bread",
"subject": "sourdough bread",
"count": 12,
"reusable_only": true,
"results": [
{
"id": "commons:sourdough.jpg",
"kind": "photo",
"title": "Sourdough loaf",
"image_url": "https://upload.wikimedia.org/…",
"thumb_url": "https://upload.wikimedia.org/…/500px-…",
"page_url": "https://commons.wikimedia.org/wiki/File:…",
"host": "commons.wikimedia.org",
"width": 4000,
"height": 3000,
"source": "wikimedia_commons",
"license": { "name": "…", "code": "cc-by-sa", "label": "CC BY-SA 4.0", "url": "https://creativecommons.org/licenses/by-sa/4.0/" },
"credit": "Ann K",
"attribution": "…",
"credit_line": "…",
"attribution_required": true,
"share_alike": true,
"commercial_ok": true
}
],
"kaer": { "request_id": "req_…", "cost_usd": 0.002 }
}Headers: x-request-id and the x-ratelimit-* headers. The cost is kaer.cost_usd in the body; there is no x-kaer-cost-usd header.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. Images also needs a JSON object. |
| 400 | query_required | query is missing or blank. |
| 400 | query_too_long | query is over 512 characters (search) or 300 (images). |
| 400 | invalid_count | count is not an integer from 1 to 30. |
| 400 | invalid_reusable | reusable is not true or false. |
| 403 | tier_required | Your tier does not include this service yet. |
| 413 | body_too_large | The body is over the endpoint's limit: chat 1 MB, search 16 KB, batch 64 KB, images 8 KB, mail 512 KB. |
| 502 | images_failed | The picture search failed. Nothing was charged. |
| 503 | images_busy | Images are at capacity. Nothing was charged. |
| 502 or 503 | images_unavailable | Images are unreachable, timed out or paused. Nothing was charged. |
curl https://api.kaer.ai/v1/images \
-H "Authorization: Bearer $KAER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "sourdough bread", "count": 20, "reusable": true}'Choosing a lane
Every lane takes the same request and returns the same shape. Pick by how far the answer has to come from.
| Lane | Searches | Send it for | Per 1,000 |
|---|---|---|---|
web | Kaer's index alone. | High volume on known topics, at the lowest price. | $0.50 |
fast | The index racing the live web. | Pages the index may not hold yet. | $1.00 |
deep | Every lane, re-ranked, with each page's text. | Grounding a model in the source, not a snippet. | $3.00 |
auto | Kaer picks a lane per query. | When you would rather not choose. | As served |
On deep, each result also carries text (the page's main text, up to 4,000 characters), text_truncated and text_source: page, or snippet when a site would not serve the page.
auto holds the deep price while it runs, then settles at the lane that answered. served_lane names it, so you always know what you paid for.
Use from assistants
Kaer's MCP server lets Claude, ChatGPT, Cursor and other MCP clients ask Kaer, and Kaer's web search runs on this index. It is billed to your Kaer account, not to an API platform balance.
It takes a Kaer account key (kaer_sk_…, made in the Kaer app), not a kaer_live_ API key:
claude mcp add --transport http kaer https://app.kaer.ai/api/mcp \
--header "Authorization: Bearer kaer_sk_…"Every client, step by step: Connect Claude and ChatGPT over MCP.
Pricing
Search is billed per query, not per result: a query costs the same with 1 result or 25. The price is set by the lane that served it.
| Lane | Per query | Per 1,000 | Per 100,000 |
|---|---|---|---|
web | $0.0005 | $0.50 | $50.00 |
fast | $0.001 | $1.00 | $100.00 |
deep | $0.003 | $3.00 | $300.00 |
auto | $0.0005 to $0.003 | As served | As served |
Images (POST /v1/images) | $0.002 per call | $2.00 | $200.00 |
Rules
- A served query is billed whatever
num_resultsis and however many results come back, an empty list included. autoholds the deep price ($0.003) while it runs, then settles at the lane that served it;served_lanenames it. If the lane cannot be read, the query is billed asdeep.- In a batch, every served row is billed like a single search, duplicates included. Failed rows are free. The hold is the dearest lane price times the number of rows.
- Queries that fail or time out are not charged.
- Free credit pays for search and images on Starter and up. Search is not open on the Free tier.
Worked examples
10,000 web searches | 10,000 × $0.0005 | $5.00 |
|---|---|---|
10,000 fast searches | 10,000 × $0.001 | $10.00 |
10,000 deep searches | 10,000 × $0.003 | $30.00 |
100 auto searches: 60 served by web, 30 by fast, 10 by deep | 60 × $0.0005 + 30 × $0.001 + 10 × $0.003 | $0.09 |
A batch of 50 fast queries, 2 rows failed | 48 × $0.001 (hold while running: 50 × $0.001) | $0.048 |
| 1,000 image calls | 1,000 × $0.002 | $2.00 |
Errors
Errors are OpenAI-shaped, so any OpenAI client shows them sensibly:
{
"error": {
"message": "The model 'gpt-9' does not exist on api.kaer.ai.",
"type": "invalid_request_error",
"param": null,
"code": "model_not_found"
}
}type is the OpenAI class; code is the precise reason, so branch on code. A call that fails with an error status is not charged; a stream that breaks after it started is charged for what it sent. The maps service's own errors keep their shape, see Maps.
Keys and access
| Code | Status | Meaning | What to do |
|---|---|---|---|
missing_api_key | 401 | No key in Authorization: Bearer or x-api-key. | Send your key. |
invalid_api_key | 401 | The key is malformed, unknown, revoked or expired. | Check the key, or create a new one. |
account_inactive | 403 | The account is suspended or closed, or owes a reversed payment. | If the message mentions a balance owed, add credit. Otherwise write to [email protected]. |
ip_not_allowed | 403 | The caller's IP is not on the key's allow-list. | Add the address or range to the key, or call from an allowed host. |
insufficient_scope | 403 | The key does not have the endpoint's scope. | Add the scope to the key in the dashboard. |
tier_required | 403 | Your tier does not include this service yet. | Search, images and maps open at Starter ($5 paid). Mail opens at Build. |
unknown_endpoint | 404 | No such endpoint under /v1. | Check the path against this page. |
not_found | 404 | The path is not part of the API. | Call https://api.kaer.ai/v1/.... |
Balance and spend
| Code | Status | Meaning | What to do |
|---|---|---|---|
insufficient_credits | 402 | Your balance cannot cover the call's hold. Free credit never pays for mail. | Top up, turn on automatic top-up, or lower max_tokens to shrink the hold. |
monthly_limit_reached | 402 | This month's spend plus open holds would pass your monthly cap (your budget or your tier's). | Raise your budget in Settings, move up a tier, or wait for the new month (UTC). |
key_budget_exceeded | 402 | The key's monthly budget is used up. Open holds count. | Raise or clear the key's budget. |
payment_required | 402 | A refunded or disputed payment left a balance owed. | Add credit to clear it. |
Rate limits
| Code | Status | Meaning | What to do |
|---|---|---|---|
rate_limit_exceeded | 429 | Too many requests this minute for the key or the account, or too many invalid keys from your address. | Wait for Retry-After, then back off. See Rate limits and tiers. |
too_many_streams | 429 | Your account already has its tier's maximum of open streams. | Wait for a stream to finish. |
key_shared | 429 | A Free tier key was used from too many networks within an hour. | Keep keys on your server. A top-up moves you off the Free tier. |
mail_rate_limit | 429 | Messages in the last minute reached your tier's cap. | Wait 60 seconds (Retry-After). |
mail_daily_limit | 429 | Recipients today (UTC) would pass your tier's cap. | Wait until 00:00 UTC (Retry-After) or move up a tier. |
Request
| Code | Status | Meaning | What to do |
|---|---|---|---|
invalid_json | 400 | The body is not valid JSON. Images also needs a JSON object. | Send a JSON object with Content-Type: application/json. |
body_too_large | 413 | The body is over the endpoint's limit: chat 1 MB, search 16 KB, batch 64 KB, images 8 KB, mail 512 KB. | Send less. |
query_required | 400 | query is missing or blank. | Send a query. |
query_too_long | 400 | query is over 512 characters (search) or 300 (images). | Shorten it. |
invalid_lane | 400 | lane is not web, fast, deep or auto. | Use one of the four lanes. |
invalid_num_results | 400 | num_results is not a number. | Send an integer from 1 to 25. |
invalid_domains | 400 | include_domains or exclude_domains is not an array of up to 20 host names. | Send arrays of host names. |
queries_required | 400 | queries is missing or empty. | Send an array of query strings. |
batch_too_large | 400 | More queries than your tier's batch size. | Split the batch, or move up a tier. |
invalid_query | 400 | A batch query is blank or over 512 characters. | Fix or drop that query. |
invalid_count | 400 | count is not an integer from 1 to 30. | Send an integer from 1 to 30. |
invalid_reusable | 400 | reusable is not true or false. | Send a boolean. |
Service
| Code | Status | Meaning | What to do |
|---|---|---|---|
search_failed | 502 | The search service failed. Nothing was charged. | Retry. |
search_busy | 503 | Search is at capacity. Nothing was charged. | Retry in a few seconds. |
search_unavailable | 502 or 503 | Search is unreachable, timed out or paused. Nothing was charged. | Retry later. |
images_failed | 502 | The picture search failed. Nothing was charged. | Retry. |
images_busy | 503 | Images are at capacity. Nothing was charged. | Retry in a few seconds. |
images_unavailable | 502 or 503 | Images are unreachable, timed out or paused. Nothing was charged. | Retry later. |
Rate limits and tiers
Your tier is earned by money paid in (net of refunds) and time since your first payment, never by spend.
| Tier | Req/min per key | Req/min per account | Open streams | Monthly cap |
|---|---|---|---|---|
| Free | 20 | 30 | 2 | $10 |
| Starter | 60 | 120 | 5 | $100 |
| Build | 300 | 600 | 20 | $1,000 |
| Scale | 1,000 | 2,000 | 50 | $5,000 |
| Pro | 3,000 | 6,000 | 100 | $25,000 |
| Enterprise | Custom | Custom | Custom | Custom |
How tiers rise
| Tier | Unlock | Services | Batch size | Active keys |
|---|---|---|---|---|
| Free | Sign up | Models | None | 3 |
| Starter | $5 paid | Models, Search, Maps | 10 | 10 |
| Build | $50 paid and 7 days since your first payment | Models, Search, Maps, Mail | 50 | 25 |
| Scale | $250 paid and 14 days since your first payment | Models, Search, Maps, Mail | 100 | 50 |
| Pro | $1,000 paid and 30 days since your first payment | Models, Search, Maps, Mail | 100 | 100 |
| Enterprise | Contract with Kaer | All | Custom | Custom |
How limits apply
- Requests are counted in fixed 60-second windows, per key and per account; whichever runs out first answers 429
rate_limit_exceededwithRetry-After. - An edge gateway also caps each key at 12,000 requests a minute (bursts up to 2000), so the per-key limit is the lower of that and your tier's figure.
- Open streams are counted per account; over the limit, 429
too_many_streams. - The monthly cap counts settled spend plus open holds in the calendar month (UTC). Your own budget in Settings can only lower it.
- The dashboard playground has its own limit of 20 calls a minute.
- Restricted accounts run at Free tier limits.
On the Free tier only models are open and spend is capped at $10 a month, even with $10 of free credit. A first top-up of $5 moves you to Starter at once.