{"_id":"serpkite","_rev":"5-60af431a705185ef5da4e5537aaec082","name":"serpkite","dist-tags":{"latest":"0.4.0"},"versions":{"0.0.0-stage":{"name":"serpkite","version":"0.0.0-stage","_id":"serpkite@0.0.0-stage","stub":true,"maintainers":[{"name":"serpkite","email":"support@serpkite.com"}],"dist":{"shasum":"1621f9057db22e23ad2960e0109829a118b3dda4","tarball":"https://registry.npmjs.org/serpkite/-/serpkite-0.0.0-stage.tgz","fileCount":2,"integrity":"sha512-WUEJsx2s2pqz3dE+3VMfFRTWp8lNGiQNl8vlCgX/mO0tZSNN6WfS5q8Zjj4LGjVbWvkxVKd3NSF0yBjYN0OjXg==","signatures":[{"sig":"MEYCIQD5Ej4kS1yeatQ/cEZ/V4voLP9Hj2k6WQ9mS44v6S0HyQIhAMsi8wtFDBah2YewFznXmDBvgVpz6nNZ6N75vVgXAQkd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":330},"_npmUser":{"name":"serpkite","email":"support@serpkite.com"},"description":"Temporary package placeholder for staged publishing","directories":{}},"0.1.1":{"name":"serpkite","version":"0.1.1","keywords":["serpkite","google","search","serp","api","ai","agents","llm","markdown"],"author":{"name":"SerpKite"},"license":"MIT","_id":"serpkite@0.1.1","maintainers":[{"name":"serpkite","email":"support@serpkite.com"}],"homepage":"https://serpkite.com/docs","bugs":{"url":"https://github.com/SerpKite/serpkite-js/issues"},"dist":{"shasum":"cd2ea7df7f0a393723451a9981f818c78b6f032b","tarball":"https://registry.npmjs.org/serpkite/-/serpkite-0.1.1.tgz","fileCount":9,"integrity":"sha512-/+vSSTqXhsCltm2YYca9nw6exGukgrMizf0R9u1OBuyuflSJShHO+HD385nX6PJGjQYIHrfZkNGHi/RH5dFg0Q==","signatures":[{"sig":"MEUCIBQfACqYLFq8JZW+RWwUA5lJa+CXjhUBvDvF6UCTgu6/AiEA8VO4idImpovbsVO9xQIsCT+DnFFYTidxQ6HXnJPgo28=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDQIBUmdGbTfL9PMW1P717y7oH1jcHYlhHZ4NiRhaiXqwIgOvSoLr1uFyD31zjvi+2AC2Ena/Qu+O1+HeNx9eHX7Vo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/serpkite@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":296191},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"6b7e237bc8ccc133a4033faabe8120166195ddfa","scripts":{"gen":"bun scripts/gen.ts","test":"bun test","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"serpkite","email":"support@serpkite.com"},"repository":{"url":"git+https://github.com/SerpKite/serpkite-js.git","type":"git"},"_npmVersion":"12.2.0","description":"Official TypeScript/JavaScript SDK for SerpKite, the Google search API built for AI agents: clean JSON or Markdown results.","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","@types/bun":"^1.3.0","typescript":"^6.0.3","openapi-typescript":"^7.13.0"},"_npmOperationalInternal":{"tmp":"tmp/serpkite_0.1.1_1790959938690_0.8499028821040344","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"serpkite","version":"0.2.0","keywords":["serpkite","google","search","serp","api","ai","agents","llm","markdown"],"author":{"name":"SerpKite"},"license":"MIT","_id":"serpkite@0.2.0","maintainers":[{"name":"serpkite","email":"support@serpkite.com"}],"homepage":"https://serpkite.com/docs","bugs":{"url":"https://github.com/SerpKite/serpkite-js/issues"},"dist":{"shasum":"85d283ff54fbf1af72c8c5be4af1c592ac200cda","tarball":"https://registry.npmjs.org/serpkite/-/serpkite-0.2.0.tgz","fileCount":9,"integrity":"sha512-xf6b13ji5/x4sTS9+XfIX1p0iYljngei8E7bPKVUHcywzoPASjPfrcctVHgT0JNBU6IXimpeUpZ6AEl8/AHHNw==","signatures":[{"sig":"MEUCIGQxYSexDKsvPu3o+xOwIpEmuhf1X2qTHrDzTcXR/Y+aAiEAimSwf7MwDqoBz5/72uCSxYpSotZni8t0dlgAGRtpc4E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDFOSyWQhwKKtvWbwMbUTi+6/qvfPXk3KJHVZUc7VHRDwIhANNazqh5liOZSDGo1JLeodmoJ+eszghSNl081Qj+BUI0","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/serpkite@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":288862},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"8fc28d5f73041076501f0d0cee956ac652035e64","scripts":{"gen":"bun scripts/gen.ts","test":"bun test","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"serpkite","email":"support@serpkite.com"},"repository":{"url":"git+https://github.com/SerpKite/serpkite-js.git","type":"git"},"_npmVersion":"12.2.0","description":"Official TypeScript/JavaScript SDK for SerpKite, the Google search API built for AI agents: clean JSON or Markdown results.","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","@types/bun":"^1.3.0","typescript":"^6.0.3","openapi-typescript":"^7.13.0"},"_npmOperationalInternal":{"tmp":"tmp/serpkite_0.2.0_1790972637291_0.4493468592700429","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"serpkite","version":"0.3.0","keywords":["serpkite","google","search","serp","api","ai","agents","llm","markdown"],"author":{"name":"SerpKite"},"license":"MIT","_id":"serpkite@0.3.0","maintainers":[{"name":"serpkite","email":"support@serpkite.com"}],"homepage":"https://serpkite.com/docs","bugs":{"url":"https://github.com/SerpKite/serpkite-js/issues"},"dist":{"shasum":"2a97871e43590b71b492129c5b663a2023601b2d","tarball":"https://registry.npmjs.org/serpkite/-/serpkite-0.3.0.tgz","fileCount":9,"integrity":"sha512-n42B3iXObX6ifKUaoi7ILFmY/m328lV5VJ3hOwcFPlVVaidSKTaimdlNIAsjlfuQrk6HiIBTG8GmzW+FmvwAAQ==","signatures":[{"sig":"MEUCIGM4/uprGR5f+Vxq4luJSzxBsNscP2ji6W/q2UKAXe5gAiEAzPj6GLOplkKb6U0hknl2WNshuU2K80ovjiteMMBFCF0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIF9aRu0AOwfLJCYmnSwpUBlXjBuYRTmIWmfQS1RgidmmAiBuELid8OLwU9CxWUhoGr0evpORma0hezUijX8+09QhJQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/serpkite@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":282483},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"0d9eea532b4d7337df25ebca6a5fdd0e0f87237c","scripts":{"gen":"bun scripts/gen.ts","test":"bun test","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"serpkite","email":"support@serpkite.com"},"repository":{"url":"git+https://github.com/SerpKite/serpkite-js.git","type":"git"},"_npmVersion":"12.2.0","description":"Official TypeScript/JavaScript SDK for SerpKite, the Google search API built for AI agents: clean JSON or Markdown results.","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","@types/bun":"^1.3.0","typescript":"^6.0.3","openapi-typescript":"^7.13.0"},"_npmOperationalInternal":{"tmp":"tmp/serpkite_0.3.0_1791044154039_0.4567763316418456","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"_id":"serpkite@0.4.0","bugs":{"url":"https://github.com/SerpKite/serpkite-js/issues"},"dist":{"shasum":"a60e57f0be230790d5e8b5453c3cac6a8c0ffe52","tarball":"https://registry.npmjs.org/serpkite/-/serpkite-0.4.0.tgz","fileCount":9,"integrity":"sha512-kYqt2QuxLPLMdc+Wnbe8WrIIZySawFIZxPzYqkDEGebYskZJtq5T67wRHqLovPQ0z4F5e19HQeLbGqW/nwkf5Q==","signatures":[{"sig":"MEYCIQC5GlMBebBGgMM9NS4a0JufNneCHtKtFh4Yie1FIUlAAAIhAJXvON1Apo+DE5xL3fjv0XuLqQippYKNliAuvLjyabQY","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDCeWZNTqVnK52sViKctP1EPT3D51wcwh5ghmaCTsfsqAiBZzPWxdIqzhJZQZ/YIGbrKOdkx+hP9bDV4Mgv9wRpZ2Q=="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/serpkite@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":472399},"main":"./dist/index.cjs","name":"serpkite","type":"module","types":"./dist/index.d.ts","author":{"name":"SerpKite"},"module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"fa6c264be6745fb93efc64212379c5ceb3e37962","license":"MIT","scripts":{"gen":"bun scripts/gen.ts","test":"bun test","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"bun run build"},"version":"0.4.0","_npmUser":{"name":"serpkite","email":"support@serpkite.com"},"homepage":"https://serpkite.com/docs","keywords":["serpkite","google","search","serp","api","ai","agents","llm","markdown"],"repository":{"url":"git+https://github.com/SerpKite/serpkite-js.git","type":"git"},"_npmVersion":"12.2.0","description":"Official TypeScript/JavaScript SDK for SerpKite, the Google search API built for AI agents: clean JSON or Markdown results.","directories":{},"maintainers":[{"name":"serpkite","email":"support@serpkite.com"}],"sideEffects":false,"_nodeVersion":"24.21.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","@types/bun":"^1.3.0","typescript":"^6.0.3","openapi-typescript":"^7.13.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/serpkite_0.4.0_1791077128405_0.1430149351023995"}}},"time":{"created":"2026-10-02T16:49:40.869Z","modified":"2026-10-04T01:25:28.768Z","0.0.0-stage":"2026-10-02T16:49:40.869Z","0.1.1":"2026-10-02T16:52:18.828Z","0.2.0":"2026-10-02T20:23:57.385Z","0.3.0":"2026-10-03T16:15:54.142Z","0.4.0":"2026-10-04T01:25:28.478Z"},"bugs":{"url":"https://github.com/SerpKite/serpkite-js/issues"},"author":{"name":"SerpKite"},"license":"MIT","homepage":"https://serpkite.com/docs","keywords":["serpkite","google","search","serp","api","ai","agents","llm","markdown"],"repository":{"url":"git+https://github.com/SerpKite/serpkite-js.git","type":"git"},"description":"Official TypeScript/JavaScript SDK for SerpKite, the Google search API built for AI agents: clean JSON or Markdown results.","maintainers":[{"name":"serpkite","email":"support@serpkite.com"}],"readme":"# serpkite\n\nOfficial TypeScript/JavaScript SDK for [SerpKite](https://serpkite.com), the Google search API built\nfor AI agents: clean JSON or Markdown, credits that never expire.\n\n- Zero runtime dependencies: uses the global `fetch`.\n- Works in Node 18+, Bun, Deno, Cloudflare Workers and other edge runtimes.\n- ESM and CommonJS builds with full type definitions, generated from the OpenAPI contract.\n- Retries with exponential backoff and jitter on `429`, `5xx` and network errors (honours `Retry-After`).\n\n## Install\n\n```bash\nnpm install serpkite\n# or: bun add serpkite / pnpm add serpkite / yarn add serpkite\n```\n\nDeno: `import { SerpKite } from \"npm:serpkite\";`\n\n## Quickstart\n\n```ts\nimport { SerpKite } from \"serpkite\";\n\nconst sk = new SerpKite(); // reads SERPKITE_API_KEY\nconst res = await sk.search({ q: \"best espresso machine\", country: \"us\" });\nconsole.log(res.results[0].title, res.meta.credits_used);\n\n// Markdown for an LLM prompt: `format: \"markdown\"` resolves to a string.\nconst md = await sk.search({ q: \"best espresso machine\", format: \"markdown\" });\n```\n\nGet an API key at [app.serpkite.com](https://app.serpkite.com) and export it:\n\n```bash\nexport SERPKITE_API_KEY=skt_live_...\n```\n\n## Configuration\n\n```ts\nconst sk = new SerpKite({\n  apiKey: \"skt_live_...\", // default: process.env.SERPKITE_API_KEY\n  baseUrl: \"https://api.serpkite.com\", // default: SERPKITE_BASE_URL or https://api.serpkite.com\n  timeoutMs: 60_000, // per attempt\n  maxRetries: 2, // retries after the first attempt\n  retryDelayMs: 500, // base of the exponential backoff\n  fetch: customFetch, // default: globalThis.fetch\n  headers: { \"X-Request-Id\": \"my-trace-id\" }, // sent with every request\n});\n```\n\nThe constructor throws a `SerpKiteError` with code `missing_api_key` when no key is found. On\nruntimes without `process.env` (browsers, some edge platforms) pass `apiKey` explicitly. Don't\nship a secret key to a public web page.\n\nEvery method also takes a second `options` argument:\n\n```ts\nawait sk.search(\n  { q: \"espresso\" },\n  {\n    signal: AbortSignal.timeout(10_000),\n    timeoutMs: 20_000,\n    maxRetries: 0,\n    headers: { \"X-Request-Id\": \"trace-123\" },\n    onResponse: (info) => console.log(info.creditsUsed, info.creditsRemaining, info.cache),\n  },\n);\n```\n\n`onResponse` receives the billing headers of the successful response: `requestId`, `creditsUsed`,\n`creditsRemaining`, `costUsd`, `cache` (`HIT` | `MISS`), `latencyMs`, `tokensEstimate`, plus the raw\n`headers` and `status`.\n\n## Methods\n\nAll request and response fields are snake_case, exactly as in the HTTP API. Every vertical returns\nthe same envelope: `request` (the normalised request), `results` (the vertical's primary list),\nvertical-specific extras and `meta` (`request_id`, `credits_used`, `cached`, `latency_ms`, …).\n\n| Method | Endpoint | Params | Returns |\n| --- | --- | --- | --- |\n| `search(params)` | `POST /v1/search` | `SearchParams` | `SearchResponse` (results, `answer_box`, `knowledge_graph`, `people_also_ask`, `related_searches`, `top_stories`, `places`, `ads`) |\n| `images(params)` | `POST /v1/images` | `SearchParams` | `ImagesResponse` |\n| `videos(params)` | `POST /v1/videos` | `SearchParams` | `VideosResponse` |\n| `news(params)` | `POST /v1/news` | `SearchParams` | `NewsResponse` |\n| `maps(params)` | `POST /v1/maps` | `SearchParams` (+ `ll`: `\"@lat,lng,14z\"`) | `PlacesResponse` |\n| `places(params)` | `POST /v1/places` | `SearchParams` | `PlacesResponse` |\n| `reviews(params)` | `POST /v1/reviews` | `ReviewsParams` (`place_id` \\| `cid` \\| `fid`, `sort`, `page_token`, `num` ≤ 50) | `ReviewsResponse` (+ `next_page_token`) |\n| `shopping(params)` | `POST /v1/shopping` | `SearchParams` | `ShoppingResponse` |\n| `scholar(params)` | `POST /v1/scholar` | `SearchParams` | `ScholarResponse` |\n| `patents(params)` | `POST /v1/patents` | `SearchParams` | `PatentsResponse` |\n| `autocomplete(params)` | `POST /v1/autocomplete` | `SearchParams` | `AutocompleteResponse` (`results[].value`) |\n| `webpage(params)` | `POST /v1/webpage` | `WebpageParams` (`url`, `include_html`) | `WebpageResponse` (`markdown`, `text`, `metadata`) |\n| `rank(params)` | `POST /v1/rank` | `RankParams` (`q`, `domain`, `num`: 10\\|20\\|30\\|50\\|100) | `RankResponse` (`position` or `null`, `matches`, `checked`) |\n| `extract(params)` | `POST /v1/extract` | `ExtractParams` (`urls` ≤ 20, `format`, `query`, `highlights`, …) | `ExtractResponse` (`results`, `failed`) |\n| `map(params)` | `POST /v1/map` | `MapParams` (`url`, `search`, `limit`, `sitemap`, path filters) | `MapResponse` (`results[].url`) |\n| `crawl(params)` | `POST /v1/crawl` | `CrawlParams` (`url`, `limit` ≤ 1000, `max_depth` ≤ 10, …) | `TaskCreated` (`202`) |\n| `getCrawl(id)` / `cancelCrawl(id)` | `GET` / `DELETE /v1/crawl/{id}` | task id | `CrawlTask` / `TaskCancelResponse` |\n| `waitForCrawl(idOrTask, options?)` | polls `GET /v1/crawl/{id}` | task id or task | `CrawlTask` (`completed`, `failed` or `canceled`) |\n| `monitors.create/list/get/update/delete/run` | `/v1/monitors…` | `MonitorCreateParams`, `MonitorUpdateParams` | `Monitor`, `MonitorList` |\n| `monitors.runs(id, params?)` / `monitors.iterRuns(id)` | `GET /v1/monitors/{id}/runs` | `limit`, `before` | `MonitorRunList` / `AsyncIterable<MonitorRun>` |\n| `account()` | `GET /v1/account` | none | `Account` (`balance`, `plan`, `rate_limit_rps`, `month`, …) |\n| `batches.create(params)` | `POST /v1/batches` | `BatchCreateParams` | `BatchCreateResponse` |\n| `batches.get(id)` | `GET /v1/batches/{id}` | batch id | `Batch` |\n| `batches.wait(id, options?)` | polls `GET /v1/batches/{id}` | batch id or entry | `Batch` (`done` or `failed`) |\n\nCommon `SearchParams`: `q` (required), `country` (default `us`), `language` (default `en`),\n`location`, `uule`, `num` (10, or 100 for the depth bundle), `page` (1-10), `time`\n(`hour`|`day`|`week`|`month`|`year`), `tbs`, `device` (`desktop`|`mobile`), `safe`, `autocorrect`,\n`format` (`json`|`markdown`|`compact`), `fields`, `include_content` (0-5), `ads`, `max_age`,\n`engine`.\n\n### Examples\n\n```ts\n// News from the last day in Germany\nconst news = await sk.news({ q: \"EZB Zinsen\", country: \"de\", language: \"de\", time: \"day\" });\n\n// Top 3 organic pages fetched as Markdown (+1 credit per page)\nconst deep = await sk.search({ q: \"rust async runtime comparison\", include_content: 3 });\nconsole.log(deep.results[0].content);\n\n// Only the fields you need\nconst lean = await sk.search({ q: \"espresso\", fields: \"results.title,results.link,answer_box\" });\n\n// Token-lean JSON for agents\nconst compact = await sk.search({ q: \"espresso\", format: \"compact\" });\n\n// Reviews, paged\nlet page = await sk.reviews({ place_id: \"ChIJN1t_tDeuEmsRUsoyG83frY4\", sort: \"newest\" });\nwhile (page.next_page_token) {\n  page = await sk.reviews({ place_id: \"ChIJN1t_tDeuEmsRUsoyG83frY4\", page_token: page.next_page_token });\n}\n\n// Any URL as Markdown\nconst doc = await sk.webpage({ url: \"https://example.com/blog/post\" });\nconsole.log(doc.metadata.title, doc.markdown);\n\n// Where does a domain rank?\nconst r = await sk.rank({ q: \"espresso machine\", domain: \"example.com\" });\nconsole.log(r.position ?? \"not in top 100\");\n\n// Account balance\nconst { balance, month } = await sk.account();\n\n// Cached result up to one hour old (half price on a hit)\nawait sk.search({ q: \"espresso\", max_age: 3600 });\n```\n\n`format: \"markdown\"` resolves to a `string` for every vertical that supports it; `format:\n\"compact\"` resolves to a `CompactResponse`. The return type follows the literal you pass. With\n`fields` the response contains only the requested keys, so treat the typed fields as optional.\n\n## Search controls\n\nDomain filters and date ranges work on search, news, images and videos; `boost_domains` on search\nand news; `highlights` on search with `include_content`. None of them costs extra credits.\n\n```ts\nconst res = await sk.search({\n  q: \"connection pooling\",\n  include_domains: [\"postgresql.org\", \"github.com/pgbouncer\", \".edu\"], // host, path prefix or TLD (≤ 20)\n  exclude_domains: [\"pinterest.com\"],\n  boost_domains: [\"postgresql.org\"], // to the top, keeping the rest\n  start_date: \"2026-01-01\", // YYYY-MM-DD, end_date too\n  include_content: 3,\n  highlights: true, // 3 query-ranked passages per page instead of the whole page\n});\nfor (const r of res.results) console.log(r.position, r.published_at, r.highlights?.[0]?.text);\n```\n\n## Search engines & fallback\n\nBy default every request is answered by Google only (`engine: \"google\"`); SerpKite already\nfails over across its own proxy pools. Opt in to other providers with `engine`:\n\n```ts\n// Fall back to other enabled providers when Google is blocked or times out\nconst res = await sk.search({ q: \"best espresso machine\", engine: \"auto\" });\nconsole.log(res.meta.engine); // \"google\", or e.g. \"brave\" if Google was unavailable\nconsole.log(res.meta.route); // [{ provider: \"google\", outcome: \"blocked\", ms: 812 }, { provider: \"brave\", outcome: \"ok\", ms: 431 }]\n\n// Only these providers, in this order\nawait sk.news({ q: \"espresso\", engine: [\"google\", \"brave\"] });\n```\n\n- `engine` is `\"google\"` (default), `\"auto\"`, `\"consensus\"`, one `Provider` (`\"brave\"`, `\"bing\"`,\n  `\"yahoo\"`, `\"duckduckgo\"`, `\"mojeek\"`, `\"wikipedia\"`) or a `Provider[]` list. `\"auto\"` and `\"consensus\"` can't\n  be combined with other names; unknown names, or a provider that doesn't serve the endpoint, return\n  `400 invalid_request`.\n- `engine: \"consensus\"` (`search` only) asks several independent indexes in parallel, merges the\n  results by URL and ranks them by agreement: each result has `sources` (e.g.\n  `[\"google\", \"brave\"]`), `meta.engine` is `\"consensus\"`, and it costs the sum of one page per\n  provider that returned results.\n- `meta.engine` names the provider that answered; `meta.route` (`RouteStep[]`) lists each attempt\n  and its `outcome` (absent on cache hits). `request.engine` echoes what you asked for.\n- Credits (`meta.credits_used`, `X-Credits-Used`) follow the answering provider's price.\n\n## Map and extract\n\n```ts\n// The URLs of a site (1 credit): sitemaps + start page, canonicalised and deduplicated\nconst site = await sk.map({ url: \"https://docs.example.com/\", search: \"install\", include_paths: [\"^/guides/\"] });\nfor (const u of site.results) console.log(u.url);\n\n// Up to 20 URLs (HTML or PDF) as Markdown in one call: 1 credit per URL that came back\nconst pages = await sk.extract({ urls: site.results.slice(0, 5).map((u) => u.url), query: \"install\", highlights: 3 });\nfor (const f of pages.failed) console.log(\"failed\", f.url, f.error.code); // not charged\n```\n\n## Crawl\n\n```ts\nconst task = await sk.crawl({\n  url: \"https://docs.example.com/\",\n  limit: 200, // up to 1000 pages; max_depth up to 10\n  include_paths: [\"^/guides/\"],\n  sitemap: \"include\", // include (default) | only | skip\n  query: \"authentication\", // read the most relevant pages first\n});\nconst done = await sk.waitForCrawl(task); // polls until completed/failed/canceled\nfor (const p of done.result?.pages ?? []) console.log(p.url, p.markdown?.length);\nconsole.log(done.result?.stats.stopped); // done | limit | time_limit | size_limit | too_many_failures | canceled\n```\n\n- `crawl` reserves `limit` credits and charges 1 per page read (0.5 from cache); the rest is\n  refunded. A crawl that reads nothing fails (`no_pages`) and costs nothing.\n- It returns `202` with a task; `waitForCrawl` takes `{ timeoutMs, pollIntervalMs, maxPollIntervalMs,\n  signal }` like `batches.wait` (default timeout 35 minutes: a crawl runs for up to 30) and\n  returns failed or canceled tasks instead of throwing.\n- `cancelCrawl(id)` refunds a queued crawl; a running one stops at its next checkpoint\n  (`status: \"canceling\"`) and is charged for the pages read.\n- Robots.txt (per host) and Crawl-delay are honoured; `result.stats` reports `discovered`, `queued`,\n  `duplicates`, `sitemap_urls`, `robots`, `robots_blocked` and why it `stopped`.\n- Pass `webhook_url` to get a signed `crawl.completed` delivery instead of polling. A result over\n  4 MB arrives as `result: null` with `result_omitted: true`; fetch it from `poll_url`.\n\n## Monitors\n\n```ts\nconst mon = await sk.monitors.create({\n  q: \"ai agents\",\n  endpoint: \"news\",\n  interval: \"hourly\", // hourly | daily | weekly, or interval_seconds (3600-2592000)\n  webhook_url: \"https://example.com/hooks/serpkite\",\n});\nawait sk.monitors.run(mon.id); // due within ~30 s\nawait sk.monitors.update(mon.id, { active: false });\nawait sk.monitors.update(mon.id, { q: \"ai agents frameworks\", num: 20 }); // change the saved search\nconst { results } = await sk.monitors.list();\n\n// Run history, newest first (new results kept 24 h): one page, or every run\nconst page = await sk.monitors.runs(mon.id, { limit: 20 }); // page.next_before → { before }\nfor await (const run of sk.monitors.iterRuns(mon.id)) console.log(run.status, run.new_results);\nawait sk.monitors.delete(mon.id);\n```\n\nA `webpage` monitor watches one page instead of a search: it checks `url` each interval (1 credit\nper check) and reports it as a `MonitorPageChange` (`url`, `title`, `change: \"new\" | \"changed\"`,\n`content_hash`, `markdown`) when its content changes. `metadata` (your own JSON object, ≤ 2 KB) is\nstored on any monitor and echoed in its `monitor.results` webhooks; on `update`, an object replaces it\nand `null` clears it.\n\n```ts\nconst watch = await sk.monitors.create({\n  endpoint: \"webpage\",\n  url: \"https://example.com/pricing\",\n  interval: \"daily\",\n  metadata: { customer: \"acme\" },\n});\nawait sk.monitors.update(watch.id, { metadata: null });\n```\n\nEach search run costs what its search costs (1 credit per 10 results, `num` 100 is 7; empty and failed runs are free) and POSTs only results it\nhasn't seen before (among the top `num`) as a `monitor.results` webhook. `webhook_url` is optional:\nwithout one, read new results from `monitors.runs`. `active: false` creates a monitor paused; after 10\nfailed runs in a row it pauses itself (`last_status: \"paused\"`) and `update(id, { active: true })`\nresumes it. `webhook_url: \"\"` removes the webhook; a changed search reports every result as new once.\n\n## Webhooks\n\nEvery delivery (`batch.completed`, `crawl.completed`, `monitor.results`) is signed with\n`X-SerpKite-Signature`. `parseWebhook` verifies it and returns the event typed by\n`X-SerpKite-Event`, with `deliveryId` from `X-SerpKite-Delivery` (for `monitor.results`, the\n`run_id`) to deduplicate retries:\n\n```ts\nimport { parseWebhook } from \"serpkite\";\n\nconst ev = await parseWebhook(process.env.SERPKITE_WEBHOOK_SECRET!, rawBody, req.headers); // throws invalid_signature\nif (ev.type === \"monitor.results\") console.log(ev.data.metadata, ev.data.url, ev.data.new_results.length);\nif (ev.type === \"crawl.completed\") console.log(ev.data.status, ev.data.poll_url);\n```\n\n`verifyWebhook(secret, rawBody, headers)` only checks the signature and returns a boolean.\n\n## Batches\n\nBatch jobs cost half price. Submit 1-100 requests for one endpoint; each request becomes a job.\n\n```ts\nimport { isBatchError, SerpKite } from \"serpkite\";\n\nconst sk = new SerpKite();\nconst { batches } = await sk.batches.create({\n  endpoint: \"search\", // or images, news, maps, reviews, webpage, …\n  requests: [{ q: \"a\" }, { q: \"b\" }],\n  webhook_url: \"https://example.com/hooks/serpkite\", // optional\n});\n\nconst done = await sk.batches.wait(batches[0].id); // polls until done/failed\nconsole.log(done.status, done.result);\n\n// Wait for all queued jobs; skip entries that were rejected at submit time\nconst jobs = await Promise.all(batches.filter((b) => !isBatchError(b)).map((b) => sk.batches.wait(b)));\n```\n\n- `create` returns one entry per request, in order: a queued `Batch` or an error object\n  (`{ error: { code, message, request_id } }`). Use `isBatchError(entry)` to tell them apart.\n  Passing a rejected entry to `wait` throws its error.\n- `wait(idOrEntry, { timeoutMs = 600000, pollIntervalMs = 1000, maxPollIntervalMs = 10000, signal })`\n  polls with a growing interval. It returns failed jobs (check `status` and `error`) and throws a\n  `SerpKiteError` with code `timeout` when the deadline passes.\n- Pass `{ idempotencyKey }` as the second argument to `create` (e.g. a UUID you store with the batch)\n  to make it safe to retry: the server replays the first response for the same key and body for\n  24 hours, and the SDK then also retries 5xx and network errors.\n- Results are kept for 24 hours. Webhooks are always signed with `X-SerpKite-Signature` (HMAC-SHA256\n  of `<X-SerpKite-Timestamp>.<body>` with your webhook secret). Check a delivery with\n  `await verifyWebhook(secret, rawBody, req.headers)`.\n\n## Errors\n\nEvery failure is a `SerpKiteError`:\n\n```ts\nimport { SerpKite, SerpKiteError } from \"serpkite\";\n\ntry {\n  await sk.search({ q: \"espresso\" });\n} catch (err) {\n  if (err instanceof SerpKiteError) {\n    console.log(err.status, err.code, err.message, err.requestId);\n  }\n}\n```\n\n| `code` | `status` | Meaning |\n| --- | --- | --- |\n| `invalid_request` | 400 | Bad or unknown parameter |\n| `unauthorized` | 401 | Missing or invalid API key |\n| `insufficient_credits` | 402 | Balance too low |\n| `spend_cap_reached`, `key_limit_reached` | 403 | Account spend cap or per-key monthly limit hit |\n| `forbidden` | 403 | Not allowed |\n| `not_found` | 404 | Unknown batch id |\n| `rate_limited` | 429 | Too many requests (retried automatically) |\n| `upstream_error`, `upstream_blocked` | 503 | Google could not be fetched (not billed, retried) |\n| `upstream_timeout` | 503 | The search took too long (not billed, retried) |\n| `unavailable` | 503 | Temporarily unavailable (retried) |\n| `internal` | 500 | Server error (retried) |\n| `connection_error`, `timeout` | 0 | No response received |\n| `missing_api_key` | 0 | No key passed and `SERPKITE_API_KEY` unset |\n\nFailed, empty and blocked searches are refunded, so they never cost credits.\n\n## Retries\n\nRequests are retried up to `maxRetries` times (default 2) on `429`, `5xx`, network errors and\ntimeouts, with exponential backoff (`retryDelayMs · 2^attempt`, capped at 8 s) and jitter. A\n`Retry-After` header (seconds or HTTP date, capped at 60 s) takes precedence. Other `4xx` errors\nare never retried. `batches.create`, `crawl`, `monitors.create` and `monitors.run` retry only on\n`429`, so a lost response can never queue (and bill) the same work twice.\n\n## Types\n\nTypes are generated from the OpenAPI contract (`backend/api/serp-api.yaml`) with\n`openapi-typescript`. Friendly aliases are exported: `SearchParams`, `SearchResponse`,\n`OrganicResult`, `AnswerBox`, `NewsResponse`, `NewsResult`, `ImagesResponse`, `PlacesResponse`,\n`ReviewsResponse`, `ShoppingResponse`, `ScholarResponse`, `PatentsResponse`,\n`AutocompleteResponse`, `WebpageResponse`, `RankResponse`,\n`Account`, `Batch`, `BatchCreateParams`, `Meta`, `Engine`, `Provider`, `RouteStep`,\n`CompactResponse`, `MapResponse`, `ExtractResponse`, `CrawlParams`, `CrawlTask`, `CrawlResult`,\n`TaskCreated`, `Monitor`, `MonitorRun`, `MonitorPageChange`, `TaskCompletedEvent`,\n`MonitorResultsEvent`, `WebhookEvent` and more. The raw\n`paths`, `components` and `operations` types are exported as well.\n\n## Development\n\n```bash\nbun install          # from the repo root\nbun run gen          # regenerate src/generated/openapi.ts from backend/api/serp-api.yaml\nbun run typecheck\nbun test\nbun run build        # dist/: ESM, CJS and .d.ts\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}