scavio 0.21.1 → 0.23.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 +181 -4
- package/dist/index.cjs +398 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -28
- package/dist/index.d.ts +601 -28
- package/dist/index.js +384 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for the [Scavio API](https://scavio.dev?utm_source=npm-scavio-js&utm_medium=package&utm_campaign=readme) — real-time structured web
|
|
4
4
|
data across 36 platforms on one API key, plus `extract()` to read
|
|
5
|
-
any URL as clean Markdown.
|
|
5
|
+
any URL as clean Markdown, and `extract.map()` / `extract.crawl()` /
|
|
6
|
+
`extract.batch()` to map, crawl and batch-extract whole websites.
|
|
6
7
|
|
|
7
8
|
Structured JSON in, structured JSON out. No per-site parsers to maintain.
|
|
8
9
|
|
|
@@ -14,6 +15,7 @@ Structured JSON in, structured JSON out. No per-site parsers to maintain.
|
|
|
14
15
|
- **Ads transparency** — Google Ads Transparency Center, Meta Ad Library
|
|
15
16
|
- **Social and video** — YouTube, TikTok, Instagram, Threads, X, LinkedIn, Reddit, Kuaishou
|
|
16
17
|
- **Any other page** — `extract()` turns a URL into Markdown, plain text or raw HTML
|
|
18
|
+
- **Whole websites** — `extract.map()` lists a site's URLs, `extract.crawl()` crawls a site, `extract.batch()` extracts a list of URLs in one job
|
|
17
19
|
|
|
18
20
|
## Install
|
|
19
21
|
|
|
@@ -103,6 +105,129 @@ boilerplate stripped. `text` is that markdown flattened to plain text. URLs are
|
|
|
103
105
|
http(s) only, a bare host is upgraded to https, and loopback, private,
|
|
104
106
|
link-local and cloud-metadata hosts are rejected with a 400.
|
|
105
107
|
|
|
108
|
+
### Website map, crawl and batch extract (new in 0.23.0)
|
|
109
|
+
|
|
110
|
+
`extract` is also a namespace: calling it reads one page, exactly as before
|
|
111
|
+
(`client.extract({ url })` sends the same request it did in 0.22), and its
|
|
112
|
+
methods work on whole sites and URL lists, with each page returned exactly
|
|
113
|
+
like [Extract](https://scavio.dev/docs/extract?utm_source=npm-scavio-js&utm_medium=package&utm_campaign=readme).
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Map: every URL on a site, from its sitemaps and start page. 1 credit.
|
|
117
|
+
const map = await client.extract.map({
|
|
118
|
+
url: "https://example.com",
|
|
119
|
+
limit: 500, // 1-5,000, default 1,000 (plan caps apply)
|
|
120
|
+
sitemap: "include", // "include" | "only" | "skip"
|
|
121
|
+
include_paths: ["/blog/**"], // optional path globs
|
|
122
|
+
});
|
|
123
|
+
map.data.links; // [{ url, source: "sitemap" | "page", lastmod? }]
|
|
124
|
+
map.data.truncated; // true when more URLs exist than `limit`
|
|
125
|
+
|
|
126
|
+
// Crawl: start a job, then poll it (polling and cancelling are free).
|
|
127
|
+
const job = await client.extract.crawl(
|
|
128
|
+
{ url: "https://example.com/docs", max_pages: 50, max_depth: 3, format: "markdown" },
|
|
129
|
+
{ idempotencyKey: "docs-crawl-2026-10-12" }, // optional, sent as Idempotency-Key
|
|
130
|
+
);
|
|
131
|
+
job.data.id; // "crawl_..."
|
|
132
|
+
job.data.status_url;
|
|
133
|
+
|
|
134
|
+
const page = await client.extract.crawlStatus(job.data.id, { limit: 25 });
|
|
135
|
+
page.data.status; // "queued" | "running" | "completed" | "failed" | "cancelled"
|
|
136
|
+
page.data.pages; // { discovered, queued, succeeded, failed, skipped }
|
|
137
|
+
page.data.results; // [{ url, depth, status, status_code, content, ... }]
|
|
138
|
+
page.data.next_cursor; // pass as { cursor } for the next page, null at the end
|
|
139
|
+
|
|
140
|
+
await client.extract.cancelCrawl(job.data.id); // 409 once the crawl has finished
|
|
141
|
+
|
|
142
|
+
// Or wait for it: polls until done and returns every result in one response.
|
|
143
|
+
const done = await client.extract.crawl(
|
|
144
|
+
{ url: "https://example.com/docs", max_pages: 20 },
|
|
145
|
+
{ wait: true, pollInterval: 2000, waitTimeout: 600_000 },
|
|
146
|
+
);
|
|
147
|
+
done.data.results.length;
|
|
148
|
+
|
|
149
|
+
// Batch: extract a list of URLs in one job (results in input order).
|
|
150
|
+
const batch = await client.extract.batch({
|
|
151
|
+
urls: ["https://example.com/a", "https://example.com/b"],
|
|
152
|
+
mode: "normal",
|
|
153
|
+
});
|
|
154
|
+
batch.data.invalid_urls; // rejected up front, never billed
|
|
155
|
+
await client.extract.batchStatus(batch.data.id);
|
|
156
|
+
await client.extract.cancelBatch(batch.data.id);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Pricing.** Map costs 1 credit. Crawl and batch bill per **successful** page
|
|
160
|
+
at Extract's price for the tier: `normal` 1, `advanced` 1, `ultra` 2. Failed
|
|
161
|
+
and skipped pages are free, starting, polling and cancelling a job are free,
|
|
162
|
+
and credits are taken page by page: if your balance runs out the job stops
|
|
163
|
+
cleanly with `stopped_reason: "insufficient_credits"`.
|
|
164
|
+
|
|
165
|
+
**Plan limits.**
|
|
166
|
+
|
|
167
|
+
| Plan | Max pages per crawl | Active jobs | URLs per batch |
|
|
168
|
+
|------|--------------------:|------------:|---------------:|
|
|
169
|
+
| Free | 10 | 1 | 10 |
|
|
170
|
+
| PAYG | 100 | 1 | 50 |
|
|
171
|
+
| Project | 500 | 2 | 100 |
|
|
172
|
+
| Bootstrap | 2,000 | 3 | 500 |
|
|
173
|
+
| Startup | 5,000 | 5 | 1,000 |
|
|
174
|
+
| Growth | 10,000 | 10 | 1,000 |
|
|
175
|
+
|
|
176
|
+
Starting a job above your plan's active-job limit is a 429 (`RateLimitError`)
|
|
177
|
+
with `Retry-After`.
|
|
178
|
+
|
|
179
|
+
**Behavior.** Crawl and map always obey the site's robots.txt. A crawl stays on
|
|
180
|
+
the start URL's site (`allow_subdomains` opts into subdomains) and visits URLs
|
|
181
|
+
under the start path first. Batch treats each URL like a single `extract()`
|
|
182
|
+
call. Results are kept for 3 days after the job finishes (`expires_at`), up to
|
|
183
|
+
2 MB per page and 250 MB per job, and a job runs for at most 6 hours.
|
|
184
|
+
|
|
185
|
+
**`wait: true`.** `crawl()` and `batch()` poll the status endpoint every
|
|
186
|
+
`pollInterval` ms (default 2,000) until the job is completed, failed or
|
|
187
|
+
cancelled, then page through every cursor and return
|
|
188
|
+
`{ data: { ...summary, results: [all], next_cursor: null } }`. If the job is
|
|
189
|
+
still running after `waitTimeout` ms (default 10 minutes) they throw
|
|
190
|
+
`ScavioJobTimeoutError`; the job keeps running and the error carries
|
|
191
|
+
`jobId`, `jobType` and `statusUrl` so you can keep polling or cancel it. For
|
|
192
|
+
big crawls, prefer a webhook or your own polling loop.
|
|
193
|
+
|
|
194
|
+
**Retries and idempotency.** The SDK retries 429/5xx and network errors. Pass
|
|
195
|
+
`idempotencyKey` when starting a job: repeating a POST with the same key within
|
|
196
|
+
24 hours returns the original job, so a retried request never starts a second
|
|
197
|
+
one.
|
|
198
|
+
|
|
199
|
+
#### Completion webhooks
|
|
200
|
+
|
|
201
|
+
Pass `webhook_url` (https) to `crawl()` or `batch()` and Scavio POSTs the job
|
|
202
|
+
summary there when it completes, fails or is cancelled (retried at 0 s, 30 s
|
|
203
|
+
and 5 min). The signing secret comes back once, as `job.data.webhook_secret`.
|
|
204
|
+
Each delivery is signed with
|
|
205
|
+
`Scavio-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`; verify
|
|
206
|
+
it against the **raw** body with `verifyWebhook` (async, Web Crypto, so it runs
|
|
207
|
+
on Node 18+, Deno, Bun and edge runtimes):
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { verifyWebhook } from "scavio";
|
|
211
|
+
|
|
212
|
+
// e.g. a Next.js / Hono / Workers route handler
|
|
213
|
+
export async function POST(req: Request) {
|
|
214
|
+
const raw = await req.text();
|
|
215
|
+
const ok = await verifyWebhook(
|
|
216
|
+
process.env.SCAVIO_WEBHOOK_SECRET!, // the job's webhook_secret
|
|
217
|
+
req.headers.get("Scavio-Signature"),
|
|
218
|
+
raw,
|
|
219
|
+
300, // max signature age in seconds (default)
|
|
220
|
+
);
|
|
221
|
+
if (!ok) return new Response("bad signature", { status: 401 });
|
|
222
|
+
const event = JSON.parse(raw);
|
|
223
|
+
// event.id, event.status, ...
|
|
224
|
+
return new Response("ok");
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`verifyWebhook` returns `false` (never throws) for a missing, malformed,
|
|
229
|
+
tampered or stale signature.
|
|
230
|
+
|
|
106
231
|
### Google
|
|
107
232
|
|
|
108
233
|
Each method hits its own `/api/v2/google*` endpoint and returns Google's full
|
|
@@ -356,7 +481,7 @@ nickname, or a listing with no public questions, is a billed 404.
|
|
|
356
481
|
|
|
357
482
|
### YouTube
|
|
358
483
|
|
|
359
|
-
Credit cost varies by endpoint: `transcript` costs
|
|
484
|
+
Credit cost varies by endpoint: `transcript` costs 5; `streams` costs 3;
|
|
360
485
|
`search` and `shorts` cost 2; every other YouTube endpoint costs 1.
|
|
361
486
|
|
|
362
487
|
```typescript
|
|
@@ -402,6 +527,10 @@ await client.youtube.channelVideos({ channel_id: "UC..." });
|
|
|
402
527
|
await client.youtube.channelShorts({ channel_id: "UC..." });
|
|
403
528
|
await client.youtube.channelCommunity({ channel_id: "UC..." });
|
|
404
529
|
await client.youtube.channelResolve({ channel: "@mkbhd" }); // handle/URL -> id
|
|
530
|
+
|
|
531
|
+
// One community post (text, author, likes, images, comments_cursor) and its comments
|
|
532
|
+
await client.youtube.communityPost({ post_id: "Ugkx_PqHZYEoCCP7e8WIOrh-1HCvtKoFMzVp" });
|
|
533
|
+
await client.youtube.communityPostComments({ post_id: "Ugkx_PqHZYEoCCP7e8WIOrh-1HCvtKoFMzVp" });
|
|
405
534
|
```
|
|
406
535
|
|
|
407
536
|
### Reddit
|
|
@@ -409,7 +538,8 @@ await client.youtube.channelResolve({ channel: "@mkbhd" }); // handle/URL -> id
|
|
|
409
538
|
Every Reddit endpoint costs 1 credit.
|
|
410
539
|
|
|
411
540
|
`search()` takes only `query` and `cursor` — there is no result-type or sort
|
|
412
|
-
filter, so anything else is dropped server-side.
|
|
541
|
+
filter, so anything else is dropped server-side. `searchComments()` does sort
|
|
542
|
+
(`RELEVANCE`, `HOT`, `TOP` or `NEW`). It returns
|
|
413
543
|
`data.results` with `next_cursor` and `has_more`. `post()` returns a flat post
|
|
414
544
|
object under `data` and carries no comments; use `postComments()` for those.
|
|
415
545
|
The subreddit and user feeds return `data.posts`.
|
|
@@ -444,6 +574,10 @@ await client.reddit.userComments({ username: "spez" });
|
|
|
444
574
|
// Site-wide popular feed and trending searches
|
|
445
575
|
await client.reddit.popular();
|
|
446
576
|
await client.reddit.trending();
|
|
577
|
+
|
|
578
|
+
// Comment search (each result carries its post and subreddit) and subreddit search
|
|
579
|
+
await client.reddit.searchComments({ query: "web scraping api", sort: "NEW" });
|
|
580
|
+
await client.reddit.searchSubreddits({ query: "web scraping" });
|
|
447
581
|
```
|
|
448
582
|
|
|
449
583
|
### X
|
|
@@ -467,8 +601,15 @@ await client.x.userFollowings({ screen_name: "elonmusk" });
|
|
|
467
601
|
|
|
468
602
|
// Trending topics
|
|
469
603
|
await client.x.trending({ country: "UnitedStates" });
|
|
604
|
+
|
|
605
|
+
// Communities: search, details (rules, member count), and posts
|
|
606
|
+
await client.x.searchCommunities({ keyword: "crypto" });
|
|
607
|
+
await client.x.community({ community_id: "1508567430188126209" });
|
|
608
|
+
await client.x.communityTweets({ community_id: "1508567430188126209", ranking: "Recency" });
|
|
470
609
|
```
|
|
471
610
|
|
|
611
|
+
Every X endpoint costs 1 credit.
|
|
612
|
+
|
|
472
613
|
### LinkedIn
|
|
473
614
|
|
|
474
615
|
```typescript
|
|
@@ -518,6 +659,10 @@ await client.tiktok.userPosts({ sec_user_id: "abc123", count: 30 });
|
|
|
518
659
|
// Video details
|
|
519
660
|
await client.tiktok.video({ video_id: "vid123" });
|
|
520
661
|
|
|
662
|
+
// Video transcript: timestamped segments and full text (video_id or url;
|
|
663
|
+
// optional 2-letter language). Caption-less videos return available: false.
|
|
664
|
+
await client.tiktok.videoTranscript({ video_id: "7609177768793787679" });
|
|
665
|
+
|
|
521
666
|
// Video comments
|
|
522
667
|
await client.tiktok.videoComments({ video_id: "vid123", count: 20 });
|
|
523
668
|
|
|
@@ -541,8 +686,39 @@ await client.tiktok.userFollowers({ sec_user_id: "abc123" });
|
|
|
541
686
|
|
|
542
687
|
// User followings
|
|
543
688
|
await client.tiktok.userFollowings({ sec_user_id: "abc123" });
|
|
689
|
+
|
|
690
|
+
// Trending searches and a sample of trending videos (about 6 per call)
|
|
691
|
+
await client.tiktok.trendingSearches();
|
|
692
|
+
await client.tiktok.trendingVideos();
|
|
693
|
+
|
|
694
|
+
// Search autocomplete
|
|
695
|
+
await client.tiktok.searchSuggestions({ keyword: "cooking" });
|
|
696
|
+
|
|
697
|
+
// Sounds: detail, videos using a sound, search, Top 50 / Viral 50 chart
|
|
698
|
+
await client.tiktok.music({ music_id: "7364498342501435408" });
|
|
699
|
+
await client.tiktok.musicVideos({ music_id: "7364498342501435408" });
|
|
700
|
+
await client.tiktok.searchMusic({ keyword: "espresso" });
|
|
701
|
+
await client.tiktok.musicChart({ chart: "viral" });
|
|
702
|
+
|
|
703
|
+
// Similar creators, playlists and the videos in one
|
|
704
|
+
await client.tiktok.userSimilar({ sec_user_id: "abc123" });
|
|
705
|
+
await client.tiktok.userPlaylists({ sec_user_id: "abc123" });
|
|
706
|
+
await client.tiktok.playlistVideos({ playlist_id: "7596415294902389534" });
|
|
707
|
+
|
|
708
|
+
// Live status and account region (region also returns sec_user_id)
|
|
709
|
+
await client.tiktok.userLive({ username: "mrbeast" });
|
|
710
|
+
await client.tiktok.userRegion({ username: "mrbeast" });
|
|
544
711
|
```
|
|
545
712
|
|
|
713
|
+
Every TikTok endpoint costs 1 credit. The trending, sound, similar, playlist,
|
|
714
|
+
live and region endpoints return TikTok's raw payload: large numeric ids can
|
|
715
|
+
lose precision when parsed as JSON numbers, so use the string forms (`id_str`,
|
|
716
|
+
`sec_uid`, `sec_user_id`). `trendingVideos()` returns about 6 videos per call
|
|
717
|
+
whatever `count` is set to, and `musicVideos()` ignores `count` (about 14 per
|
|
718
|
+
page). Search suggestions can be empty for long-tail keywords and still bill.
|
|
719
|
+
An unknown username is a billed 404 on `userLive()` and an unbilled 404 on
|
|
720
|
+
`userRegion()`.
|
|
721
|
+
|
|
546
722
|
### TikTok Shop
|
|
547
723
|
|
|
548
724
|
Every TikTok Shop endpoint costs 1 credit. Two limits to design around:
|
|
@@ -1178,7 +1354,8 @@ All error classes:
|
|
|
1178
1354
|
| `InsufficientCreditsError` | 402 | No credits remaining |
|
|
1179
1355
|
| `NotFoundError` | 404 | No data for that id (see TikTok Shop above) |
|
|
1180
1356
|
| `RateLimitError` | 429 | Rate limit exceeded |
|
|
1181
|
-
| `ScavioAPIError` | other | Catch-all (has `.statusCode
|
|
1357
|
+
| `ScavioAPIError` | other | Catch-all (has `.statusCode`; a 409 from cancelling a finished job lands here) |
|
|
1358
|
+
| `ScavioJobTimeoutError` | — | `crawl()`/`batch()` with `wait: true` outlived `waitTimeout`; carries `jobId` |
|
|
1182
1359
|
|
|
1183
1360
|
Threads and Kuaishou answer a missing or conflicting identifier with **422**, not
|
|
1184
1361
|
400 — those routes have no 400 at all. Both map to `BadRequestError`, so one
|