kamai

Headless browser & search API for AI agents | skill.md | health | source

kamai gives your agent a real Chromium browser, web, image & social search with automatic provider failover, per-domain memory, and PDF brochure generation — over plain HTTPS JSON. No SDK required.

Quick start

Base URL: https://kamai.minai.work

Identify your app on authenticated routes with either header: x-api-key: <key> or x-wallet-address: 0x… (Celo). Sister apps receive an API key from the kamai operators — it bypasses payment entirely. Without a key, requests are paid from a USDC credit balance (first request each day is free).

curl -X POST https://kamai.minai.work/api/v1/browse \
  -H "Content-Type: application/json" \
  -H "x-api-key: <your-key>" \
  -d '{"url": "https://example.com"}'

Legacy routes need no auth at all — sister backends can call POST /browse, POST /search/web, POST /search/image and /browse/memories directly. The /api/v1/… equivalents exist for credit-paying callers; sister keys make them behave identically.

Browse

POST /api/v1/browse  ·  legacy alias /browse

Navigates a real headless Chromium to a URL, optionally performs actions (fill forms, click through flows), then extracts clean text, links, and form fields. Stealth measures are applied to avoid headless detection. JavaScript dialogs (alert/confirm/prompt) are auto-accepted and logged.

{
  "url": "https://example.gov.ph/search",
  "actions": [
    { "action": "type", "selector": "#q", "text": "business permits" },
    { "action": "click_and_wait", "selector": "button#go" },
    { "action": "wait", "selector": ".results" }
  ],
  "selector": ".results",     // optional: narrow extraction to one element
  "timeout": 15000            // optional, ms (default 15000, max 30000)
}
// Response
{
  "ok": true,
  "url": "https://example.gov.ph/results",
  "title": "Search Results",
  "text": "…page content as plain text…",
  "links":  [{ "text": "Permit application", "href": "https://…" }],
  "forms":  [{ "tag": "input", "type": "text", "name": "q", "selector": "#q" }],
  "memories": ["Use /Indexes/index for keyword search instead of homepage"],
  "actions_performed": ["typed \"business permits\" into #q", "clicked button#go → navigated"],
  "sessionId": "…",           // your persistent session (see Sessions)
  "length": 4500
}

Safety: file:, data:, localhost and private-IP URLs are blocked. Text is capped at 30 000 chars.

Browse actions

Up to 20 actions per request, executed in order before extraction. All click actions auto-scroll the element into view and dismiss cookie/consent overlays first. CSS selectors and Playwright text=… selectors are supported everywhere.

ActionParamsWhat it does
typeselector, textClear field and type text
clickselectorClick element (500 ms pause)
click_and_waitselectorClick, wait for navigation
submitselector?Programmatic form submit (ASP.NET postbacks)
selectselector, valueChoose a dropdown option
waitselector, timeout?Wait for element to appear
wait_msmsPause (max 5000)
scroll_toselectorScroll element into view (centered)
js_clickselectorForce-click via JS — bypasses overlays/pointer interception
set_dateselector, valueSet any date picker, value yyyy-mm-dd
evaluatetextRun arbitrary JS on the page

Sessions

Sessions are automatic. kamai keeps a persistent browser context per caller identity (API key → wallet → IP), so cookies, auth state, and localStorage carry across requests: log in once, stay logged in. Idle sessions expire after 30 minutes.

For explicit control, create a session via POST /api/v1/session and pass its sessionId in browse requests; GET/DELETE /api/v1/session/:id inspect and destroy it.

Screenshots

POST /api/v1/screenshot  ·  legacy alias /screenshot

Capture the relevant part of any URL — including social posts, which are captured by rendering the platform's own embed rather than its login wall.

{ "url": "https://x.com/jack/status/20", "mode": "auto", "format": "jpeg" }

// → { "ok": true, "screenshotId": "…", "imageUrl": "/api/v1/screenshot/…/image",
//     "strategy": "embed:x", "width": 550, "height": 225, "sizeBytes": 37888,
//     "expiresAt": "…" }

