serpkite 0.2.0 → 0.4.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/README.md CHANGED
@@ -92,9 +92,15 @@ vertical-specific extras and `meta` (`request_id`, `credits_used`, `cached`, `la
92
92
  | `scholar(params)` | `POST /v1/scholar` | `SearchParams` | `ScholarResponse` |
93
93
  | `patents(params)` | `POST /v1/patents` | `SearchParams` | `PatentsResponse` |
94
94
  | `autocomplete(params)` | `POST /v1/autocomplete` | `SearchParams` | `AutocompleteResponse` (`results[].value`) |
95
- | `lens(params)` | `POST /v1/lens` | `LensParams` (`url` of an image) | `LensResponse` |
96
95
  | `webpage(params)` | `POST /v1/webpage` | `WebpageParams` (`url`, `include_html`) | `WebpageResponse` (`markdown`, `text`, `metadata`) |
97
96
  | `rank(params)` | `POST /v1/rank` | `RankParams` (`q`, `domain`, `num`: 10\|20\|30\|50\|100) | `RankResponse` (`position` or `null`, `matches`, `checked`) |
97
+ | `extract(params)` | `POST /v1/extract` | `ExtractParams` (`urls` ≤ 20, `format`, `query`, `highlights`, …) | `ExtractResponse` (`results`, `failed`) |
98
+ | `map(params)` | `POST /v1/map` | `MapParams` (`url`, `search`, `limit`, `sitemap`, path filters) | `MapResponse` (`results[].url`) |
99
+ | `crawl(params)` | `POST /v1/crawl` | `CrawlParams` (`url`, `limit` ≤ 1000, `max_depth` ≤ 10, …) | `TaskCreated` (`202`) |
100
+ | `getCrawl(id)` / `cancelCrawl(id)` | `GET` / `DELETE /v1/crawl/{id}` | task id | `CrawlTask` / `TaskCancelResponse` |
101
+ | `waitForCrawl(idOrTask, options?)` | polls `GET /v1/crawl/{id}` | task id or task | `CrawlTask` (`completed`, `failed` or `canceled`) |
102
+ | `monitors.create/list/get/update/delete/run` | `/v1/monitors…` | `MonitorCreateParams`, `MonitorUpdateParams` | `Monitor`, `MonitorList` |
103
+ | `monitors.runs(id, params?)` / `monitors.iterRuns(id)` | `GET /v1/monitors/{id}/runs` | `limit`, `before` | `MonitorRunList` / `AsyncIterable<MonitorRun>` |
98
104
  | `account()` | `GET /v1/account` | none | `Account` (`balance`, `plan`, `rate_limit_rps`, `month`, …) |
99
105
  | `batches.create(params)` | `POST /v1/batches` | `BatchCreateParams` | `BatchCreateResponse` |
100
106
  | `batches.get(id)` | `GET /v1/batches/{id}` | batch id | `Batch` |
@@ -147,6 +153,24 @@ await sk.search({ q: "espresso", max_age: 3600 });
147
153
  "compact"` resolves to a `CompactResponse`. The return type follows the literal you pass. With
148
154
  `fields` the response contains only the requested keys, so treat the typed fields as optional.
149
155
 
156
+ ## Search controls
157
+
158
+ Domain filters and date ranges work on search, news, images and videos; `boost_domains` on search
159
+ and news; `highlights` on search with `include_content`. None of them costs extra credits.
160
+
161
+ ```ts
162
+ const res = await sk.search({
163
+ q: "connection pooling",
164
+ include_domains: ["postgresql.org", "github.com/pgbouncer", ".edu"], // host, path prefix or TLD (≤ 20)
165
+ exclude_domains: ["pinterest.com"],
166
+ boost_domains: ["postgresql.org"], // to the top, keeping the rest
167
+ start_date: "2026-01-01", // YYYY-MM-DD, end_date too
168
+ include_content: 3,
169
+ highlights: true, // 3 query-ranked passages per page instead of the whole page
170
+ });
171
+ for (const r of res.results) console.log(r.position, r.published_at, r.highlights?.[0]?.text);
172
+ ```
173
+
150
174
  ## Search engines & fallback
151
175
 
152
176
  By default every request is answered by Google only (`engine: "google"`); SerpKite already
@@ -174,6 +198,104 @@ await sk.news({ q: "espresso", engine: ["google", "brave"] });
174
198
  and its `outcome` (absent on cache hits). `request.engine` echoes what you asked for.
175
199
  - Credits (`meta.credits_used`, `X-Credits-Used`) follow the answering provider's price.
176
200
 
201
+ ## Map and extract
202
+
203
+ ```ts
204
+ // The URLs of a site (1 credit): sitemaps + start page, canonicalised and deduplicated
205
+ const site = await sk.map({ url: "https://docs.example.com/", search: "install", include_paths: ["^/guides/"] });
206
+ for (const u of site.results) console.log(u.url);
207
+
208
+ // Up to 20 URLs (HTML or PDF) as Markdown in one call: 1 credit per URL that came back
209
+ const pages = await sk.extract({ urls: site.results.slice(0, 5).map((u) => u.url), query: "install", highlights: 3 });
210
+ for (const f of pages.failed) console.log("failed", f.url, f.error.code); // not charged
211
+ ```
212
+
213
+ ## Crawl
214
+
215
+ ```ts
216
+ const task = await sk.crawl({
217
+ url: "https://docs.example.com/",
218
+ limit: 200, // up to 1000 pages; max_depth up to 10
219
+ include_paths: ["^/guides/"],
220
+ sitemap: "include", // include (default) | only | skip
221
+ query: "authentication", // read the most relevant pages first
222
+ });
223
+ const done = await sk.waitForCrawl(task); // polls until completed/failed/canceled
224
+ for (const p of done.result?.pages ?? []) console.log(p.url, p.markdown?.length);
225
+ console.log(done.result?.stats.stopped); // done | limit | time_limit | size_limit | too_many_failures | canceled
226
+ ```
227
+
228
+ - `crawl` reserves `limit` credits and charges 1 per page read (0.5 from cache); the rest is
229
+ refunded. A crawl that reads nothing fails (`no_pages`) and costs nothing.
230
+ - It returns `202` with a task; `waitForCrawl` takes `{ timeoutMs, pollIntervalMs, maxPollIntervalMs,
231
+ signal }` like `batches.wait` (default timeout 35 minutes: a crawl runs for up to 30) and
232
+ returns failed or canceled tasks instead of throwing.
233
+ - `cancelCrawl(id)` refunds a queued crawl; a running one stops at its next checkpoint
234
+ (`status: "canceling"`) and is charged for the pages read.
235
+ - Robots.txt (per host) and Crawl-delay are honoured; `result.stats` reports `discovered`, `queued`,
236
+ `duplicates`, `sitemap_urls`, `robots`, `robots_blocked` and why it `stopped`.
237
+ - Pass `webhook_url` to get a signed `crawl.completed` delivery instead of polling. A result over
238
+ 4 MB arrives as `result: null` with `result_omitted: true`; fetch it from `poll_url`.
239
+
240
+ ## Monitors
241
+
242
+ ```ts
243
+ const mon = await sk.monitors.create({
244
+ q: "ai agents",
245
+ endpoint: "news",
246
+ interval: "hourly", // hourly | daily | weekly, or interval_seconds (3600-2592000)
247
+ webhook_url: "https://example.com/hooks/serpkite",
248
+ });
249
+ await sk.monitors.run(mon.id); // due within ~30 s
250
+ await sk.monitors.update(mon.id, { active: false });
251
+ await sk.monitors.update(mon.id, { q: "ai agents frameworks", num: 20 }); // change the saved search
252
+ const { results } = await sk.monitors.list();
253
+
254
+ // Run history, newest first (new results kept 24 h): one page, or every run
255
+ const page = await sk.monitors.runs(mon.id, { limit: 20 }); // page.next_before → { before }
256
+ for await (const run of sk.monitors.iterRuns(mon.id)) console.log(run.status, run.new_results);
257
+ await sk.monitors.delete(mon.id);
258
+ ```
259
+
260
+ A `webpage` monitor watches one page instead of a search: it checks `url` each interval (1 credit
261
+ per check) and reports it as a `MonitorPageChange` (`url`, `title`, `change: "new" | "changed"`,
262
+ `content_hash`, `markdown`) when its content changes. `metadata` (your own JSON object, ≤ 2 KB) is
263
+ stored on any monitor and echoed in its `monitor.results` webhooks; on `update`, an object replaces it
264
+ and `null` clears it.
265
+
266
+ ```ts
267
+ const watch = await sk.monitors.create({
268
+ endpoint: "webpage",
269
+ url: "https://example.com/pricing",
270
+ interval: "daily",
271
+ metadata: { customer: "acme" },
272
+ });
273
+ await sk.monitors.update(watch.id, { metadata: null });
274
+ ```
275
+
276
+ Each 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
277
+ hasn't seen before (among the top `num`) as a `monitor.results` webhook. `webhook_url` is optional:
278
+ without one, read new results from `monitors.runs`. `active: false` creates a monitor paused; after 10
279
+ failed runs in a row it pauses itself (`last_status: "paused"`) and `update(id, { active: true })`
280
+ resumes it. `webhook_url: ""` removes the webhook; a changed search reports every result as new once.
281
+
282
+ ## Webhooks
283
+
284
+ Every delivery (`batch.completed`, `crawl.completed`, `monitor.results`) is signed with
285
+ `X-SerpKite-Signature`. `parseWebhook` verifies it and returns the event typed by
286
+ `X-SerpKite-Event`, with `deliveryId` from `X-SerpKite-Delivery` (for `monitor.results`, the
287
+ `run_id`) to deduplicate retries:
288
+
289
+ ```ts
290
+ import { parseWebhook } from "serpkite";
291
+
292
+ const ev = await parseWebhook(process.env.SERPKITE_WEBHOOK_SECRET!, rawBody, req.headers); // throws invalid_signature
293
+ if (ev.type === "monitor.results") console.log(ev.data.metadata, ev.data.url, ev.data.new_results.length);
294
+ if (ev.type === "crawl.completed") console.log(ev.data.status, ev.data.poll_url);
295
+ ```
296
+
297
+ `verifyWebhook(secret, rawBody, headers)` only checks the signature and returns a boolean.
298
+
177
299
  ## Batches
178
300
 
179
301
  Batch jobs cost half price. Submit 1-100 requests for one endpoint; each request becomes a job.
@@ -247,8 +369,8 @@ Failed, empty and blocked searches are refunded, so they never cost credits.
247
369
  Requests are retried up to `maxRetries` times (default 2) on `429`, `5xx`, network errors and
248
370
  timeouts, with exponential backoff (`retryDelayMs · 2^attempt`, capped at 8 s) and jitter. A
249
371
  `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.
372
+ are never retried. `batches.create`, `crawl`, `monitors.create` and `monitors.run` retry only on
373
+ `429`, so a lost response can never queue (and bill) the same work twice.
252
374
 
253
375
  ## Types
254
376
 
@@ -256,9 +378,11 @@ Types are generated from the OpenAPI contract (`backend/api/serp-api.yaml`) with
256
378
  `openapi-typescript`. Friendly aliases are exported: `SearchParams`, `SearchResponse`,
257
379
  `OrganicResult`, `AnswerBox`, `NewsResponse`, `NewsResult`, `ImagesResponse`, `PlacesResponse`,
258
380
  `ReviewsResponse`, `ShoppingResponse`, `ScholarResponse`, `PatentsResponse`,
259
- `AutocompleteResponse`, `LensResponse`, `WebpageResponse`, `RankResponse`,
381
+ `AutocompleteResponse`, `WebpageResponse`, `RankResponse`,
260
382
  `Account`, `Batch`, `BatchCreateParams`, `Meta`, `Engine`, `Provider`, `RouteStep`,
261
- `CompactResponse` and more. The raw
383
+ `CompactResponse`, `MapResponse`, `ExtractResponse`, `CrawlParams`, `CrawlTask`, `CrawlResult`,
384
+ `TaskCreated`, `Monitor`, `MonitorRun`, `MonitorPageChange`, `TaskCompletedEvent`,
385
+ `MonitorResultsEvent`, `WebhookEvent` and more. The raw
262
386
  `paths`, `components` and `operations` types are exported as well.
263
387
 
264
388
  ## Development
package/dist/index.cjs CHANGED
@@ -50,8 +50,9 @@ function errorFromResponse(status, headers, body) {
50
50
  }
51
51
 
52
52
  // src/client.ts
53
- var VERSION = "0.2.0";
53
+ var VERSION = "0.4.0";
54
54
  var DEFAULT_BASE_URL = "https://api.serpkite.com";
55
+ var TASK_DONE = /* @__PURE__ */ new Set(["completed", "failed", "canceled"]);
55
56
  var SerpKite = class {
56
57
  baseUrl;
57
58
  timeoutMs;
@@ -113,18 +114,154 @@ var SerpKite = class {
113
114
  autocomplete = this.#vertical("/v1/autocomplete");
114
115
  /** Fetch any public URL as clean Markdown plus metadata. */
115
116
  webpage = this.#vertical("/v1/webpage");
116
- /** Google Lens visual matches for an image URL. */
117
- lens(params, options) {
118
- return this.request({ method: "POST", path: "/v1/lens", body: params, options });
119
- }
120
117
  /** Position of `domain` for keyword `q` in the top `num` results (default 100). */
121
118
  rank(params, options) {
122
119
  return this.request({ method: "POST", path: "/v1/rank", body: params, options });
123
120
  }
121
+ /**
122
+ * Read up to 20 URLs (HTML or PDF) as Markdown, text or HTML in one call, with optional
123
+ * query-ranked `highlights` (BM25), `include_links` and `include_images`. 1 credit per URL
124
+ * that comes back (0.5 from cache); URLs that fail are listed in `failed` and cost nothing.
125
+ */
126
+ extract(params, options) {
127
+ const wait = ((params.timeout ?? 50) + 15) * 1e3;
128
+ return this.request({
129
+ method: "POST",
130
+ path: "/v1/extract",
131
+ body: params,
132
+ options: {
133
+ ...options,
134
+ timeoutMs: options?.timeoutMs ?? (this.timeoutMs > 0 ? Math.max(this.timeoutMs, wait) : 0)
135
+ },
136
+ retry: "rate_limit"
137
+ });
138
+ }
139
+ /**
140
+ * The URLs of a site from its sitemaps (XML, RSS/Atom, gzip, plain text) and start page,
141
+ * canonicalised and deduplicated, optionally ranked by `search`. `include_paths` /
142
+ * `exclude_paths` (regular expressions on the URL path) and `ignore_query_parameters`
143
+ * narrow the list. 1 credit; free when nothing is found.
144
+ */
145
+ map(params, options) {
146
+ return this.request({ method: "POST", path: "/v1/map", body: params, options });
147
+ }
124
148
  /** Balance, limits and this month's usage for the key's account. */
125
149
  account(options) {
126
150
  return this.request({ method: "GET", path: "/v1/account", options });
127
151
  }
152
+ // ── Async tasks: crawl ────────────────────────────────────────────────
153
+ /**
154
+ * Starts an async crawl of one site and returns the queued task (`202`). 1 credit per page
155
+ * read (0.5 from cache); `limit` credits (up to 1000 pages, `max_depth` up to 10) are
156
+ * reserved up front and the rest refunded. `sitemap` (`include` default, `only`, `skip`)
157
+ * seeds the crawl with sitemap URLs; `query` reads the most relevant pages first. robots.txt
158
+ * and Crawl-delay are honoured; `result.stats.stopped` says why the crawl ended (`done`,
159
+ * `limit`, `time_limit`, `size_limit`, `too_many_failures`, `canceled`).
160
+ * Only retried on 429, so a lost response never starts (and reserves) a second crawl.
161
+ */
162
+ crawl(params, options) {
163
+ return this.request({
164
+ method: "POST",
165
+ path: "/v1/crawl",
166
+ body: params,
167
+ options,
168
+ retry: "rate_limit"
169
+ });
170
+ }
171
+ /** Polls a crawl (results are kept for 24 hours). */
172
+ getCrawl(id, options) {
173
+ return this.request({ method: "GET", path: `/v1/crawl/${encodeURIComponent(id)}`, options });
174
+ }
175
+ /** Cancels a crawl: refunded if still queued, else stops at the next checkpoint. */
176
+ cancelCrawl(id, options) {
177
+ return this.request({
178
+ method: "DELETE",
179
+ path: `/v1/crawl/${encodeURIComponent(id)}`,
180
+ options
181
+ });
182
+ }
183
+ /**
184
+ * Polls until the crawl is `completed`, `failed` or `canceled` and returns it (a failed
185
+ * task is returned, not thrown; check `status` and `error`). Accepts an id or the task
186
+ * returned by `crawl`.
187
+ */
188
+ async waitForCrawl(idOrTask, options = {}) {
189
+ return this.#waitTask(taskId(idOrTask), (id) => this.getCrawl(id, { signal: options.signal }), {
190
+ ...options,
191
+ timeoutMs: options.timeoutMs ?? 21e5
192
+ });
193
+ }
194
+ // ── Monitors ──────────────────────────────────────────────────────────
195
+ monitors = {
196
+ /**
197
+ * Saves a `search` or `news` request (`q`) that runs on a schedule (hourly to monthly) and
198
+ * POSTs results it hasn't seen before to `webhook_url` (`monitor.results`). With
199
+ * `endpoint: "webpage"` and a `url` it checks that page each interval (1 credit per check)
200
+ * and reports it (`MonitorPageChange`) when its content changes. `metadata` (your JSON
201
+ * object, at most 2 KB) is echoed on the monitor and in its webhooks. Without a
202
+ * `webhook_url`, read new results from `monitors.runs`. `active: false`
203
+ * creates it paused. A monitor pauses itself after 10 failed runs in a row
204
+ * (`last_status: "paused"`); `update(id, { active: true })` resumes it. Each run costs
205
+ * what its search costs. Only retried on 429.
206
+ */
207
+ create: (params, options) => this.request({
208
+ method: "POST",
209
+ path: "/v1/monitors",
210
+ body: params,
211
+ options,
212
+ retry: "rate_limit"
213
+ }),
214
+ /** The account's monitors. */
215
+ list: (options) => this.request({ method: "GET", path: "/v1/monitors", options }),
216
+ get: (id, options) => this.request({ method: "GET", path: `/v1/monitors/${encodeURIComponent(id)}`, options }),
217
+ /**
218
+ * Changes `name`, `interval` / `interval_seconds`, `webhook_url` (`""` removes it),
219
+ * `active` (`true` resumes a paused monitor), `metadata` (an object replaces it, `null`
220
+ * clears it) or the saved request (`endpoint`, `q`, `url`, `country`, `num`, domains,
221
+ * `engine`…). Switching `endpoint` between a search and `webpage` drops the other kind's
222
+ * fields. A changed search reports every result as new on the next run.
223
+ */
224
+ update: (id, params, options) => this.request({
225
+ method: "PATCH",
226
+ path: `/v1/monitors/${encodeURIComponent(id)}`,
227
+ body: params,
228
+ options
229
+ }),
230
+ delete: async (id, options) => {
231
+ await this.request({
232
+ method: "DELETE",
233
+ path: `/v1/monitors/${encodeURIComponent(id)}`,
234
+ options
235
+ });
236
+ },
237
+ /** Runs an active monitor at the next scheduler poll (within about 30 seconds).
238
+ * Paused monitors return 409; update(id, { active: true }) resumes and schedules them. */
239
+ run: (id, options) => this.request({
240
+ method: "POST",
241
+ path: `/v1/monitors/${encodeURIComponent(id)}/run`,
242
+ options,
243
+ retry: "rate_limit"
244
+ }),
245
+ /**
246
+ * One page of the monitor's run history, newest first (`limit` 1-100, default 20). Each
247
+ * run carries its new results for 24 hours. Pass `next_before` as `before` for the next
248
+ * page; it is null on the last one.
249
+ */
250
+ runs: (id, params = {}, options) => this.request({
251
+ method: "GET",
252
+ path: `/v1/monitors/${encodeURIComponent(id)}/runs${query(params)}`,
253
+ options
254
+ }),
255
+ /**
256
+ * Iterates over every run of the monitor, newest first, fetching pages of `limit` as
257
+ * needed (starting at `before` when given).
258
+ *
259
+ * ```ts
260
+ * for await (const run of sk.monitors.iterRuns("m1")) console.log(run.new_results);
261
+ * ```
262
+ */
263
+ iterRuns: (id, params = {}, options) => this.#iterRuns(id, params, options)
264
+ };
128
265
  // ── Batches ───────────────────────────────────────────────────────────
129
266
  batches = {
130
267
  /**
@@ -154,6 +291,35 @@ var SerpKite = class {
154
291
  wait: (idOrEntry, options = {}) => this.#wait(idOrEntry, options)
155
292
  };
156
293
  // ── Plumbing ──────────────────────────────────────────────────────────
294
+ async *#iterRuns(id, params, options) {
295
+ let before = params.before;
296
+ for (; ; ) {
297
+ const page = await this.monitors.runs(id, { ...params, before }, options);
298
+ yield* page.results;
299
+ if (!page.next_before || page.results.length === 0) return;
300
+ before = page.next_before;
301
+ }
302
+ }
303
+ async #waitTask(id, get, o) {
304
+ const timeoutMs = o.timeoutMs ?? 6e5;
305
+ const maxInterval = o.maxPollIntervalMs ?? 1e4;
306
+ let interval = o.pollIntervalMs ?? 1e3;
307
+ const deadline = Date.now() + timeoutMs;
308
+ for (; ; ) {
309
+ const task = await get(id);
310
+ if (TASK_DONE.has(task.status)) return task;
311
+ const left = deadline - Date.now();
312
+ if (left <= 0) {
313
+ throw new SerpKiteError(
314
+ 0,
315
+ "timeout",
316
+ `task ${id} still ${task.status} after ${timeoutMs} ms`
317
+ );
318
+ }
319
+ await sleep(Math.min(interval, left), o.signal);
320
+ interval = Math.min(interval * 1.5, maxInterval);
321
+ }
322
+ }
157
323
  #vertical(path) {
158
324
  return ((params, options) => this.request({ method: "POST", path, body: params, options }));
159
325
  }
@@ -288,6 +454,19 @@ var SerpKite = class {
288
454
  return after === void 0 ? this.#backoff(attempt) : Math.min(after, 6e4);
289
455
  }
290
456
  };
457
+ function query(params) {
458
+ const q = new URLSearchParams();
459
+ for (const [k, v] of Object.entries(params)) {
460
+ if (v !== void 0 && v !== null) q.set(k, String(v));
461
+ }
462
+ const s = q.toString();
463
+ return s ? `?${s}` : "";
464
+ }
465
+ function taskId(idOrTask) {
466
+ const id = typeof idOrTask === "string" ? idOrTask : idOrTask?.id;
467
+ if (!id) throw new SerpKiteError(0, "invalid_request", "a task id is required");
468
+ return id;
469
+ }
291
470
  function parseRetryAfter(v) {
292
471
  if (!v) return void 0;
293
472
  const secs = Number(v);
@@ -408,6 +587,29 @@ async function verifyWebhook(secret, payload, headers, options = {}) {
408
587
  for (let i = 0; i < want.length; i++) diff |= want.charCodeAt(i) ^ got.charCodeAt(i);
409
588
  return diff === 0;
410
589
  }
590
+ async function parseWebhook(secret, payload, headers, options = {}) {
591
+ if (!await verifyWebhook(secret, payload, headers, options)) {
592
+ throw new SerpKiteError(400, "invalid_signature", "webhook signature or timestamp is invalid");
593
+ }
594
+ const text = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
595
+ let data;
596
+ try {
597
+ data = JSON.parse(text);
598
+ } catch (err) {
599
+ throw new SerpKiteError(
600
+ 400,
601
+ "invalid_payload",
602
+ "webhook body is not JSON",
603
+ void 0,
604
+ void 0,
605
+ {
606
+ cause: err
607
+ }
608
+ );
609
+ }
610
+ const type = header(headers, "x-serpkite-event") ?? (typeof data?.event === "string" ? data.event : "batch.completed");
611
+ return { type, deliveryId: header(headers, "x-serpkite-delivery"), data };
612
+ }
411
613
 
412
614
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
413
615
  exports.SerpKite = SerpKite;
@@ -415,6 +617,7 @@ exports.SerpKiteError = SerpKiteError;
415
617
  exports.VERSION = VERSION;
416
618
  exports.isBatchError = isBatchError;
417
619
  exports.parseRetryAfter = parseRetryAfter;
620
+ exports.parseWebhook = parseWebhook;
418
621
  exports.verifyWebhook = verifyWebhook;
419
622
  //# sourceMappingURL=index.cjs.map
420
623
  //# sourceMappingURL=index.cjs.map