Skip to content

Commit ca92285

Browse files
committed
feat: typed canonical response shapes (breaking)
Every endpoint now returns a single canonical envelope ({ data, page?, meta }) across all six platforms, replacing the raw upstream-platform shapes (TikTok's user.uniqueId, Twitter's legacy.screen_name, Instagram's data.user.edge_followed_by.count, etc.). - Response field names are snake_case and consistent cross-platform - Dates are ISO 8601 - Pagination unified as page.cursor + page.has_more on list endpoints - New exported types: Envelope, Creator, MiniCreator, Post, Comment, Transcript, Hashtag, Song, Subreddit, Company, Media, Platform - Method names and parameters unchanged Bumps to 0.3.0. See CHANGELOG.md for migration notes.
1 parent 3d82561 commit ca92285

11 files changed

Lines changed: 479 additions & 66 deletions

File tree

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Changelog
2+
3+
## 0.3.0
4+
5+
### Breaking changes
6+
7+
All endpoints now return a single canonical response envelope across every platform, replacing the raw upstream-platform shapes returned by 0.2.x and earlier.
8+
9+
Every response now has the shape:
10+
11+
```ts
12+
type Envelope<T> = {
13+
data: T
14+
page?: { cursor: string | null; has_more: boolean; total?: number }
15+
meta: { platform: Platform; fetched_at: string }
16+
}
17+
```
18+
19+
What this means in practice:
20+
21+
- TikTok profiles return `data.handle` / `data.follower_count` instead of `user.uniqueId` / `stats.followerCount`.
22+
- Twitter profiles return `data.handle` / `data.verified_tier` instead of `legacy.screen_name` / `is_blue_verified`.
23+
- Instagram profiles return `data.follower_count` instead of `data.user.edge_followed_by.count`.
24+
- YouTube channels return `data.handle` / `data.subscriber_count` mapped into `data.follower_count` for cross-platform consistency.
25+
- Reddit, LinkedIn (profile + company), and all post / comment / transcript endpoints follow the same envelope.
26+
- All dates are ISO 8601 strings.
27+
- All field names are `snake_case`.
28+
- Pagination is now consistent: `page.cursor` + `page.has_more` on list endpoints.
29+
30+
### Migration
31+
32+
Method names and parameters are unchanged. Only the response shape changed.
33+
34+
Before (0.2.x):
35+
36+
```ts
37+
const res = await cc.twitter.profile({ handle: 'elonmusk' })
38+
console.log(res.legacy.screen_name, res.legacy.followers_count)
39+
```
40+
41+
After (0.3.0):
42+
43+
```ts
44+
const res = await cc.twitter.profile({ handle: 'elonmusk' })
45+
console.log(res.data.handle, res.data.follower_count)
46+
```
47+
48+
### Added
49+
50+
- Canonical exported types: `Envelope`, `Creator`, `MiniCreator`, `Post`, `Comment`, `Transcript`, `Hashtag`, `Song`, `Subreddit`, `Company`, `Media`, `Platform`, `PageInfo`, plus enums for `PostType`.
51+
- `CreatorWithPosts` and `SubredditWithPosts` helpers for endpoints that return a primary entity plus recent posts.
52+
53+
## 0.2.0
54+
55+
Initial public release.

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ One typed client, six platforms: **TikTok, Instagram, YouTube, LinkedIn, Twitter
99
- Works in Node 18+, Bun, Deno, Cloudflare Workers, and browsers
1010
- Same endpoints are also available as a native MCP server for Claude, Cursor, Windsurf, and Zed
1111

12+
## Breaking changes in 0.3
13+
14+
0.3.0 unifies every response under a single canonical envelope. Field names are now `snake_case` and identical across platforms, so a `Creator` from TikTok and one from YouTube share the same shape.
15+
16+
```ts
17+
type Envelope<T> = {
18+
data: T
19+
page?: { cursor: string | null; has_more: boolean; total?: number }
20+
meta: { platform: Platform; fetched_at: string }
21+
}
22+
```
23+
24+
If you were reading raw upstream fields (`legacy.screen_name`, `stats.followerCount`, `data.user.edge_followed_by.count`, etc.), you will need to update your call sites. Method names and parameters are unchanged. See `CHANGELOG.md` for migration notes.
25+
1226
## Install
1327
1428
```bash
@@ -33,6 +47,76 @@ const transcript = await cc.youtube.transcript({ url: 'https://youtu.be/...' })
3347
const company = await cc.linkedin.company({ url: 'https://www.linkedin.com/company/openai' })
3448
```
3549

50+
## Response shape
51+
52+
Every endpoint returns the same envelope:
53+
54+
```ts
55+
{
56+
data: T, // Creator | Post | Post[] | Comment[] | Transcript | ...
57+
page?: { // present on list endpoints
58+
cursor: string | null,
59+
has_more: boolean,
60+
total?: number
61+
},
62+
meta: {
63+
platform: 'tiktok' | 'instagram' | 'twitter' | 'youtube' | 'reddit' | 'linkedin',
64+
fetched_at: string // ISO 8601
65+
}
66+
}
67+
```
68+
69+
A Twitter profile call:
70+
71+
```ts
72+
const res = await cc.twitter.profile({ handle: 'elonmusk' })
73+
// {
74+
// data: {
75+
// id: '44196397',
76+
// handle: 'elonmusk',
77+
// name: 'Elon Musk',
78+
// bio: '...',
79+
// url: 'https://twitter.com/elonmusk',
80+
// avatar_url: 'https://...',
81+
// verified: true,
82+
// verified_tier: 'blue',
83+
// follower_count: 200000000,
84+
// following_count: 800,
85+
// post_count: 50000,
86+
// created_at: '2009-06-02T20:12:29.000Z',
87+
// platform: 'twitter',
88+
// recent_posts: [ /* Post[] */ ]
89+
// },
90+
// meta: { platform: 'twitter', fetched_at: '2025-01-15T12:00:00.000Z' }
91+
// }
92+
```
93+
94+
A TikTok video list:
95+
96+
```ts
97+
const res = await cc.tiktok.profileVideos({ handle: 'khaby.lame' })
98+
// {
99+
// data: [
100+
// {
101+
// id: '7234567890',
102+
// url: 'https://www.tiktok.com/@khaby.lame/video/7234567890',
103+
// type: 'video',
104+
// created_at: '2024-12-01T15:30:00.000Z',
105+
// text: 'Caption here #fyp',
106+
// view_count: 12000000,
107+
// like_count: 1200000,
108+
// comment_count: 8000,
109+
// share_count: 50000,
110+
// author: { id: '...', handle: 'khaby.lame', name: 'Khaby Lame', url: '...', platform: 'tiktok' },
111+
// hashtags: ['fyp'],
112+
// platform: 'tiktok'
113+
// }
114+
// ],
115+
// page: { cursor: '1701436200', has_more: true },
116+
// meta: { platform: 'tiktok', fetched_at: '2025-01-15T12:00:00.000Z' }
117+
// }
118+
```
119+
36120
## Resources
37121

38122
```ts

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@creatorcrawl/sdk",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Official TypeScript SDK for the CreatorCrawl social media data API. Scrape TikTok, Instagram, YouTube, LinkedIn, Twitter/X, and Reddit profiles, posts, comments, transcripts, and ads.",
55
"license": "MIT",
66
"author": "CreatorCrawl <support@creatorcrawl.com>",

src/index.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,29 @@ export {
66
RateLimitError,
77
UpstreamError,
88
} from './errors'
9-
export type { CreatorCrawlOptions, JsonRecord, RequestOptions } from './types'
9+
export type {
10+
Comment,
11+
Company,
12+
Creator,
13+
CreatorCrawlOptions,
14+
CreatorWithPosts,
15+
Envelope,
16+
Hashtag,
17+
JsonRecord,
18+
Media,
19+
MiniCreator,
20+
PageInfo,
21+
Platform,
22+
Post,
23+
PostMusic,
24+
PostType,
25+
RequestOptions,
26+
Song,
27+
Subreddit,
28+
SubredditWithPosts,
29+
Transcript,
30+
TranscriptSegment,
31+
} from './types'
1032
export type {
1133
TikTokCommentsParams,
1234
TikTokCreatorTranscriptsParams,

src/resources/instagram.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import type { CreatorCrawl } from '../client'
2-
import type { JsonRecord, RequestOptions } from '../types'
2+
import type {
3+
Comment,
4+
Creator,
5+
CreatorWithPosts,
6+
Envelope,
7+
JsonRecord,
8+
Post,
9+
RequestOptions,
10+
Transcript,
11+
} from '../types'
312

413
export type InstagramProfileParams = { handle: string }
514
export type InstagramBasicProfileParams = { userId: string }
@@ -17,46 +26,46 @@ export class Instagram {
1726
constructor(private readonly client: CreatorCrawl) {}
1827

1928
profile(params: InstagramProfileParams, options?: RequestOptions) {
20-
return this.client.get<JsonRecord>('/instagram/profile', params, options)
29+
return this.client.get<Envelope<CreatorWithPosts>>('/instagram/profile', params, options)
2130
}
2231

2332
basicProfile(params: InstagramBasicProfileParams, options?: RequestOptions) {
24-
return this.client.get<JsonRecord>('/instagram/basic-profile', params, options)
33+
return this.client.get<Envelope<Creator>>('/instagram/basic-profile', params, options)
2534
}
2635

2736
posts(params: InstagramPostsParams, options?: RequestOptions) {
28-
return this.client.get<JsonRecord>('/instagram/user/posts', params, options)
37+
return this.client.get<Envelope<Post[]>>('/instagram/user/posts', params, options)
2938
}
3039

3140
reels(params: InstagramReelsParams, options?: RequestOptions) {
32-
return this.client.get<JsonRecord>('/instagram/user/reels', params, options)
41+
return this.client.get<Envelope<Post[]>>('/instagram/user/reels', params, options)
3342
}
3443

3544
postInfo(params: InstagramPostInfoParams, options?: RequestOptions) {
36-
return this.client.get<JsonRecord>('/instagram/post', params, options)
45+
return this.client.get<Envelope<Post>>('/instagram/post', params, options)
3746
}
3847

3948
comments(params: InstagramCommentsParams, options?: RequestOptions) {
40-
return this.client.get<JsonRecord>('/instagram/post/comments', params, options)
49+
return this.client.get<Envelope<Comment[]>>('/instagram/post/comments', params, options)
4150
}
4251

4352
transcript(params: InstagramTranscriptParams, options?: RequestOptions) {
44-
return this.client.get<JsonRecord>('/instagram/media/transcript', params, options)
53+
return this.client.get<Envelope<Transcript>>('/instagram/media/transcript', params, options)
4554
}
4655

4756
storyHighlights(params: InstagramStoryHighlightsParams = {}, options?: RequestOptions) {
48-
return this.client.get<JsonRecord>('/instagram/user/highlights', params, options)
57+
return this.client.get<Envelope<JsonRecord[]>>('/instagram/user/highlights', params, options)
4958
}
5059

5160
highlightsDetails(params: InstagramHighlightsDetailsParams, options?: RequestOptions) {
52-
return this.client.get<JsonRecord>('/instagram/user/highlight/detail', params, options)
61+
return this.client.get<Envelope<JsonRecord>>('/instagram/user/highlight/detail', params, options)
5362
}
5463

5564
searchReels(params: InstagramSearchReelsParams, options?: RequestOptions) {
56-
return this.client.get<JsonRecord>('/instagram/reels/search', params, options)
65+
return this.client.get<Envelope<Post[]>>('/instagram/reels/search', params, options)
5766
}
5867

5968
embed(params: InstagramEmbedParams, options?: RequestOptions) {
60-
return this.client.get<JsonRecord>('/instagram/user/embed', params, options)
69+
return this.client.get<Envelope<JsonRecord>>('/instagram/user/embed', params, options)
6170
}
6271
}

src/resources/linkedin.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { CreatorCrawl } from '../client'
2-
import type { JsonRecord, RequestOptions } from '../types'
2+
import type {
3+
Company,
4+
Creator,
5+
Envelope,
6+
JsonRecord,
7+
Post,
8+
RequestOptions,
9+
} from '../types'
310

411
export type LinkedInProfileParams = { url: string }
512
export type LinkedInCompanyParams = { url: string }
@@ -20,26 +27,26 @@ export class LinkedIn {
2027
constructor(private readonly client: CreatorCrawl) {}
2128

2229
profile(params: LinkedInProfileParams, options?: RequestOptions) {
23-
return this.client.get<JsonRecord>('/linkedin/profile', params, options)
30+
return this.client.get<Envelope<Creator>>('/linkedin/profile', params, options)
2431
}
2532

2633
company(params: LinkedInCompanyParams, options?: RequestOptions) {
27-
return this.client.get<JsonRecord>('/linkedin/company', params, options)
34+
return this.client.get<Envelope<Company>>('/linkedin/company', params, options)
2835
}
2936

3037
companyPosts(params: LinkedInCompanyPostsParams, options?: RequestOptions) {
31-
return this.client.get<JsonRecord>('/linkedin/company/posts', params, options)
38+
return this.client.get<Envelope<Post[]>>('/linkedin/company/posts', params, options)
3239
}
3340

3441
post(params: LinkedInPostParams, options?: RequestOptions) {
35-
return this.client.get<JsonRecord>('/linkedin/post', params, options)
42+
return this.client.get<Envelope<Post>>('/linkedin/post', params, options)
3643
}
3744

3845
adsSearch(params: LinkedInAdsSearchParams = {}, options?: RequestOptions) {
39-
return this.client.get<JsonRecord>('/linkedin/ads/search', params, options)
46+
return this.client.get<Envelope<JsonRecord[]>>('/linkedin/ads/search', params, options)
4047
}
4148

4249
ad(params: LinkedInAdParams, options?: RequestOptions) {
43-
return this.client.get<JsonRecord>('/linkedin/ad', params, options)
50+
return this.client.get<Envelope<JsonRecord>>('/linkedin/ad', params, options)
4451
}
4552
}

src/resources/reddit.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { CreatorCrawl } from '../client'
2-
import type { JsonRecord, RequestOptions } from '../types'
2+
import type {
3+
Comment,
4+
Envelope,
5+
Post,
6+
RequestOptions,
7+
Subreddit,
8+
SubredditWithPosts,
9+
} from '../types'
310

411
export type RedditSearchParams = {
512
query: string
@@ -33,22 +40,26 @@ export class Reddit {
3340
constructor(private readonly client: CreatorCrawl) {}
3441

3542
search(params: RedditSearchParams, options?: RequestOptions) {
36-
return this.client.get<JsonRecord>('/reddit/search', params, options)
43+
return this.client.get<Envelope<Post[]>>('/reddit/search', params, options)
3744
}
3845

3946
subredditDetails(params: RedditSubredditDetailsParams, options?: RequestOptions) {
40-
return this.client.get<JsonRecord>('/reddit/subreddit/details', params, options)
47+
return this.client.get<Envelope<Subreddit | SubredditWithPosts>>(
48+
'/reddit/subreddit/details',
49+
params,
50+
options,
51+
)
4152
}
4253

4354
subredditPosts(params: RedditSubredditPostsParams, options?: RequestOptions) {
44-
return this.client.get<JsonRecord>('/reddit/subreddit/posts', params, options)
55+
return this.client.get<Envelope<Post[]>>('/reddit/subreddit/posts', params, options)
4556
}
4657

4758
subredditSearch(params: RedditSubredditSearchParams, options?: RequestOptions) {
48-
return this.client.get<JsonRecord>('/reddit/subreddit/search', params, options)
59+
return this.client.get<Envelope<Post[]>>('/reddit/subreddit/search', params, options)
4960
}
5061

5162
postComments(params: RedditPostCommentsParams, options?: RequestOptions) {
52-
return this.client.get<JsonRecord>('/reddit/post/comments', params, options)
63+
return this.client.get<Envelope<Comment[]>>('/reddit/post/comments', params, options)
5364
}
5465
}

0 commit comments

Comments
 (0)