├── .gitignore
├── Dockerfile
├── bun.lockb
├── package.json
├── public
└── favicon.ico
├── readme.md
├── src
├── api
│ └── index.ts
├── lib
│ ├── consts.ts
│ ├── env.ts
│ ├── fetch.ts
│ ├── index.ts
│ └── map.ts
└── types
│ └── threads-api.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM oven/bun:0.6.13
2 | WORKDIR /app
3 | COPY package.json package.json
4 | COPY bun.lockb bun.lockb
5 | RUN bun install
6 | COPY . .
7 | EXPOSE 3000
8 | ENTRYPOINT ["bun", "run", "src/api/index.ts"]
9 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/threads-api/32afc6fcbd84792451a1af8a8014efa648f48928/bun.lockb
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.103",
3 | "name": "threads-api",
4 | "devDependencies": {
5 | "bun-types": "latest"
6 | },
7 | "dependencies": {
8 | "hono": "^3.3.0"
9 | },
10 | "scripts": {
11 | "start:api": "bun run src/api/index.ts",
12 | "watch:api": "bun run --watch src/api/index.ts"
13 | },
14 | "module": "src/lib/index.js"
15 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/threads-api/32afc6fcbd84792451a1af8a8014efa648f48928/public/favicon.ico
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Threads API no oficial
2 |
3 |
4 | Para fines educativos
5 |
6 |
7 | ## Primeras pruebas con Curl
8 |
9 | ```sh
10 | curl 'https://www.threads.net/api/graphql' \
11 | -H 'content-type: application/x-www-form-urlencoded' \
12 | -H 'sec-fetch-site: same-origin' \
13 | -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36' \
14 | -H 'x-ig-app-id: 238260118697367' \
15 | --data 'variables={ "userID": "8242141302" }' \
16 | --data doc_id=23996318473300828
17 | ```
--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from 'hono';
2 | import { fetchThreadReplies, fetchUserProfile, fetchUserProfileThreads } from '../lib/fetch';
3 |
4 | const port = +(Bun.env.PORT ?? 3000);
5 |
6 | console.log('Initializing API server on port', port);
7 |
8 | const app = new Hono();
9 |
10 |
11 |
12 | // Endpoint to get user profiles based on userName
13 | app.get('/api/users', async (context) => {
14 | try {
15 | // Extract the userName query parameter from the request
16 | const userName = context.req.query('userName');
17 |
18 | // If the userName is missing, return a "Missing userName" error response with status code 400
19 | if (!userName) return context.text('Missing userName', 400);
20 |
21 | // Fetch the user profile using the provided userName
22 | const data = await fetchUserProfile({ userName });
23 |
24 | // Return the fetched data as a JSON response
25 | return context.json(data);
26 | } catch (error) {
27 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message
28 | return context.text('Internal Server Error', 500);
29 | }
30 | });
31 |
32 | // Endpoint to get a specific user profile based on userId
33 | app.get('/api/users/:userId', async (context) => {
34 | try {
35 | // Extract the userId from the request parameters
36 | const userId = context.req.param('userId');
37 |
38 | // If the userName is missing, return a "Missing userId" error response with status code 400
39 | if (!userId) return context.text('Missing userId', 400);
40 |
41 | // Fetch the user profile using the provided userId
42 | const data = await fetchUserProfile({ userId });
43 |
44 | // Return the fetched data as a JSON response
45 | return context.json(data);
46 | } catch (error) {
47 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message
48 | return context.text('Internal Server Error', 500);
49 | }
50 | });
51 |
52 | // Endpoint to get replies from a specific thread
53 | app.get('/api/threads/:threadId/replies', async (context) => {
54 | try {
55 | // Extract the threadId from the request parameters
56 | const threadId = context.req.param('threadId');
57 |
58 | // If the userName is missing, return a "Missing threadId" error response with status code 400
59 | if (!threadId) return context.text('Missing threadId', 400);
60 |
61 | // Fetch the thread replies using the provided threadId
62 | const data = await fetchThreadReplies({ threadId });
63 |
64 | // Return the fetched data as a JSON response
65 | return context.json(data);
66 | } catch (error) {
67 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message
68 | return context.text('Internal Server Error', 500);
69 | }
70 | });
71 |
72 |
73 | // Endpoint to get user profile threads
74 | app.get('/api/users/:userId/threads', async (context) => {
75 | try {
76 | // Extract the userId from the request parameters
77 | const userId = context.req.param('userId');
78 |
79 | // If the userName is missing, return a "Missing userId" error response with status code 400
80 | if (!userId) return context.text('Missing userId', 400);
81 |
82 | // Fetch the user profile threads using the provided userId
83 | const data = await fetchUserProfileThreads({ userId });
84 |
85 | // Return the fetched data as a JSON response
86 | return context.json(data);
87 | } catch (error) {
88 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message
89 | return context.text('Internal Server Error', 500);
90 | }
91 | });
92 |
93 |
94 |
95 |
96 |
97 | app.use('*', async (c) => {
98 | c.notFound();
99 | });
100 |
101 | export default {
102 | port,
103 | fetch: app.fetch,
104 | };
105 |
--------------------------------------------------------------------------------
/src/lib/consts.ts:
--------------------------------------------------------------------------------
1 | export const THREADS_APP_ID = "238260118697367"
2 | export const GRAPHQL_ENDPOINT = 'https://www.threads.net/api/graphql'
3 |
4 | export const ENDPOINTS_DOCUMENT_ID = {
5 |
6 | USER_PROFILE: 23996318473300828,
7 | USER_PROFILE_THREADS: 6451898791498605,
8 | USER_PROFILE_THREADS_REPLIES: 6529829603744567,
9 | USER_REPLIES: 6684830921547925,
10 | THREADS_REPLIES: 6960296570650501,
11 |
12 | }
13 |
14 | export const getMidudev = () => `pheralb`
15 |
--------------------------------------------------------------------------------
/src/lib/env.ts:
--------------------------------------------------------------------------------
1 | export const IS_DEBUG = Boolean(Bun.env.DEBUG)
--------------------------------------------------------------------------------
/src/lib/fetch.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ENDPOINTS_DOCUMENT_ID,
3 | THREADS_APP_ID,
4 | GRAPHQL_ENDPOINT,
5 | } from './consts';
6 | import { IS_DEBUG } from './env';
7 | import { ThreadsUserProfileResponse } from '../types/threads-api';
8 | import { mapUserProfile } from './map';
9 |
10 | const fetchBase = ({ documentId, variables }) => {
11 | return fetch(GRAPHQL_ENDPOINT, {
12 | method: 'POST',
13 | headers: {
14 | 'content-type': 'application/x-www-form-urlencoded',
15 | 'user-agent': 'Threads API midu client',
16 | 'x-ig-app-id': THREADS_APP_ID,
17 | 'x-fb-lsd': 'jdFoLBsUcm9h-j90PeanuC',
18 | },
19 | body: `lsd=jdFoLBsUcm9h-j90PeanuC&jazoest=21926&variables=${JSON.stringify(
20 | variables
21 | )}&doc_id=${documentId}`,
22 | }).then((response) => response.json());
23 | };
24 |
25 | export const fetchUserIdByName = ({ userName }) => {
26 | if (IS_DEBUG) console.info(`https://www.threads.net/@${userName}`);
27 |
28 | return fetch(`https://www.threads.net/@${userName}`, {
29 | headers: { 'sec-fetch-site': 'same-site' },
30 | })
31 | .then((res) => res.text())
32 | .then((html) => {
33 | const userId = html.match(/"user_id":"(\d+)"/)?.[1];
34 | return userId;
35 | });
36 | };
37 |
38 | export const fetchUserProfile = async ({
39 | userId,
40 | userName,
41 | }: {
42 | userId?: string;
43 | userName?: string;
44 | }) => {
45 | if (userName && !userId) {
46 | userId = await fetchUserIdByName({ userName });
47 | }
48 |
49 | const variables = { userID: userId };
50 | const data = (await fetchBase({
51 | variables,
52 | documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE,
53 | })) as ThreadsUserProfileResponse;
54 |
55 | return mapUserProfile(data);
56 | };
57 |
58 | export const fetchUserProfileThreads = async ({
59 | userId,
60 | userName,
61 | }: {
62 | userId?: string;
63 | userName?: string;
64 | }) => {
65 | if (userName && !userId) {
66 | userId = await fetchUserIdByName({ userName });
67 | }
68 |
69 | const variables = { userID: userId };
70 | return fetchBase({
71 | variables,
72 | documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE_THREADS,
73 | });
74 | };
75 |
76 | export const fetchUserReplies = async ({
77 | userId,
78 | userName,
79 | }: {
80 | userId?: string;
81 | userName?: string;
82 | }) => {
83 | if (userName && !userId) {
84 | userId = await fetchUserIdByName({ userName });
85 | }
86 |
87 | const variables = { userID: userId };
88 | return fetchBase({
89 | variables,
90 | documentId: ENDPOINTS_DOCUMENT_ID.USER_REPLIES,
91 | });
92 | };
93 |
94 | export const fetchThreadReplies = ({ threadId }) => {
95 | const variables = { postID: threadId };
96 | return fetchBase({
97 | variables,
98 | documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE_THREADS_REPLIES,
99 | });
100 | };
101 |
102 | export const fetchPostReplies = ({ threadId }) => {
103 | const variables = { postID: threadId };
104 | return fetchBase({
105 | variables,
106 | documentId: ENDPOINTS_DOCUMENT_ID.THREADS_REPLIES,
107 | });
108 | };
109 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './fetch';
--------------------------------------------------------------------------------
/src/lib/map.ts:
--------------------------------------------------------------------------------
1 | import { ThreadsUserProfileResponse} from '../types/threads-api'
2 |
3 | /*
4 | {"data":{"userData":{"user":{"is_private":false,"profile_pic_url":"https://scontent.cdninstagram.com/v/t51.2885-19/358174537_954616899107816_8099109910283809308_n.jpg?stp=dst-jpg_s150x150&_nc_ht=scontent.cdninstagram.com&_nc_cat=108&_nc_ohc=s5qTOIc_KREAX8qfpDD&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfDaktW3vHUeFvaE14qoy7LmddGuAqWUh2uirC7ulm_TsQ&oe=64B34341&_nc_sid=10d13b","username":"midu.dev","hd_profile_pic_versions":[{"height":320,"url":"https://scontent.cdninstagram.com/v/t51.2885-19/358174537_954616899107816_8099109910283809308_n.jpg?stp=dst-jpg_s320x320&_nc_ht=scontent.cdninstagram.com&_nc_cat=108&_nc_ohc=s5qTOIc_KREAX8qfpDD&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfBUgVik0k-VaqXmyuuJUp6bEmAyDHIkkB3ssbnHYwGg_A&oe=64B34341&_nc_sid=10d13b","width":320},{"height":640,"url":"https://scontent.cdninstagram.com/v/t51.2885-19/358174537_954616899107816_8099109910283809308_n.jpg?stp=dst-jpg_s640x640&_nc_ht=scontent.cdninstagram.com&_nc_cat=108&_nc_ohc=s5qTOIc_KREAX8qfpDD&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfCG0VVjm58zezRMrUgG_HlTuOL0MlMMsUpGRDgn4CrMiA&oe=64B34341&_nc_sid=10d13b","width":640}],"is_verified":false,"biography":"👨💻 Ingeniero de Software + JavaScript\n⌨️ Aprende Programación conmigo\n🏆 Google Expert + GitHub Star\n🙌 Comparto recursos y tutoriales","biography_with_entities":null,"follower_count":34756,"profile_context_facepile_users":null,"bio_links":[{"url":"https://twitch.tv/midudev"}],"pk":"8242141302","full_name":"midudev • Programación y Desarrollo JavaScript","id":null}}},"extensions":{"is_final":true}}
5 | */
6 |
7 | export const mapUserProfile = (rawResponse: ThreadsUserProfileResponse) => {
8 | const userApiResponse = rawResponse?.data?.userData?.user
9 | if (!userApiResponse) return null
10 |
11 | const { username, is_verified, biography, follower_count, bio_links, pk: id, full_name, hd_profile_pic_versions, profile_pic_url } = userApiResponse
12 |
13 | const profile_pics = [{
14 | height: 150,
15 | width: 150,
16 | url: profile_pic_url
17 | }, ...hd_profile_pic_versions]
18 |
19 | return {
20 | id,
21 | username,
22 | is_verified,
23 | biography,
24 | follower_count,
25 | bio_links,
26 | full_name,
27 | profile_pics
28 | }
29 | }
--------------------------------------------------------------------------------
/src/types/threads-api.ts:
--------------------------------------------------------------------------------
1 | export type ThreadsUserProfileResponse = {
2 | data: Data;
3 | extensions: Extensions;
4 | }
5 |
6 | export type Data = {
7 | userData: UserData;
8 | }
9 |
10 | export type UserData = {
11 | user: User;
12 | }
13 |
14 | export type User = {
15 | is_private: boolean;
16 | profile_pic_url: string;
17 | username: string;
18 | hd_profile_pic_versions: HDProfilePicVersion[];
19 | is_verified: boolean;
20 | biography: string;
21 | biography_with_entities: null;
22 | follower_count: number;
23 | profile_context_facepile_users: null;
24 | bio_links: BioLink[];
25 | pk: string;
26 | full_name: string;
27 | id: null;
28 | }
29 |
30 | export type BioLink = {
31 | url: string;
32 | }
33 |
34 | export type HDProfilePicVersion = {
35 | height: number;
36 | url: string;
37 | width: number;
38 | }
39 |
40 | export type Extensions = {
41 | is_final: boolean;
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext"],
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | // "bun-types" is the important part
8 | "types": ["bun-types"]
9 | }
10 | }
--------------------------------------------------------------------------------