api reference
One tool. One endpoint.
The MCP server and the REST API run the same code and return the same digest. $0.02 an image post, $0.02 per minute of video, 10 free on sign-up. No key needed for a first look.
Quickstart
Claude Code, one line. It registers a single tool, digest_url, and the agent calls it by itself whenever it meets a social link it cannot open.
claude mcp add --transport http linkdigest \ https://linkdigest.dev/mcp \ --header "Authorization: Bearer ld_live_..."
Cursor
Add this to ~/.cursor/mcp.json:
{
"mcpServers": {
"linkdigest": {
"url": "https://linkdigest.dev/mcp",
"headers": { "Authorization": "Bearer ld_live_..." }
}
}
}First call over REST
curl -X POST https://linkdigest.dev/api/v1/digest \
-H "Authorization: Bearer ld_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://www.youtube.com/watch?v=CEvIs9y1uog"}'That link is a cached example, so it answers in about a second and costs nothing.
Authentication
Every request carries your key as a bearer token. Keys are created on the API keys page and shown once. A key can be given its own monthly credit cap, so one handed to a script cannot drain the account.
Authorization: Bearer ld_live_...
No key at all: the first look
A single link can be read with no key, no account and no card, and comes back as Markdown an agent can read straight away:
curl -sL https://linkdigest.dev/read/https://www.youtube.com/watch?v=CEvIs9y1uog
One uncached link per visitor; links already read are free as often as you like. YouTube is not included. A read still running answers 202 with a job id and a poll URL. The same call is available as /read/?u=<url-encoded link>.
Accepted links
Paste the link in whatever shape the app gave you. Share-button short links, full page addresses, a profile page with the post open, and the whole share-sheet text with the link buried in it all work — the first link is taken out for you. Measured 2026-09-13: 23 of 24 shapes read in full.
| platform | shapes that work |
|---|---|
| Xiaohongshu | xhslink.com/o/…, xhslink.cn/o/…, /explore/…, /discovery/item/…, /search_result/…, /user/profile/…/…, rednote.com/…; a xiaohongshu.com/login?redirectPath=… address is unwrapped to the note it carries |
| Douyin | v.douyin.com/…, douyin.com/video/…, iesdouyin.com/share/video/…, douyin.com/user/…?modal_id=…, /discover?modal_id=… |
| TikTok | vt.tiktok.com/…, tiktok.com/t/…, tiktok.com/@user/video/…, m.tiktok.com/v/….html |
| YouTube | youtube.com/watch?v=…, youtu.be/…?si=…, &t=…, m.youtube.com |
| X | x.com/…/status/…, twitter.com/…?s=…, x.com/i/status/…, mobile.twitter.com |
| weibo.com/<user>/<id>, weibo.com/status/<id>, m.weibo.cn/detail/…, m.weibo.cn/status/…; a numeric id and its short id are the same post | |
| Bilibili | b23.tv/…, bilibili.com/video/BV…, m.bilibili.com/video/BV…; share parameters (?share_source=…, spm_id_from=…) are ignored |
| WeChat articles | mp.weixin.qq.com/s/… (the short form; the long ?__biz=… form is often answered with WeChat's verification page) |
| instagram.com/reel/…, instagram.com/p/… (a carousel gives its first slide) | |
| Podcasts | xiaoyuzhoufm.com/episode/…, podcasts.apple.com/…?i=… (an episode, not the show page) |
A Xiaohongshu page address without its xsec_token is the least reliable shape — Xiaohongshu often answers it with a login wall or its “page not found” screen, though bare /explore/<id> addresses do read on some days. Copy the link from the share sheet to be sure.
When Xiaohongshu puts its captcha page in front of a note (its security layer does this for stretches of a few hours), the read is not answered on the first attempt: it waits and asks again, for up to about three minutes, before giving up. A read that is still refused then is not billed, and the browser extension reads the note from your own session instead.
Discovery
Everything above starts from a link you already have. Discovery is the step before: from a topic to a set of links, so an agent can go topic → links → digests → one page on its own. There are two routes, and they are different because the platforms are.
From a search API
For xiaohongshu, douyin, youtube, x, web the MCP tool search_links and POST /api/v1/search find posts on a topic through a web search scoped to the platform, filtered to links a digest can read. A search that finds posts costs 4 credits on Douyin and Xiaohongshu and 1 elsewhere; one that finds nothing is free. Searching is metered by credits, not by an hourly count, and free accounts also share a pool of 30 searches a day; with digest: true the results become a batch with a comparison, billed as any batch is. For Xiaohongshu this returns the notes the web has indexed, and each is read from its public page — the page a logged-out phone gets — with no login, no session and no signature of ours. It is a subset of the platform: what search engines have seen, not your feed.
From a page you can see
For your own Xiaohongshu feed, or a search the web has not indexed, open the page in your own browser and press the browser extension’s Digest the notes on this page button (on Douyin, Digest this tab on a video): it starts a batch from the links on the tab you are looking at, labelled with the page, reading as many as you choose. The agent then calls list_batches (or GET /api/v1/digest/batches), takes the newest batch_id, and passes it to compare_links for the comparison. LinkDigest never holds your session. For Xiaohongshu every search answer also carries own_browser_url, the same search in your own browser; the batch the extension starts there is named after the query, so list_batches with label set to the query finds it. In the console this is the Search my Xiaohongshu button, which waits for that batch and shows it.
In the console
The same two routes without code: the Workspace’s Find posts mode runs the search and reads what you tick, its Many links → one page mode takes a pasted list (or the batch the extension started) and returns the comparison, and the Batches page lists every batch with its label — the console’s list_batches.
Research is those routes as a conversation, and it has its own place in the console: Research in the sidebar, also the fourth tab of the Workspace. Ask in plain words — 最近30天小红书上便携咖啡机哪个好 — and an assistant decides which platforms to search and which posts to read, says what it is doing as it goes, and answers in sentences with every point linked to a numbered source. 2 credits an answer; each search and each post read is charged as it is everywhere else, and a post already read is free. A follow-up carries what was read before. It is the console only; an agent does the same thing with search_links and digest.
Research skills put the job on the screen instead of a blank box: 开店前看同行, 定价, 选品, 竞品动作, 内容选题 and 游客口碑. One click runs a template — its own searches and platforms — and its answer must end with 「这周就做」: one to three steps for this week, each tied to the source number that justifies it. Same price as any other question.
Read one link. Returns the digest, or a job id when the media takes longer than a request can wait.
| parameter | type | meaning |
|---|---|---|
| url | string, required | The link. Any shape the app gave you, including the whole share-sheet text — see Accepted links. |
| format | string | json (default), markdown, or annotation: the json plus `annotation`, the read as a timeline for a dataset — speech { start, end, text, approx }, frames { t, description, ocr }, on_screen { start, end, text }, chapters, key_points { text, quote, t, verified }. Ends are derived from the next item. The same works on GET /api/v1/digest/{jobId}?format=annotation and GET /api/v1/capture/{id}?format=annotation; a batch answers GET /api/v1/digest/batch/{batchId}?format=jsonl with one annotation per line. Added 2026-11-07. |
| translate_to | string | A language code such as en. Adds a translation block beside the original, which is never altered. One extra credit, quoted before anything runs; a cached translation is free. |
| breakdown | boolean | Viral breakdown (爆款拆解): also take the post apart — the hook, the structure as timed beats, title formula, cover text, call to action, audience and a reusable template. Quotes are verbatim and checked against the post; engagement and hashtags come from the platform. Written in the post's language, or translate_to. One extra credit; a cached breakdown is free. |
| ask | string | A question to answer from the post, in any language — "what products and prices does it recommend?", "does the speaker say X?". The answer comes with 1–3 quotes it rests on, each looked up in the post by code (verified true/false); found is false when the post does not address it, and the answer says so. 1 extra credit; the same question on the same link is a free cache hit. |
| insight | boolean | Consumer insight (消费者洞察): who the post speaks to, the claims it makes, the objections and pain points readers raise, purchase-intent cues ("求链接", "多少钱", "哪里买"), the products, brands and prices named, and the questions readers ask. Every item is a quote looked up by code in the post or its top comments (verified true/false, kind says where); a section the material does not support has found false and is named in missing. 1 extra credit; a cached insight is free. Added 2026-10-17. |
| max_credits | integer | Your own ceiling for this link. The engine prices the link before it spends, so anything over the ceiling costs nothing and answers 402. Exception: YouTube is read by Gemini and its length is only known after the read, so a long YouTube video is read in full and billed at most this ceiling (and at most what the longest video your plan accepts costs) instead of refused. |
| partial_ok | boolean | Read the first minutes the budget affords instead of refusing a long video. Default true on the free plan (since 2026-11-07): 10 credits read the first 9 minutes of a long video instead of a 402, and the response carries partial and read_seconds. Send false to get the whole video or a 402 that costs nothing; a monthly plan always gets the whole video or the 402. An image post the budget does not cover whole (a long WeChat article, a big note) reads its first images instead: 6 for the first credit and 6 more for each further one. A WeChat article does this even without partial_ok. |
| depth | string | full (default) or transcript. transcript reads a long video transcript-first: the whole transcript plus about one frame a minute (one per chapter when it has chapters), for 1 credit per started 2 minutes instead of 1 per minute, and up to 120 minutes on a paid plan (60 free) against 30 (10) for a full read. Quoted before anything runs, like every price. Its own cache row: a transcript-first digest is never served to a full-depth request, while a cached full read answers a transcript-first one free. A YouTube video read by Gemini has no transcript-only form: it is read and priced in full. Added 2026-10-05. script (2026-10-29) takes the speech only — no frames, no on-screen text — for 1 credit per started 2 minutes with no base credit (a 5-minute video 3, against 6 full), at the transcript-first length caps; served free by a cached transcript-first or full read. With no depth given, an ask for the words only (文案, 口播, 逐字稿, "script") is a script read. On the console and the keyless look only (2026-11-05): a free account's full read that would not fit its credits is read transcript-first and whole instead, and the digest carries auto_depth with the full read's price; an API call always gets the depth it asked for. |
Example request
curl -X POST https://linkdigest.dev/api/v1/digest \
-H "Authorization: Bearer ld_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://www.xiaohongshu.com/discovery/item/6a6bf662000000002402de7e?xsec_token=CBCe01s1uyd2C8uMx6ycYcnEwPV5_31OX1u_I19FCSd54=&xsec_source=app_share", "translate_to": "en", "max_credits": 4}'Example response
{
"platform": "xiaohongshu",
"author": "...",
"title": "...",
"posted_at": "2026-09-10",
"caption": "...",
"transcript": [{ "t": 0, "text": "..." }],
"transcript_source": "asr",
"ocr_text": ["...", "..."],
"images": [{ "description": "...", "ocr": "..." }],
"key_points": ["...", "..."],
"key_point_support": [
{ "point_index": 0, "kind": "transcript", "t": 12.4, "image_index": null, "quote": "...", "verified": true }
],
"coverage": {
"seconds_transcribed": 95, "duration_seconds": 95, "largest_frame_gap_seconds": 4.2,
"images_read": 1, "images_total": 1, "transcript_source": "asr",
"not_captured": ["comments not read"], "links_found": 1
},
"links": [
{ "url": "https://arxiv.org/abs/1706.03762", "found_in": "on_screen", "label": "", "kind": "web" }
],
"translation": { "language": "en", "key_points": ["..."] },
"degraded": [],
"cached": false,
"credits": 13
}Cached links, web pages and short posts come back as 200 straight away. A video that has to be downloaded and transcribed returns 202 with a job id. Collect it with the same key.
# 202 {"pending":true,"jobId":"abc…","poll":"/api/v1/digest/abc…","stage":"fetching"}
curl "https://linkdigest.dev/api/v1/digest/JOB_ID?wait=20" \
-H "Authorization: Bearer ld_live_..."
# 202 while it runs, 200 with the digest when it is done.
# wait=0..20 holds the request until the job finishes (at most 20 s),
# so a loop with no sleep step (Coze, a Feishu shortcut) still works.While it runs, stage says what is happening: resolving, fetching, transcribing 5:42 of audio · reading 42 frames, summarising, translating. Transcription and image reading run side by side.
Over MCP this is handled for you: if the tool reports a job id, call digest_url again with job_id and no url.
Read a video you already have. Send its sound and a few frames; the video itself is never uploaded. This is the fallback for a Douyin video whose link the embed player will not serve, and for any video already on your machine.
The one command
The CLI extracts both pieces on your machine and makes the call. Python 3.11 or newer, and ffmpeg plus ffprobe on PATH.
pip install linkdigest-mcp # needs ffmpeg + ffprobe on PATH # Save the video first — in the Douyin app: 分享 → 保存本地 linkdigest capture ~/Downloads/video.mp4 --url "https://www.douyin.com/video/…" # or, without a link: LINKDIGEST_API_KEY=ld_live_... linkdigest capture ./video.mp4 --title "My video" # see what would be sent, and charge nothing: linkdigest capture ./video.mp4 --dry-run --verbose
A link passed in place of a file is refused with the save-then-rerun steps, never fetched. Markdown goes to stdout; --json gives the digest without raw_markdown, -o out.md writes it to a file. Exit codes: 0 ok, 2 the file or the account (unreadable, bad key, out of credits, rate-limited), 3 the server or the network.
Over HTTP
Multipart form data: one audio part, one frames part per JPEG, and one meta part of JSON.
ffmpeg -i video.mp4 -vn -ac 1 -ar 16000 -f mp3 -b:a 24k audio.mp3
ffmpeg -ss 4 -i video.mp4 -frames:v 1 -vf "scale='min(720,iw)':-2" -q:v 6 frame_00.jpg
curl -X POST https://linkdigest.dev/api/v1/capture \
-H "Authorization: Bearer ld_live_..." \
-F "audio=@audio.mp3;type=audio/mpeg" \
-F "frames=@frame_00.jpg;type=image/jpeg" \
-F 'meta={"page_url":"https://www.douyin.com/video/…","duration_seconds":65,"frame_times":[4]}'
# 202 {"id":"jobid.signature","poll":"/api/v1/capture/jobid.signature","retryAfter":3}
curl "https://linkdigest.dev/api/v1/capture/jobid.signature" -H "Authorization: Bearer ld_live_..."
# 202 while it runs, 200 {"status":"done","credits":13,"view_url":"…","digest":{…}}| limit | value |
|---|---|
| duration | 3 to 620 seconds. Longer is not accepted; send the part you need. |
| audio | MP3 (16 kHz mono is plenty) or the extension's WebM, at most 3 MB. |
| frames | At most 40 JPEGs, 400 KB each. The CLI takes 36. |
| whole request | 4.5 MB. Over it answers 413 and costs nothing. |
A capture is private to the account that sent it: it is never cached under the post's key, never served to anyone else who asks for that link, and its id is signed with the account, so only that key can collect it. A video the caller already has is not a whole read of the platform and is not counted as one on platforms.
Everything else works as a link does: the same pricing by the second, the same digest fields, depth is always full. A refusal from the platform itself answers 503 with error_code platform_refused, how_to_read_now and a Retry-After header; nothing is charged.
LangChain and LlamaIndex
The Python SDK (linkdigest-mcp on PyPI, 0.2.5 or later) carries a LangChain tool and a LlamaIndex reader as optional extras; the base SDK still has no dependencies. The tool is named digest_url and passes LangChain's standard tool tests; the reader gives one Document per link with flat metadata (platform, title, author, source_url, credits).
pip install "linkdigest-mcp[langchain]" # or [llamaindex] export LINKDIGEST_API_KEY=ld_live_... from linkdigest.integrations.langchain import LinkDigestTool tool = LinkDigestTool(max_credits=20) # the ceiling is yours, not the model's from linkdigest.integrations.llamaindex import LinkDigestReader docs = LinkDigestReader(depth="script").load_data(["https://v.douyin.com/xxxx/"])
Out of credits comes back to the model as a sentence with the pay link; a removed or private post as "cannot be read, nothing charged"; a long read as a job id it collects on the next call. The same package has linkdigest.links, an offline share-text parser (linkdigest-links "<share text>"). Source and examples: github.com/jcaiagent7143-ui/linkdigest-mcp.
Many links in one call: up to 10 on the free plan, 50 on Dev and 200 on Studio. Every link is billed exactly as a single call would be: cached links are free, credits are quoted before spend, the key’s cap applies. Collect by polling, or hand over a webhook and the summary is pushed to you.
| parameter | type | meaning |
|---|---|---|
| urls | array, required | Up to 10 links on the free plan, 50 on Dev, 200 on Studio (a larger batch answers 400 with the plan's limit). Duplicates that resolve to the same post are merged. |
| webhook_url | string | Public https target. The summary is POSTed here when the last link finishes. |
| translate_to | string | Applied to every link in the batch. |
| breakdown | boolean | Take every link apart (爆款拆解), one extra credit each. |
| ask | string | Ask every link the same question, one extra credit each. |
| insight | boolean | Draw every link's consumer insight (消费者洞察), one extra credit each. |
| draft | boolean | Write one post from the batch after the comparison (成稿): three titles, a body, hashtags and cover text, in the shape of platform (xiaohongshu 图文笔记 by default, douyin 口播稿, or en), in the creator's own voice when voice carries a sample of their writing. Every number, price or quantity in it occurs in the notes, or its sentence is cut; a draft that fails that twice falls back to the notes' own titles and key points (draft.made_by = "fallback"). Returned as draft on the batch (null while it runs); +1 credit once per batch, and it implies synthesis. Added 2026-10-31. |
| synthesis | boolean | Batch synthesis (批量综合): once every link has been read, write one comparison over all of them — the themes, hooks, formats and claims they share, the objections readers raise, the gaps none of them cover — every example naming which link it came from (note, 1-based over the links read, in order) and checked against it word for word. Returned as synthesis on the batch once it is done; 1 credit per started 10 links, once per batch. A comparison that fails leaves the links as they are, sets synthesis_error and costs nothing. Added 2026-10-17. |
| partial_ok | boolean | Applied to every link: read the opening minutes (of an image post, the first images) the budget affords instead of failing the item. |
| depth | string | Applied to every link: full (default), transcript or script, as on a single call. |
| label | string | A name for the batch — a search query, the page it came from — shown by GET /api/v1/digest/batches and the MCP list_batches so one batch can be told from another. Up to 80 characters; longer is cut, never refused. Returned as label on the batch. Added 2026-10-17. |
curl -X POST https://linkdigest.dev/api/v1/digest/batch \
-H "Authorization: Bearer ld_live_..." \
-H "Content-Type: application/json" \
-d '{"urls": ["https://www.youtube.com/watch?v=CEvIs9y1uog", "https://www.xiaohongshu.com/discovery/item/6a6bf662000000002402de7e?xsec_token=CBCe01s1uyd2C8uMx6ycYcnEwPV5_31OX1u_I19FCSd54=&xsec_source=app_share"],
"webhook_url": "https://example.com/hooks/linkdigest",
"translate_to": "en",
"synthesis": true}'
# 202
{ "batch_id": "…", "status": "running", "total": 2, "done": 1,
"failed": 0, "credits_total": 0, "poll": "/api/v1/digest/batch/…",
"synthesis": null, "synthesis_credits": 0, "synthesis_error": null,
"items": [ … ] }A link that could not be read is an item with status: error and an error_code; the rest of the batch is unaffected. Batches are kept for seven days.
The synthesis object
With synthesis: true the batch stays running after the last link until the comparison is written, then carries it as synthesis:
| field | type | meaning |
|---|---|---|
| language / notes / summary | string / integer / string | The language it is written in, how many links were compared, and the comparison in a paragraph. |
| themes, hooks, formats, claims, objections, gaps | array | Each a list of { text, count, examples [{ note, quote, verified }] }: the point, how many links it was seen in, and examples — note is the 1-based number of the link among those read, quote is verbatim from it, verified is whether the engine found it there word for word. |
| missing | array | The sections the material could not support. |
| raw_markdown | string | The whole comparison as Markdown. |
Where the batch stands. Add ?include=digests to embed every finished digest, keyed by the url you sent.
curl "https://linkdigest.dev/api/v1/digest/batch/BATCH_ID?include=digests" \ -H "Authorization: Bearer ld_live_..." # 202 while links run, 200 when done
A competitor report
A batch sent with insight: true and synthesis: true is a competitor report: every note’s consumer insight plus one comparison across them. Over the API it is that batch, nothing new to call; in the console it is the Competitor report switch, and its batch page reads as a report to print, save as PDF or copy. It runs on credits from any plan: each note at its own price (typically 2) plus 1 for its insight, plus the comparison, so 3 notes come to about 10 credits. The console sizes it to the plan: 3 notes free, 20 on Dev, 50 on Studio.
This account’s last batches, newest first — including the ones the browser extension started. ?limit=1..10, default 10. One line per batch: batch_id, label, created_at, status, done / total / failed, credits_total and synthesis (ready, pending, failed, or null when none was asked for). Starts nothing, costs nothing.
curl "https://linkdigest.dev/api/v1/digest/batches?limit=3" \
-H "Authorization: Bearer ld_live_..."
{ "batches": [
{ "batch_id": "…", "label": "护肤 早C晚A — 小红书搜索", "created_at": "2026-10-17T09:12:03.000Z",
"status": "done", "total": 8, "done": 8, "failed": 0, "credits_total": 9, "synthesis": "ready" }
] }Find posts on a topic on xiaohongshu, douyin, youtube, x, web. A search that finds posts costs 4 credits on douyin and xiaohongshu, 1 elsewhere, and credits says what it cost; one that finds nothing is free (402 when the balance is short). Searching is metered by credits, not by an hourly count. Add digest: true to read the results as one batch with a comparison in the same call.
| parameter | type | meaning |
|---|---|---|
| query | string, required | The topic, in any language, up to 200 characters. |
| platform | string, required | xiaohongshu, douyin, youtube, x, web. youtube returns watch and shorts links only, x returns status links only; channel and profile pages are dropped; xiaohongshu returns note pages only (explore / discovery/item), the ones the web has indexed. douyin returns video and 图文 pages only (douyin.com/video, /note), the ones the web has indexed, each read through Douyin's own player. |
| limit | integer | How many links, 1 to 10. Default 5. |
| digest | boolean | Also start a batch over the results with synthesis: true, labelled with the query. The response then carries batch_id and the batch summary, and answers 202; the batch is billed exactly as POST /digest/batch would be, on top of the search (4 credits on douyin and xiaohongshu, 1 elsewhere, when it finds posts). The search keeps the results past limit in reserve: a post that will not read (a Xiaohongshu note without its share token, a video the platform will not play) is skipped, costs nothing, and the next result is read in its place — the summary's skipped and note_zh say so. The summary's ranked lists the posts hottest first (heat = likes + 2 × saves + comments, as of each read). draft: true also writes one post from the results (+1 credit; voice carries a sample of your writing). |
| insight | boolean | With digest: draw every post's consumer insight, +1 credit a link. |
| breakdown | boolean | With digest: take every post apart, +1 credit a link. |
curl -X POST https://linkdigest.dev/api/v1/search \
-H "Authorization: Bearer ld_live_..." \
-H "Content-Type: application/json" \
-d '{"query": "retinol beginners", "platform": "youtube", "limit": 5, "digest": true}'
# 202
{ "query": "retinol beginners", "platform": "youtube", "credits": 1,
"results": [ { "url": "https://www.youtube.com/watch?v=…", "title": "…", "snippet": "…" } ],
"batch_id": "…", "batch": { "status": "running", "label": "retinol beginners", … } }
# platform: "douyin" (since 2026-10-21) → the video pages the web has indexed,
# newest first, each with the day it went up (read from its id)
{ "query": "露营装备", "platform": "douyin", "credits": 2,
"results": [ { "url": "https://www.douyin.com/video/7611810685512633654", "title": "…", "snippet": "…", "posted_at": "2026-02-28" } ] }Without a search key on the server the route answers 503 with error_code: search_unavailable; nothing else is affected.
The daily brief (每日稿, added 2026-10-31): watch a topic, and every day at hour_utc its hottest new posts on platform (xiaohongshu or douyin) are read (reads of them, 5 by default, up to 20 within the plan’s batch size), ranked and compared, and one post is written from them in draft_platform’s shape — in your voice when voice carries a sample — then mailed to the account’s address with a share link. Each run is the batch it starts: search + reads + comparison + draft, about 8–12 credits; a day with nothing new costs nothing, and an account that cannot pay is mailed instead and tried again the next day. The first run starts at once. Free plans watch 1 topic, Dev 5, Studio 20 (402 watch_limit past that). GET lists them; PATCH /api/v1/watch/{id} takes status (active / paused), run_now, voice and hour_utc; DELETE removes one. Nothing is published for you: the post is yours to paste, or to prefill with the extension.
curl -X POST https://linkdigest.dev/api/v1/watch \
-H "Authorization: Bearer ld_live_..." -H "Content-Type: application/json" \
-d '{"query": "露营装备", "platform": "xiaohongshu", "voice": "家人们,我是刚开始露营的小白……"}'
{ "watch_id": "…", "query": "露营装备", "platform": "xiaohongshu", "draft_platform": "xiaohongshu",
"status": "active", "hour_utc": 1, "next_run_at": "…", "runs": 0, "last": null }The Pain-Point Radar (痛点雷达, added 2026-11-07): one topic — a product, category or brand — and the run answers three things. What people search for after it: the suggestions Baidu, Taobao and Bing give (market cn) or Bing and YouTube (global), grouped by need. What the posts about it and their top comments complain about, want and ask: 12, 24 or 48 posts (size quick, standard, deep) from Xiaohongshu and Douyin (cn) or YouTube and TikTok (global), read with the consumer insight, every checked finding clustered into pain points, needs, purchase blockers, products, unanswered questions and gaps — each with count_posts, the number of posts it came from, counted by code, and quotes checked word for word. And, on a re-run of the same topic, what changed: new and rising points, new search suggestions. It answers 202 at once; poll GET /api/v1/radar/{id} (202 while it searches, reads and aggregates, 200 when done; ?format=md for the report as Markdown, and a share_url anyone can open for 30 days).
Billed as its parts: the two searches, each post at its own price with insight (+1), and the aggregation, 3 credits for 12 posts up to 6 for 48 — about 47 credits in all for a quick run, said as estimate before it starts, and fewer posts when the credits cover fewer. Fewer than 3 posts read: the aggregation is not charged. Without the credits for a minimal radar: 402 insufficient_credits with the pay links. weekly: true runs it again every week and mails what changed; PATCH turns that off. POST /api/v1/radar/preview answers the search suggestions alone, for 1 credit.
curl -X POST https://linkdigest.dev/api/v1/radar \
-H "Authorization: Bearer ld_live_..." -H "Content-Type: application/json" \
-d '{"topic": "扫地机器人", "market": "cn", "size": "quick"}'
{ "radar_id": "…", "status": "searching", "estimate": 47, "poll": "https://linkdigest.dev/api/v1/radar/…" }
curl https://linkdigest.dev/api/v1/radar/<radar_id> -H "Authorization: Bearer ld_live_..."
{ "status": "done", "posts_read": 12, "credits_total": …,
"radar": { "sections": { "pain": [ { "label": "…", "count_posts": 5, "share": 0.42,
"quotes": [ { "text": "…", "note": 3, "kind": "comment", "verified": true } ] } ], … } },
"diff": { "since": null, "new_terms": [], … }, "share_url": "https://linkdigest.dev/r/…" }What this key’s account has left: plan, rolling window, credits used and remaining, pack credits, the key’s own monthly cap, and its webhook_secret. Starts nothing, costs nothing. failed lists this key’s last 10 reads that did not go through, each with the answer that names its fix (a Xiaohongshu link needs its share token; a Douyin video its player will not serve can be uploaded). Failed reads cost nothing.
curl https://linkdigest.dev/api/v1/usage -H "Authorization: Bearer ld_live_..."
{ "plan": "dev", "unit": "credits",
"period": { "window": "30d", "since": "…", "renewsAt": "…" },
"used": 104, "limit": 500, "remaining": 646, "pack_credits": 250,
"key": { "hint": "…", "monthly_cap": 30, "used": 13, "remaining": 17,
"webhook_secret": "…" },
"max_duration_seconds": 1800,
"failed": [ { "at": "…", "url": "https://www.xiaohongshu.com/explore/…",
"error_code": "unreadable", "message": "This Xiaohongshu link has no share token, …" } ] }Public, no key. For each platform: its state, an ok / degraded / unknown status, and when a real post on it was last read in full.
Webhooks
The webhook receives the same summary as the poll, as JSON, with two headers. Only https targets on public hosts are accepted. Delivery is at-least-once: a non-2xx answer is retried after 1 m, 5 m, 30 m and 2 h, five attempts in all.
| header | value |
|---|---|
| X-LinkDigest-Batch | The batch id. |
| X-LinkDigest-Signature | t=<unix seconds>,v1=<hex> — v1 is HMAC-SHA256 of `${t}.${raw body}` under the key's webhook secret. |
The secret is in GET /api/v1/usage under key.webhook_secret; it is derived from the key and retires with it.
Verifying a delivery
// Node
const [t, v1] = sig.split(",").map((p) => p.split("=")[1]);
const expect = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
crypto.timingSafeEqual(Buffer.from(expect), Buffer.from(v1));
# Python
t, v1 = (p.split("=", 1)[1] for p in sig.split(","))
expect = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), "sha256").hexdigest()
hmac.compare_digest(expect, v1)Response fields
| field | type | meaning |
|---|---|---|
| platform | string | Which site the link resolved to. |
| author | string | Post author's display name. |
| title | string | Post title, in its original language. |
| posted_at | string | Publication date, YYYY-MM-DD. |
| caption | string | The post's own caption or description text. |
| transcript | array | { t, text } — seconds and speech. Empty when the post has no speech. |
| transcript_source | string | native_captions, asr, or none — how the transcript was obtained. |
| ocr_text | array | Text visible on screen: burned-in captions, UI labels, slide text. |
| on_screen | array | Video only: { t, text } — on-screen text with roughly when it appeared (to a keyframe, a few seconds). |
| images | array | { description, ocr } — one entry per image on image posts. |
| key_points | array | The substantive takeaways, in the post's original language. |
| stats | object | { likes, comments, shares, collects, plays, as_of } — the platform's counts when the post was read (Douyin, Xiaohongshu, Weibo). A snapshot; null when not reported. |
| tags | array | The post's hashtags. |
| comments | array | { text, likes, replies, posted_at, reply_texts } — the first comments the post's public share page served with it (Xiaohongshu and Weibo: the top few), read with no second request and no login; never the whole thread, and coverage says how many of the post's comments they are. Added 2026-10-17: absent on links cached before then. |
| chapters | array | { t, title, summary } — chapters the author or the platform marked on the video, never inferred. summary is Douyin's one line for a chapter it generated, else empty. On a partial read, only the chapters in the part read. Added 2026-10-05, like the three below: absent on links cached before then. |
| chapter_source | string | Who marked the chapters: author, platform (generated by Douyin), or empty. |
| outline | array | { start, end, title, summary[], quote, generated } — a timed outline of a long video: when the transcript runs 8 minutes or more and the video has no marked chapters, 5 to 20 sections in order that cover everything read. The model proposes the cuts; code makes them contiguous, snaps every start and end to a transcript line's own time and keeps a quote only when it is found in that section. generated is model, or fallback (equal stretches titled by their opening words) when no plan could be kept. Included in the read, no extra credit. Added 2026-11-10; absent on links cached before then. |
| parts | object | { count, current, items: [{ p, title, seconds }] } — a multi-part Bilibili video's own parts (分P), at most 200 listed; null otherwise. current is the part this digest is. Each part is its own read: send the same link with ?p=<number>, one at a time or as a batch. Added 2026-11-10. |
| category | array | The platform's own category, broadest first: 财经 › 投资理财 › 股票 on Douyin, Education on YouTube. |
| music | string | The soundtrack, title — artist, when it is not the poster's own voice. Empty otherwise. |
| breakdown | object | Present only when you asked for breakdown: { language, format, hook { spoken, on_screen, visual, why }, beats [{ t, image, label, summary, quote }], title_formula, cover_text, cta, audience, tags, engagement, template, remix_ideas, missing }. |
| answer | object | Present only when you sent ask: { question, answer, found, support [{ kind, t, image_index, quote, verified, approx }] }. Shown as an Answer section in raw_markdown. |
| insight | object | Present only when you sent insight: { language, audience, claims, objections, purchase_intent, products, questions, comments_read, missing }. Each section is { found, items [{ text, quote, kind, verified, t, image_index, approx }] }; kind is caption, transcript, on_screen, image, chapter or comment. Added 2026-10-17. |
| translation | object | Present only when you asked for translate_to. Title, caption, key points, transcript and on-screen text in that language. A transcript past 24,000 characters is translated from its start only, and note says how far ("translation covers the first 27:12 of 60:00 of the transcript"), as does coverage.not_captured. Added 2026-10-06. |
| raw_markdown | string | The whole digest pre-rendered as Markdown, including any breakdown and translation. |
| credits | integer | What this call cost. Zero on a cache hit. |
| degraded | array | Anything that did not work fully, in plain words. Empty on a clean run. |
| duration_seconds | number | Length of the media as fetched. Absent for image and text posts. |
| read_seconds | number | How much of it was read. Equal to duration_seconds on a complete read. |
| partial | object | Present only when partial_ok was set and the budget did not cover the whole video: { read_seconds, duration_seconds, read_credits, full_credits, transcript_credits }. A full read later is a fresh digest at full_credits; transcript_credits is what the whole video costs read transcript-first (depth transcript), null when it is past that length cap too. An image post read in part (since 2026-10-06) has read_seconds and duration_seconds 0; image_count and images_read say how far it got. |
| depth | string | How the video was read: full, transcript (transcript-first: the whole transcript and sparse frames, which coverage.not_captured says) or script (the speech only). A YouTube video read by Gemini is always full. Absent on links cached before 2026-10-05, all of which were full reads. |
| image_count | integer | How many images the post has. Absent for posts without images, and on links cached before 2026-10-05. |
| images_read | integer | How many of them were taken and priced: every one up to 60. Past that, the first 60; with partial_ok, the first ones the budget covers. degraded names the rest. Taken is not read: an image whose download or vision read failed still counts here, and coverage.images_read is how many were actually read. |
| frames | array | Video only: every sampled frame with its time, a short description and its text: [{ t, description, ocr }]. Added 2026-11-07: absent on links cached before then. |
| coverage | object | The receipt for what was captured, computed by the engine's code, never by a model: { seconds_transcribed, duration_seconds, largest_frame_gap_seconds, images_read, images_total, transcript_source, not_captured, links_found }. For video, how many seconds of speech are in the transcript and the longest stretch with no sampled frame; for image posts, how many images came back read; not_captured lists in plain words what was not read, e.g. "minutes 12:00-43:58 not read (budget)", "3 of 21 images skipped", "comments not read". Null where a figure does not apply. Added 2026-10-05, like key_point_support: absent on links cached before then. |
| key_point_support | array | { point_index, kind, t, image_index, quote, verified } — one short quote per key point, with where it is: kind is transcript, on_screen, image, caption or chapter; t is seconds into the video. point_index and image_index are 0-based indexes into key_points and images. The engine looks each quote up in the post; verified is false when it is not there word for word, and such a row is kept, not dropped. |
| links | array | { url, found_in, label, kind } — the links the post points to, found by the engine's code and never opened: reading one is a separate digest at its own price, if you choose it. found_in is caption, description (YouTube), wechat_read_more (a WeChat article's 阅读原文), qr_code (decoded from the images and frames the read downloaded), on_screen (a well-formed URL in on-screen or image text) or chapter. kind is web, payment (WeChat Pay, Alipay…), group_invite (WeChat, QQ, Telegram groups…), app_deeplink or other; label names it when known ("WeChat Pay", "阅读原文") or carries the words before it in the post. Deduplicated, at most 20; coverage.links_found counts them all. Added 2026-10-05: absent on links cached before then. |
Status codes
| code | meaning |
|---|---|
| 200 | The digest is in the body. |
| 202 | Still working. The body carries jobId, poll and stage. |
| 400 | The request was malformed — usually a url that is not a link. |
| 401 | Missing or revoked API key. |
| 402 | Your allowance is used up, or the link costs more than max_credits or than what is left. Nothing was spent. When buying would help, the body carries starter_url (the smallest pack), buy_url (a one-off credit pack), subscribe_url (Dev, by card) and pass_url (the Dev pass: Dev for 30 days by Alipay, ¥65 paid once, it does not renew by itself) — each opens payment for this key's account, no sign-in — and offer_zh, the same offer as one Chinese sentence to show the person. |
| 413 | A capture is over the upload limit (4.5 MB in all, 3 MB of sound). Nothing was spent. |
| 422 | The link could not be fetched — blocked, removed, or login-walled. error_code not_served means the platform would not serve this one post through the surface we read (Douyin's embed player: core_dep, or its 当前内容无法播放 notice) without saying the post is gone; nothing was charged, and the message names the routes that still read it — save the video in the app and upload it, or read your own tab with the extension. |
| 429 | Too many requests, this key's own monthly cap is used up, or the account's credits are held by reads still running — then the body lists them under running, each with the poll URL that collects it and frees its credits. A read holds its own price once the link has been fetched, so reads run side by side while credits last; sending the same POST for a link you are already reading joins that read. Honour Retry-After. Cached links never count. |
| 503 | The platform itself is refusing automated reads right now (error_code platform_refused). Not billed, and not your link. how_to_read_now says what to do instead — read a saved file or your own tab; Retry-After says when the link may work again. |
| 502 | The digest engine failed. Safe to retry. |
Limits
| limit | value |
|---|---|
| Links per batch | 10 free, 50 Dev, 200 Studio |
| Requests per hour | 60 per account (300 on Studio) |
| Search billing | 4 credits on Douyin and Xiaohongshu, 1 elsewhere, 0 when nothing is found; no hourly cap. Free accounts share 30 searches a day |
| Batch retention | 7 days |
| Cached links | Free, and never counted |
| Free allowance | 10 credits, once per person — they do not renew. A new account opened from a network where another account used its free credits up in the last 7 days starts without them. |
What LinkDigest will not do is on the platforms page, in writing: X long-form articles expose only their lead image, an Instagram carousel gives only its first slide, and a Bilibili video the embed player will not serve is refused rather than guessed at. Anything that could not be read is named in degraded, and a digest that read nothing is not charged.
How each platform is opened — embed players, residential exits, what is stored and for how long — is on data sourcing & compliance.