mode auto (default) · relevant · viewport · full · element (needs selector) · format jpeg/png · width 320–2000 · maxHeight 200–8000 · scale 1–3 · encoding url (default) or base64. GET /api/v1/screenshot/:id/image is public and needs no auth, so the link can go straight to a vision model.

PlatformStrategy
x · instagram · linkedin · facebook · threads · blueskyembed:* — full post card via the official embed
tiktokembed:tiktok — card renders, video poster frame often does not
redditapify:reddit-card — rendered from scraped data, ~110s; not a pixel capture
any other URLpage:* — cropped to the main content region

If an embed returns an error page ("Post not found", a rate-limit notice) the request fails with that reason rather than returning a blank image — a screenshot that proves nothing is worse than no screenshot.

Web, news, image, and social search across multiple premium providers with automatic failover — one API, no search keys to manage on your side. Post-comment crawl for Facebook, X, and Reddit.

POST /api/v1/search/web  ·  legacy alias /search/web

{ "q": "Tim Cook age", "count": 5, "country": "US" }

// → { "ok": true, "source": "web",
//     "results": [{ "title", "url", "description", "content"?, "age"? }] }

count 1–20 (default 5) · country 2-letter code or ALL · freshness pd|pw|pm|py or durations like 90min, 2h, 3d · maxTokens context budget (default 4096). source identifies which backend answered — failover is automatic. When available, results include extracted page content.

News search

POST /api/v1/search/news  ·  legacy alias /search/news

Searches news indexes instead of the general web — use it whenever recency matters. Unlike /search/web, the freshness window is enforced exactly and every result carries an ISO 8601 publishedAt you can sort on.

{ "q": "openai funding round", "freshness": "6h", "count": 10 }

// → { "ok": true, "source": "serper_news",
//     "results": [{ "title", "url", "description", "source",
//                   "publishedAt", "age"? }] }

count 1–50 (default 10) · freshness pd|pw|pm|py or durations like 90min, 2h, 3d · sort date (default) or relevance · country 2-letter code or ALL · filterSources true (default) · cursor/nextCursor pagination for deep coverage. Ranked newest-first, and non-news sources — app stores, corporate FAQ and support pages, retailers, job boards, wikis — are dropped; set filterSources: false to keep them. Tight windows may return fewer than count results — that means there was no fresher coverage.

POST /api/v1/search/image  ·  legacy alias /search/image

{ "q": "Eiffel Tower at night", "count": 10, "safesearch": "strict" }

// → { "ok": true, "results": [{ "title", "url", "imageUrl", "thumbnailUrl",
//     "width", "height", "source" }] }

count 1–50 (default 5) · safesearch strict (default) or off.

Social search

POST /api/v1/search/social  ·  legacy alias /search/social

Keyword search across social platforms — brand mentions, competitor tracking, community research. One request shape for every platform; results are normalized to one schema.

{ "platform": "reddit", "q": "your brand", "freshness": "3d", "count": 10 }

// → { "ok": true, "source": "social", "platform": "reddit",
//     "results": [{ "id", "url", "text", "author", "publishedAt",
//                   "likes", "comments", "shares", "views", "meta"? }],
//     "nextCursor": "…", "hasMore": true }

Ranked newest-first, with publishedAt normalized to ISO 8601 (or null) on every backend. Pass sort to hand ordering back to the platform instead.

PlatformNotes
x (aliases twitter, x.com)Keyword search with engagement counts · chronological · cursor pagination for multi-day backfill (page with nextCursor; bound with freshness)
redditFull post search · sort: relevance|new|top|comment_count · timeframe: day|week|month|year|all
linkedinPublic post search · sort: relevance|date · timeframe: day|week|month
tiktok · youtube · threads · pinterestKeyword search
instagramKeyword search of public reels — runs live, expect 15–60s
facebookPublic post search — runs live, so expect 20–60s response times; public content only
facebook-eventsEvents search

