serpkite 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/dist/index.cjs +420 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2259 -0
- package/dist/index.d.ts +2259 -0
- package/dist/index.js +412 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SerpKite
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,276 @@
|
|
|
1
|
-
#
|
|
1
|
+
# serpkite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official TypeScript/JavaScript SDK for [SerpKite](https://serpkite.com), the Google search API built
|
|
4
|
+
for AI agents: clean JSON or Markdown, credits that never expire.
|
|
5
|
+
|
|
6
|
+
- Zero runtime dependencies: uses the global `fetch`.
|
|
7
|
+
- Works in Node 18+, Bun, Deno, Cloudflare Workers and other edge runtimes.
|
|
8
|
+
- ESM and CommonJS builds with full type definitions, generated from the OpenAPI contract.
|
|
9
|
+
- Retries with exponential backoff and jitter on `429`, `5xx` and network errors (honours `Retry-After`).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install serpkite
|
|
15
|
+
# or: bun add serpkite / pnpm add serpkite / yarn add serpkite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Deno: `import { SerpKite } from "npm:serpkite";`
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { SerpKite } from "serpkite";
|
|
24
|
+
|
|
25
|
+
const sk = new SerpKite(); // reads SERPKITE_API_KEY
|
|
26
|
+
const res = await sk.search({ q: "best espresso machine", country: "us" });
|
|
27
|
+
console.log(res.results[0].title, res.meta.credits_used);
|
|
28
|
+
|
|
29
|
+
// Markdown for an LLM prompt: `format: "markdown"` resolves to a string.
|
|
30
|
+
const md = await sk.search({ q: "best espresso machine", format: "markdown" });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Get an API key at [app.serpkite.com](https://app.serpkite.com) and export it:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export SERPKITE_API_KEY=skt_live_...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const sk = new SerpKite({
|
|
43
|
+
apiKey: "skt_live_...", // default: process.env.SERPKITE_API_KEY
|
|
44
|
+
baseUrl: "https://api.serpkite.com", // default: SERPKITE_BASE_URL or https://api.serpkite.com
|
|
45
|
+
timeoutMs: 60_000, // per attempt
|
|
46
|
+
maxRetries: 2, // retries after the first attempt
|
|
47
|
+
retryDelayMs: 500, // base of the exponential backoff
|
|
48
|
+
fetch: customFetch, // default: globalThis.fetch
|
|
49
|
+
headers: { "X-Request-Id": "my-trace-id" }, // sent with every request
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The constructor throws a `SerpKiteError` with code `missing_api_key` when no key is found. On
|
|
54
|
+
runtimes without `process.env` (browsers, some edge platforms) pass `apiKey` explicitly. Don't
|
|
55
|
+
ship a secret key to a public web page.
|
|
56
|
+
|
|
57
|
+
Every method also takes a second `options` argument:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
await sk.search(
|
|
61
|
+
{ q: "espresso" },
|
|
62
|
+
{
|
|
63
|
+
signal: AbortSignal.timeout(10_000),
|
|
64
|
+
timeoutMs: 20_000,
|
|
65
|
+
maxRetries: 0,
|
|
66
|
+
headers: { "X-Request-Id": "trace-123" },
|
|
67
|
+
onResponse: (info) => console.log(info.creditsUsed, info.creditsRemaining, info.cache),
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`onResponse` receives the billing headers of the successful response: `requestId`, `creditsUsed`,
|
|
73
|
+
`creditsRemaining`, `costUsd`, `cache` (`HIT` | `MISS`), `latencyMs`, `tokensEstimate`, plus the raw
|
|
74
|
+
`headers` and `status`.
|
|
75
|
+
|
|
76
|
+
## Methods
|
|
77
|
+
|
|
78
|
+
All request and response fields are snake_case, exactly as in the HTTP API. Every vertical returns
|
|
79
|
+
the same envelope: `request` (the normalised request), `results` (the vertical's primary list),
|
|
80
|
+
vertical-specific extras and `meta` (`request_id`, `credits_used`, `cached`, `latency_ms`, …).
|
|
81
|
+
|
|
82
|
+
| Method | Endpoint | Params | Returns |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| `search(params)` | `POST /v1/search` | `SearchParams` | `SearchResponse` (results, `answer_box`, `knowledge_graph`, `people_also_ask`, `related_searches`, `top_stories`, `places`, `ads`) |
|
|
85
|
+
| `images(params)` | `POST /v1/images` | `SearchParams` | `ImagesResponse` |
|
|
86
|
+
| `videos(params)` | `POST /v1/videos` | `SearchParams` | `VideosResponse` |
|
|
87
|
+
| `news(params)` | `POST /v1/news` | `SearchParams` | `NewsResponse` |
|
|
88
|
+
| `maps(params)` | `POST /v1/maps` | `SearchParams` (+ `ll`: `"@lat,lng,14z"`) | `PlacesResponse` |
|
|
89
|
+
| `places(params)` | `POST /v1/places` | `SearchParams` | `PlacesResponse` |
|
|
90
|
+
| `reviews(params)` | `POST /v1/reviews` | `ReviewsParams` (`place_id` \| `cid` \| `fid`, `sort`, `page_token`, `num` ≤ 50) | `ReviewsResponse` (+ `next_page_token`) |
|
|
91
|
+
| `shopping(params)` | `POST /v1/shopping` | `SearchParams` | `ShoppingResponse` |
|
|
92
|
+
| `scholar(params)` | `POST /v1/scholar` | `SearchParams` | `ScholarResponse` |
|
|
93
|
+
| `patents(params)` | `POST /v1/patents` | `SearchParams` | `PatentsResponse` |
|
|
94
|
+
| `autocomplete(params)` | `POST /v1/autocomplete` | `SearchParams` | `AutocompleteResponse` (`results[].value`) |
|
|
95
|
+
| `lens(params)` | `POST /v1/lens` | `LensParams` (`url` of an image) | `LensResponse` |
|
|
96
|
+
| `webpage(params)` | `POST /v1/webpage` | `WebpageParams` (`url`, `include_html`) | `WebpageResponse` (`markdown`, `text`, `metadata`) |
|
|
97
|
+
| `rank(params)` | `POST /v1/rank` | `RankParams` (`q`, `domain`, `num`: 10\|20\|30\|50\|100) | `RankResponse` (`position` or `null`, `matches`, `checked`) |
|
|
98
|
+
| `account()` | `GET /v1/account` | none | `Account` (`balance`, `plan`, `rate_limit_rps`, `month`, …) |
|
|
99
|
+
| `batches.create(params)` | `POST /v1/batches` | `BatchCreateParams` | `BatchCreateResponse` |
|
|
100
|
+
| `batches.get(id)` | `GET /v1/batches/{id}` | batch id | `Batch` |
|
|
101
|
+
| `batches.wait(id, options?)` | polls `GET /v1/batches/{id}` | batch id or entry | `Batch` (`done` or `failed`) |
|
|
102
|
+
|
|
103
|
+
Common `SearchParams`: `q` (required), `country` (default `us`), `language` (default `en`),
|
|
104
|
+
`location`, `uule`, `num` (10, or 100 for the depth bundle), `page` (1-10), `time`
|
|
105
|
+
(`hour`|`day`|`week`|`month`|`year`), `tbs`, `device` (`desktop`|`mobile`), `safe`, `autocorrect`,
|
|
106
|
+
`format` (`json`|`markdown`|`compact`), `fields`, `include_content` (0-5), `ads`, `max_age`,
|
|
107
|
+
`engine`.
|
|
108
|
+
|
|
109
|
+
### Examples
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// News from the last day in Germany
|
|
113
|
+
const news = await sk.news({ q: "EZB Zinsen", country: "de", language: "de", time: "day" });
|
|
114
|
+
|
|
115
|
+
// Top 3 organic pages fetched as Markdown (+1 credit per page)
|
|
116
|
+
const deep = await sk.search({ q: "rust async runtime comparison", include_content: 3 });
|
|
117
|
+
console.log(deep.results[0].content);
|
|
118
|
+
|
|
119
|
+
// Only the fields you need
|
|
120
|
+
const lean = await sk.search({ q: "espresso", fields: "results.title,results.link,answer_box" });
|
|
121
|
+
|
|
122
|
+
// Token-lean JSON for agents
|
|
123
|
+
const compact = await sk.search({ q: "espresso", format: "compact" });
|
|
124
|
+
|
|
125
|
+
// Reviews, paged
|
|
126
|
+
let page = await sk.reviews({ place_id: "ChIJN1t_tDeuEmsRUsoyG83frY4", sort: "newest" });
|
|
127
|
+
while (page.next_page_token) {
|
|
128
|
+
page = await sk.reviews({ place_id: "ChIJN1t_tDeuEmsRUsoyG83frY4", page_token: page.next_page_token });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Any URL as Markdown
|
|
132
|
+
const doc = await sk.webpage({ url: "https://example.com/blog/post" });
|
|
133
|
+
console.log(doc.metadata.title, doc.markdown);
|
|
134
|
+
|
|
135
|
+
// Where does a domain rank?
|
|
136
|
+
const r = await sk.rank({ q: "espresso machine", domain: "example.com" });
|
|
137
|
+
console.log(r.position ?? "not in top 100");
|
|
138
|
+
|
|
139
|
+
// Account balance
|
|
140
|
+
const { balance, month } = await sk.account();
|
|
141
|
+
|
|
142
|
+
// Cached result up to one hour old (half price on a hit)
|
|
143
|
+
await sk.search({ q: "espresso", max_age: 3600 });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`format: "markdown"` resolves to a `string` for every vertical that supports it; `format:
|
|
147
|
+
"compact"` resolves to a `CompactResponse`. The return type follows the literal you pass. With
|
|
148
|
+
`fields` the response contains only the requested keys, so treat the typed fields as optional.
|
|
149
|
+
|
|
150
|
+
## Search engines & fallback
|
|
151
|
+
|
|
152
|
+
By default every request is answered by Google only (`engine: "google"`); SerpKite already
|
|
153
|
+
fails over across its own proxy pools. Opt in to other providers with `engine`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// Fall back to other enabled providers when Google is blocked or times out
|
|
157
|
+
const res = await sk.search({ q: "best espresso machine", engine: "auto" });
|
|
158
|
+
console.log(res.meta.engine); // "google", or e.g. "brave" if Google was unavailable
|
|
159
|
+
console.log(res.meta.route); // [{ provider: "google", outcome: "blocked", ms: 812 }, { provider: "brave", outcome: "ok", ms: 431 }]
|
|
160
|
+
|
|
161
|
+
// Only these providers, in this order
|
|
162
|
+
await sk.news({ q: "espresso", engine: ["google", "brave"] });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `engine` is `"google"` (default), `"auto"`, `"consensus"`, one `Provider` (`"brave"`, `"bing"`,
|
|
166
|
+
`"yahoo"`, `"duckduckgo"`, `"mojeek"`, `"wikipedia"`) or a `Provider[]` list. `"auto"` and `"consensus"` can't
|
|
167
|
+
be combined with other names; unknown names, or a provider that doesn't serve the endpoint, return
|
|
168
|
+
`400 invalid_request`.
|
|
169
|
+
- `engine: "consensus"` (`search` only) asks several independent indexes in parallel, merges the
|
|
170
|
+
results by URL and ranks them by agreement: each result has `sources` (e.g.
|
|
171
|
+
`["google", "brave"]`), `meta.engine` is `"consensus"`, and it costs the sum of one page per
|
|
172
|
+
provider that returned results.
|
|
173
|
+
- `meta.engine` names the provider that answered; `meta.route` (`RouteStep[]`) lists each attempt
|
|
174
|
+
and its `outcome` (absent on cache hits). `request.engine` echoes what you asked for.
|
|
175
|
+
- Credits (`meta.credits_used`, `X-Credits-Used`) follow the answering provider's price.
|
|
176
|
+
|
|
177
|
+
## Batches
|
|
178
|
+
|
|
179
|
+
Batch jobs cost half price. Submit 1-100 requests for one endpoint; each request becomes a job.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { isBatchError, SerpKite } from "serpkite";
|
|
183
|
+
|
|
184
|
+
const sk = new SerpKite();
|
|
185
|
+
const { batches } = await sk.batches.create({
|
|
186
|
+
endpoint: "search", // or images, news, maps, reviews, webpage, …
|
|
187
|
+
requests: [{ q: "a" }, { q: "b" }],
|
|
188
|
+
webhook_url: "https://example.com/hooks/serpkite", // optional
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const done = await sk.batches.wait(batches[0].id); // polls until done/failed
|
|
192
|
+
console.log(done.status, done.result);
|
|
193
|
+
|
|
194
|
+
// Wait for all queued jobs; skip entries that were rejected at submit time
|
|
195
|
+
const jobs = await Promise.all(batches.filter((b) => !isBatchError(b)).map((b) => sk.batches.wait(b)));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- `create` returns one entry per request, in order: a queued `Batch` or an error object
|
|
199
|
+
(`{ error: { code, message, request_id } }`). Use `isBatchError(entry)` to tell them apart.
|
|
200
|
+
Passing a rejected entry to `wait` throws its error.
|
|
201
|
+
- `wait(idOrEntry, { timeoutMs = 600000, pollIntervalMs = 1000, maxPollIntervalMs = 10000, signal })`
|
|
202
|
+
polls with a growing interval. It returns failed jobs (check `status` and `error`) and throws a
|
|
203
|
+
`SerpKiteError` with code `timeout` when the deadline passes.
|
|
204
|
+
- Pass `{ idempotencyKey }` as the second argument to `create` (e.g. a UUID you store with the batch)
|
|
205
|
+
to make it safe to retry: the server replays the first response for the same key and body for
|
|
206
|
+
24 hours, and the SDK then also retries 5xx and network errors.
|
|
207
|
+
- Results are kept for 24 hours. Webhooks are always signed with `X-SerpKite-Signature` (HMAC-SHA256
|
|
208
|
+
of `<X-SerpKite-Timestamp>.<body>` with your webhook secret). Check a delivery with
|
|
209
|
+
`await verifyWebhook(secret, rawBody, req.headers)`.
|
|
210
|
+
|
|
211
|
+
## Errors
|
|
212
|
+
|
|
213
|
+
Every failure is a `SerpKiteError`:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { SerpKite, SerpKiteError } from "serpkite";
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await sk.search({ q: "espresso" });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof SerpKiteError) {
|
|
222
|
+
console.log(err.status, err.code, err.message, err.requestId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| `code` | `status` | Meaning |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| `invalid_request` | 400 | Bad or unknown parameter |
|
|
230
|
+
| `unauthorized` | 401 | Missing or invalid API key |
|
|
231
|
+
| `insufficient_credits` | 402 | Balance too low |
|
|
232
|
+
| `spend_cap_reached`, `key_limit_reached` | 403 | Account spend cap or per-key monthly limit hit |
|
|
233
|
+
| `forbidden` | 403 | Not allowed |
|
|
234
|
+
| `not_found` | 404 | Unknown batch id |
|
|
235
|
+
| `rate_limited` | 429 | Too many requests (retried automatically) |
|
|
236
|
+
| `upstream_error`, `upstream_blocked` | 503 | Google could not be fetched (not billed, retried) |
|
|
237
|
+
| `upstream_timeout` | 503 | The search took too long (not billed, retried) |
|
|
238
|
+
| `unavailable` | 503 | Temporarily unavailable (retried) |
|
|
239
|
+
| `internal` | 500 | Server error (retried) |
|
|
240
|
+
| `connection_error`, `timeout` | 0 | No response received |
|
|
241
|
+
| `missing_api_key` | 0 | No key passed and `SERPKITE_API_KEY` unset |
|
|
242
|
+
|
|
243
|
+
Failed, empty and blocked searches are refunded, so they never cost credits.
|
|
244
|
+
|
|
245
|
+
## Retries
|
|
246
|
+
|
|
247
|
+
Requests are retried up to `maxRetries` times (default 2) on `429`, `5xx`, network errors and
|
|
248
|
+
timeouts, with exponential backoff (`retryDelayMs · 2^attempt`, capped at 8 s) and jitter. A
|
|
249
|
+
`Retry-After` header (seconds or HTTP date, capped at 60 s) takes precedence. Other `4xx` errors
|
|
250
|
+
are never retried. `batches.create` retries only on `429`, so a lost response can never queue
|
|
251
|
+
(and bill) the same requests twice.
|
|
252
|
+
|
|
253
|
+
## Types
|
|
254
|
+
|
|
255
|
+
Types are generated from the OpenAPI contract (`backend/api/serp-api.yaml`) with
|
|
256
|
+
`openapi-typescript`. Friendly aliases are exported: `SearchParams`, `SearchResponse`,
|
|
257
|
+
`OrganicResult`, `AnswerBox`, `NewsResponse`, `NewsResult`, `ImagesResponse`, `PlacesResponse`,
|
|
258
|
+
`ReviewsResponse`, `ShoppingResponse`, `ScholarResponse`, `PatentsResponse`,
|
|
259
|
+
`AutocompleteResponse`, `LensResponse`, `WebpageResponse`, `RankResponse`,
|
|
260
|
+
`Account`, `Batch`, `BatchCreateParams`, `Meta`, `Engine`, `Provider`, `RouteStep`,
|
|
261
|
+
`CompactResponse` and more. The raw
|
|
262
|
+
`paths`, `components` and `operations` types are exported as well.
|
|
263
|
+
|
|
264
|
+
## Development
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
bun install # from the repo root
|
|
268
|
+
bun run gen # regenerate src/generated/openapi.ts from backend/api/serp-api.yaml
|
|
269
|
+
bun run typecheck
|
|
270
|
+
bun test
|
|
271
|
+
bun run build # dist/: ESM, CJS and .d.ts
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT
|