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.

Base URL https://linkdigest.devAuth Authorization: Bearer ld_live_…

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_...
POST/api/v1/digest

Read one link. Returns the digest, or a job id when the media takes longer than a request can wait.

parametertypemeaning
urlstring, requiredThe link. Any shape the app gave you, including the whole share-sheet text — see Accepted links.
formatstringjson (default) or markdown.
translate_tostringA 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.
breakdownbooleanViral 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.
askstringA 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.
max_creditsintegerYour 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_okbooleanRead the first minutes the budget affords instead of refusing a long video. Default false: a video over max_credits or the plan's length cap answers 402 and costs nothing. When true, the response carries partial and read_seconds. 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.
depthstringfull (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.

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
}
GET/api/v1/digest/{job_id}

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.

POST/api/v1/digest/batch

Up to 50 links in one call. 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.

parametertypemeaning
urlsarray, requiredUp to 50 links. Duplicates that resolve to the same post are merged.
webhook_urlstringPublic https target. The summary is POSTed here when the last link finishes.
translate_tostringApplied to every link in the batch.
breakdownbooleanTake every link apart (爆款拆解), one extra credit each.
partial_okbooleanApplied to every link: read the opening minutes (of an image post, the first images) the budget affords instead of failing the item.
depthstringApplied to every link: full (default) or transcript, as on a single call.
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"}'

# 202
{ "batch_id": "…", "status": "running", "total": 2, "done": 1,
  "failed": 0, "credits_total": 0, "poll": "/api/v1/digest/batch/…",
  "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.

GET/api/v1/digest/batch/{batch_id}

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
GET/api/v1/usage

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.

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 }
GET/api/v1/health/platforms

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.

headervalue
X-LinkDigest-BatchThe batch id.
X-LinkDigest-Signaturet=<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

fieldtypemeaning
platformstringWhich site the link resolved to.
authorstringPost author's display name.
titlestringPost title, in its original language.
posted_atstringPublication date, YYYY-MM-DD.
captionstringThe post's own caption or description text.
transcriptarray{ t, text } — seconds and speech. Empty when the post has no speech.
transcript_sourcestringnative_captions, asr, or none — how the transcript was obtained.
ocr_textarrayText visible on screen: burned-in captions, UI labels, slide text.
on_screenarrayVideo only: { t, text } — on-screen text with roughly when it appeared (to a keyframe, a few seconds).
imagesarray{ description, ocr } — one entry per image on image posts.
key_pointsarrayThe substantive takeaways, in the post's original language.
statsobject{ likes, comments, shares, collects, plays, as_of } — the platform's counts when the post was read (Douyin, Xiaohongshu). A snapshot; null when not reported.
tagsarrayThe post's hashtags.
chaptersarray{ 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_sourcestringWho marked the chapters: author, platform (generated by Douyin), or empty.
categoryarrayThe platform's own category, broadest first: 财经 › 投资理财 › 股票 on Douyin, Education on YouTube.
musicstringThe soundtrack, title — artist, when it is not the poster's own voice. Empty otherwise.
breakdownobjectPresent 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 }.
answerobjectPresent only when you sent ask: { question, answer, found, support [{ kind, t, image_index, quote, verified, approx }] }. Shown as an Answer section in raw_markdown.
translationobjectPresent 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_markdownstringThe whole digest pre-rendered as Markdown, including any breakdown and translation.
creditsintegerWhat this call cost. Zero on a cache hit.
degradedarrayAnything that did not work fully, in plain words. Empty on a clean run.
duration_secondsnumberLength of the media as fetched. Absent for image and text posts.
read_secondsnumberHow much of it was read. Equal to duration_seconds on a complete read.
partialobjectPresent 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.
depthstringHow the video was read: full, or transcript (transcript-first: the whole transcript and sparse frames, which coverage.not_captured says). A YouTube video read by Gemini is always full. Absent on links cached before 2026-10-05, all of which were full reads.
image_countintegerHow many images the post has. Absent for posts without images, and on links cached before 2026-10-05.
images_readintegerHow 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.
coverageobjectThe 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_supportarray{ 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.
linksarray{ 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

codemeaning
200The digest is in the body.
202Still working. The body carries jobId, poll and stage.
400The request was malformed — usually a url that is not a link.
401Missing or revoked API key.
402Your 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 buy_url (a one-off credit pack) and subscribe_url (Dev) — each opens checkout for this key's account, no sign-in.
422The link could not be fetched — blocked, removed, or login-walled.
429Too many requests, or this key's own monthly cap is used up. Honour Retry-After. Cached links never count.
502The digest engine failed. Safe to retry.

Limits

limitvalue
Links per batch50
Requests per hour60 per account
Batch retention7 days
Cached linksFree, and never counted
Free allowance10 credits, once per account — they do not renew

What LinkDigest will not do is on the platforms page, in writing: Bilibili answers HTTP 412 to our address, X long-form articles expose only their lead image, Instagram is wired but unverified. Anything that could not be read is named in degraded, and a digest that read nothing is not charged.