freshness limits results by age: presets pd|pw|pm|py or exact durations like 90min, 2h, 3d, 1w — the exact window is always enforced server-side; tight windows may return fewer than count results. Pagination: one page per request (count max 100); pass the response's nextCursor back as cursor and repeat while hasMore is true. Cursors are bound to their platform+q; bound a backfill with freshness. Available on x (by timestamp), facebook (by day), reddit, linkedin, tiktok, youtube, threads, pinterest; instagram is single-page. If a platform's primary backend is unavailable, queries automatically fall back through secondary providers, then a site-scoped web search (source: "web") where the platform indexes well.

Post comments

POST /api/v1/search/comments  ·  legacy alias /search/comments

Replies under one public Facebook, X, or Reddit post. Nested replies are flattened. Textless items are dropped.

{ "platform": "facebook", "url": "https://www.facebook.com/…/posts/pfbid…", "count": 300 }

// → { "ok": true, "results": [{ "id", "url"?, "author", "text",
//                   "publishedAt"?, "likes"? }],
//     "hasMore": false, "nextCursor": null }

platform facebook · x (aliases twitter, x.com) · reddit. count 1–500 (default 100). A deleted, private, or empty post returns ok: true with an empty list — never HTTP 404 (404 means the route itself is missing). Expect 20–150s; reddit is the slow end.

Domain memories

Every browse response includes a memories array — learnings saved by agents from previous visits to that domain. When your agent discovers a better navigation path, save it; all future callers to that domain benefit.

RoutePurpose
GET/browse/memories?domain=example.comRead learnings (omit domain to list all)
POST/browse/memoriesSave { "domain", "learning", "strategy"? }
DELETE/browse/memories/:idRemove a learning

Also mounted at /api/v1/browse/memories. Free on both paths. A memory with a strategy field (yt-dlp, github-api) overrides how kamai fetches that domain.

PDF brochures

Generate multi-page corporate PDFs from structured JSON. Templates: corporate-overview, product-showcase, event-invitation.

RoutePurpose
GET/api/v1/brochure/templatesfree — list templates + required fields
POST/api/v1/brochure/generate — render a new brochure
PATCH/api/v1/brochure/:id — merge changes & re-render
GET/api/v1/brochure/:id/downloadfree — the PDF itself, no auth (shareable link)

Full content schema (sections, products, charts, contact info, images as URLs or base64) is in skill.md.

Pricing & credits

RequestCost
Browse (no actions)$0.009
Browse with actions$0.013
Search (web or image)$0.003
Brochure generate / update$0.050
Brochure templates & downloads, memories, healthfree

Sister keys bypass payment entirely. Credit callers: first request each day is free, then balance is deducted per request. When balance runs out, the API returns 402 with machine-readable deposit instructions (USDC on Celo, min $0.10). Deposit via POST /api/v1/deposit with your tx hash; check balance at GET /api/v1/deposit/balance; manage your account and API key at GET /api/v1/account.

Endpoint summary

RouteAuth
POST/api/v1/browsekey / wallet (legacy /browse: none)
POST/api/v1/search/web · /api/v1/search/news · /api/v1/search/image · /api/v1/search/social · /api/v1/search/commentskey / wallet (legacy /search/*: none)
POST/api/v1/screenshot · GET /:id/image is publickey / wallet
GET·POST·DEL/browse/memories · /api/v1/browse/memoriesnone
POST/api/v1/brochure/generatekey / wallet
PATCH/api/v1/brochure/:idkey / wallet
GET/api/v1/brochure/templates · /api/v1/brochure/:id/downloadnone
POST·GET·DEL/api/v1/sessionrate-limited
GET·POST/api/v1/account · /api/v1/depositwallet
GET/skill.mdnone
GET/healthnone

Errors & limits

{ "ok": false, "error": "Navigation timeout: 15000ms exceeded" }