Skip to content

Playground and tools

Three console pages let you learn this system by using it: /playground drives any read endpoint from the browser and tells you whose fault a failure was, /tools exposes the primitives the service uses on every request, and /docs is the API reference generated by the instance you are actually running. After this page you should be able to make a call, read the failure when it fails, reproduce the exact platform request outside this instance, and take a signature apart.

Page Path What it does What it costs
Playground /playground Calls the real read endpoints with a form, then shows the normalized result, the raw platform payload, the failure facts and copy-ready snippets A real upstream request and an identity, unless the answer is cached
Building blocks /tools Sign a URL, decode a signature, parse a link, batch-parse a list, mint a guest identity Nothing at all, except minting — which launches a browser
API docs /docs Swagger UI for this instance’s own OpenAPI document, in the console’s language Nothing

They are separate because their costs are separate. Everything on /tools except the last tab is arithmetic over strings you supply: no network, no identity, no row in the request log. Everything on /playground goes through the scheduler and spends something. Keeping the cheap things where they cannot accidentally become expensive is the point of the split.

All three are console pages behind a login. See Console overview for the shell, the navigation and the roles.


The page fills the viewport as three resizable panes rather than one scrolling column:

  1. Endpoints — the catalogue, and below it this session’s history.
  2. Request — the form for the selected endpoint.
  3. Response — status, payloads, failure facts, signature, snippets.

The splitter positions are remembered per browser under dtk.split.playground. Default widths are 16 / 42 / 42 percent, and the panes will not shrink below 170 / 320 / 320 pixels.

Above the panes is an address bar showing the method and the URL exactly as it will be called, query string included, rebuilt as you type. The Send button lives there, next to the URL it will call. Pressing Enter in the form sends too.

Response panels are collapsed by default and opened deliberately — one large raw payload would otherwise push everything below it off the screen. The failure panel is the exception and is never collapsed, because on a failed call it is the entire reason to be on the page.

Ten entries. The first is platform-agnostic; the other nine take a platform selector at the top of the form and are addressed as /api/v1/{platform}/....

Entry Method Path Scheduler endpoint
Parse any link POST /api/v1/parse decided after the link is expanded
Post detail GET /api/v1/{platform}/video {platform}.content_detail
Post comments GET /api/v1/{platform}/video/comments {platform}.comments
Comment replies GET /api/v1/{platform}/video/comments/replies {platform}.comment_replies
Author profile GET /api/v1/{platform}/user {platform}.author_profile
Author posts GET /api/v1/{platform}/user/posts {platform}.author_posts
Author likes GET /api/v1/{platform}/user/likes {platform}.author_likes
Mix / playlist posts GET /api/v1/{platform}/mix/posts {platform}.mix_posts
Author followers GET /api/v1/{platform}/user/followers {platform}.author_followers
Author following GET /api/v1/{platform}/user/following {platform}.author_following

The last two rows are TikTok only. The registry declares tiktok.author_followers and tiktok.author_following and no Douyin equivalent, so asking for either with the platform selector on Douyin is refused at the API boundary with UNSUPPORTED_CONTENT (400): no request id, and no scheduler endpoint of that name for the failure panel to match. The asymmetry is the platform’s own — Douyin answers “not signed in” for a follow graph even to a healthy guest identity, and a route that submitted the task anyway would hand back an empty page and let you conclude the author has no followers. The Playground starts on Douyin, so this is what you get if you click one of them first.

The scheduler endpoint name is what the token bucket and the circuit breaker are keyed on, and it is the name the failure panel and the health table use. POST /api/v1/parse has no fixed one because the worker only learns which endpoint it needs after following the link.

The catalogue is a hand-written list of the read endpoints, not a rendering of the OpenAPI document — the rest of the API (identities, downloads, settings, archive) is not here. Use /docs or the REST API guide for those.

Fields are grouped into three sections.

Target. What you are asking about. Where two fields are alternatives — url or aweme_id, url or sec_user_id — exactly one has to be filled in, and the form says so on both if neither is. Some endpoints have a genuinely required field instead: comment_id for replies, mix_id for a collection.

Paging. cursor and count, on the endpoints that page. count is capped at 50 by the form and by the API.

Request. The envelope parameters every read endpoint accepts:

Parameter Type Console default What it does
include_raw boolean on Also return the platform’s own untouched payload. The API’s own default is off; the console turns it on because comparing the two is what this page is for. It is large — a single post runs to hundreds of kilobytes.
wait number empty Hold the connection this many seconds waiting for the task. Empty or 0 returns a task id immediately. The form allows 0–30; the instance ceiling is api.max_wait_seconds (default 30) and asking for more is a 400.
proxy text empty Send the upstream request through this egress. Refused unless an administrator has set security.request_proxy to public or any.
identity picker any Send as one named identity and no other. Needs identity:manage (or admin) and an operator role.
refresh boolean off Ignore the response cache and any identical task already in flight, and ask upstream again.
explain boolean off Report the request as it actually went out. See below.

The identity field is a picker rather than a text box, because the value is a UUID nobody types from memory and the real question is “which of my accounts”. The list shows the full id, the identity’s state and whether it is logged in. Retired identities are excluded — retirement wipes the jar, so there is nothing left to send as — and on a platform-scoped endpoint only that platform’s identities are offered. Switching platform clears a pin that no longer applies, rather than letting you send a Douyin identity to a TikTok endpoint and read the 400 that comes back.

If the picker is empty, the caller cannot list identities. That is not a bug on this page: the same scope gates listing them and using one.

The proxy field is offered even on an instance where security.request_proxy is deny, which is the default. That is deliberate — a refusal that names the setting is more use than a field that does not exist. See Configuration.

refresh is worth understanding before you reach for it. Two separate mechanisms make a repeat call free: the task layer joins an identical run that is already going or has just finished, and the fetch layer caches the shaped body. refresh turns off both, and costs an identity and a real upstream request. The cached answer’s lifetime depends on what was asked for and is configurable:

Setting Default Applies to
cache.content_ttl 1800 s (30 minutes) one post
cache.author_ttl 900 s (15 minutes) a profile
cache.list_ttl 300 s (5 minutes) anything paged

Validation is client-side first: required fields, one-of groups, numbers, and ranges are checked before anything is sent, so a typo costs nothing.

The console calls with its session cookie. The API is async-first: a call without wait returns 202 with a task id, and the console polls the task for you. The HTTP request times out after 60 seconds; polling a task gives up after 180. A Cancel button appears while a call is running.

When it settles, a strip across the top of the response pane reports:

Item Meaning
HTTP <status> The status of the final HTTP response
Duration Measured in the browser, from click to answer — includes polling
Server meta.duration_ms, the time the worker recorded for the upstream call
Cache hit Whether the answer came from the response cache (success only)
Request id The correlation id, copyable. This is the value to search for on the Logs page

The two durations are shown separately on purpose. A long browser duration with a short server duration means you spent the time in the queue, not on the platform.

On success, four collapsible panels:

  • Normalized result — the payload with every raw island stripped out. This is the stable contract every client sees.
  • Raw response — the platform’s own payload, present only when include_raw was set. When it was not, the panel says so and offers Include the raw payload and run again, which sets the flag and re-sends in one click.
  • Response metadata — the whole meta object. If the payload is a page with a cursor and has_more is not false, a Fetch the next page button appears here; it fills cursor in and sends again, which spends another request.
  • Copy-ready call — see Copy-ready snippets.

The failure panel opens automatically and puts five things in one place: the stable error code (with a retry-after when the error carries one), the human message and its hint, which identity served the call, the endpoint’s circuit state, and the identity pool census. The request id sits in the status strip just above.

Those are the facts that separate the platform changed from we are out of identities:

What you see What it usually means
A code, an identity, circuit closed, healthy pool This request or this target. Read the error code.
Circuit open on this endpoint The breaker tripped on repeated failures; the endpoint is being rested. Every endpoint’s circuit is listed on the Overview page (/) — see Console overview — and Identities and proxies says what to do while one is open.
active 0 in the pool Nothing can serve any request. Calls are rejected with IDENTITY_POOL_EXHAUSTED until an identity recovers. See Identities and proxies.
No request id at all The call never reached the scheduler — it was rejected at the API boundary (scope, validation, rate limit).

The serving identity is resolved from two sources. If the run asked for an explanation and the console took the async path — no wait, or a wait that expired — the identity comes straight back with the task and appears at once. Otherwise the console looks the request id up in the request log, which lags a little; while it lags the panel says so and offers a refresh rather than pretending the answer is unknown.

Filling wait in the form is the second case even with explain on. The console reads the explanation only off the task it polled, so an answer that came back inline inside the wait window leaves the identity to the request-log lookup, and leaves the signature, cookie-jar and upstream-curl panels empty. Leave wait empty on the runs you intend to explain.

Below the response, and available even before you have made any call, is the Endpoint and pool panel: the circuit for the selected endpoint (or a count of how many of all endpoints are tripped), success rate, risk rate and sample count, and a per-state census of the identity pool. It is polled every few seconds, so it is current at the moment a call fails. An operator who arrives to find out why nothing works needs this before they need a response body.

The Signature panel lists the signature parameters this request actually carried, read straight out of the URL the worker sent. Values are shown whole and not elided — the reason to look at a signature is to compare it with something, and half of one compares with nothing.

The parameters it picks out:

a_bogus, X-Bogus, X-Gnarly, X-Dynosaur, msToken, x-secsdk-web-signature, verifyFp, fp, uifid.

A Read them back button posts the whole signed URL and its User-Agent to /api/v1/tools/decode and renders the result inline — the same view the Decode tab produces. That is a separate, deliberate click rather than something that happens on arrival: decoding is a question most runs do not ask, and firing it automatically would spend a round trip on every call.

This panel needs an explanation to exist, because the signature that matters was computed in the worker against the platform’s own URL, and that URL is not part of an ordinary answer. Without one, the panel says so and offers Send again and explain it.

It also needs the async path. The console picks the explanation out of the task it polled, so a run with wait filled in that finished inside the window still shows Explain is on. Send the request and this panel fills in. — even though explain did succeed and was audited — and the cookie jar and upstream curl below it stay empty for the same reason. Clear wait and send again.

explain — what request did this instance actually make?

Section titled “explain — what request did this instance actually make?”

explain=true makes the instance describe the upstream call it made, in the shape it made it.

What comes back

Field What it is
method The method sent upstream
url The full platform URL, signature parameters included
headers Every header the transport was about to send — the signer’s, plus the identity’s own User-Agent, language and client hints
cookie_header The identity’s jar as one Cookie: header, ready to paste
identity_id Which identity served the attempt
signer Which signer produced the signature
endpoint The internal endpoint name, e.g. douyin.content_detail
proxy The identity’s exit, masked

What it costs. An explanation describes an attempt, and a cached answer made none — so explain implies refresh: it skips the response cache, and an explained call is never joined onto an unexplained one already in flight. Every explained call is a real upstream request and spends an identity.

Who may ask. The answer contains a live cookie jar, so it is gated like pool management and not like reading: an identity:manage (or admin) scope and an operator role. A douyin:read key may ask this instance to use a jar; it may not ask to be handed one, and “show me the request you made” is the same disclosure by a longer route.

It is audited. Every accepted explain writes a request.explained entry to the audit log naming who asked and for which endpoint — never the jar itself. The same bargain the identities page makes when it reveals cookies.

It survives a refusal. The explanation is stored on both outcomes, because “the request was refused, show me what we sent” is the case the feature exists for. A failed task carries it in exactly the same place a successful one does.

It is stripped for readers who may not see it. The explanation is stored with the task result. When anyone reads that task back, the explain block is removed unless that reader could have revealed the jar directly.

Where it lands on the wire. In the response metadata under explain — as result_meta.explain when you fetch the task, and in meta when a wait long enough to finish the task returned a successful answer inline. A task that failed inside the wait window is rendered as an error envelope, and an error envelope’s meta carries only request_id. The explanation is still stored on that task; it has to be read back from GET /api/v1/tasks/{task_id} as result_meta.explain.

With an explanation present, the Endpoint and pool panel gains the concrete facts about the run in front of you: the identity id, the signer, the masked exit, and the jar.

The jar is behind a Show the jar click rather than behind a mask. Half a cookie helps nobody, and you have already asked for it by name; what the click buys is that it is not sitting open on a shared screen. Until you click, the panel shows only how many cookies there are.

Treat what you copy as a credential. It is a live session for a real platform account or guest, it will be accepted by anyone who has it, and it does not belong in a bug report, a shared Postman workspace or a chat message. See Security.

Below the jar, the console assembles the whole upstream request as a curl line: the platform’s own endpoint, the signed query byte for byte, every header the transport was going to send, and the identity’s jar.

This is the thing anybody debugging a refused call ends up building by hand, and building it by hand is where it goes wrong. Three things have to line up or the platform refuses you, and all three are known here and were not guessable before:

  1. The query goes byte for byte. The signature covers the query as its own encoder produced it. Re-encoding it — which most HTTP clients do helpfully and silently — changes the bytes the signature covers.
  2. The User-Agent has to be the one it was hashed with. Both platforms hash the UA into the signature. “The UA I am sending is not the UA I signed with” is the most common way a hand-built request fails while looking perfectly correct.
  3. The jar has to be the identity’s own. The platform answers a session. A correct signature with no cookies gets 200 and an empty body.

The curl line is also short-lived. The signature carries a clock and the jar carries a session; both go stale. If it worked ten minutes ago and does not now, re-run the explained call rather than debugging the old line.

Postman’s importer accepts a raw cURL command — Import → Raw text, paste, and it becomes a request. Two caveats matter more here than in most APIs:

  • Check the query survived. Any client that re-encodes the query string breaks the signature, and the failure looks like a bad signature rather than a bad client. After importing, compare the URL in Postman against the line you pasted, character for character. If they differ, send the URL as one opaque string rather than as parsed key/value parameters.
  • TikTok will refuse it anyway. TikTok checks that the TLS fingerprint agrees with the User-Agent. Postman, curl, httpx and every ordinary HTTP client present their own TLS fingerprint, so a Chrome User-Agent over a non-Chrome handshake is refused whatever the signature says. That is not something a header can fix — it is why this project ships a browser-grade transport. On Douyin the same line usually does work.

The same warning applies to the curl line itself on a machine whose curl is not built against a browser TLS profile. Use it to check what was sent; do not conclude from its failure that the signature was wrong.

The Copy-ready call panel generates a snippet for the form as it currently stands, in three flavours:

Snippet What it produces
curl curl -X <method> with Authorization: Bearer and Accept-Language, plus Content-Type and -d on a POST
Python An httpx call with params, json, headers and a 60-second timeout, printing payload["data"]
JavaScript A fetch call that throws on payload.success === false and logs payload.data

Every snippet carries the placeholder key dtk_xxxxxxxx_your_key_here. Replace it with a real key from the API keys page — the console itself authenticates with a session cookie, which a script does not have. Outside the browser, use Authorization: Bearer <key> or the X-API-Key header.

The snippets call this API. They are not the upstream curl and they do not need a cookie jar.

Under the catalogue, the last 12 calls of this session: which endpoint, which platform, whether it succeeded, and how long it took. Clicking one puts its parameters back in the form — and deliberately does not re-send, because a click that replays an upstream call would spend an identity by accident.

History is held in memory only and is gone when you leave the page. A request can carry a proxy password or an identity id, and neither belongs in storage that outlives the tab.


Five tabs over five API endpoints. These are the pieces the service uses on every request, published because this is an open-source project and people build on them. Everything here except the last tab is a pure function of what you type: no network, no identity, no request log row.

One warning is worth having before you start, because it is the first thing anyone hits: a signature alone is not enough. Both platforms answer a session, so a correct signature sent with no cookies gets 200 and an empty body. The signing tab says what else has to line up; the minting tab is what produces it.

POST /api/v1/tools/sign — computes the signature parameters a platform API URL needs. Pure arithmetic: nothing is fetched, no identity is spent, and the result depends only on what you send.

Field Limit Notes
Platform douyin or tiktok
API URL 4096 chars The full API URL including its query. Must start http:// or https://.
User-Agent 512 chars Both platforms hash it in. Leave blank for a Chrome default, which is echoed back.
msToken 512 chars TikTok only. Sealed into the signature, so it must be the token the request will really carry — an invented value is worse than none.
Cookie jar 8192 chars Any paste format: a Cookie: header, DevTools JSON, Netscape. Douyin computes x-secsdk-web-signature over the visitor id inside it.

The default User-Agent, used when you leave the field blank:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36

Start from an example. Two buttons fill every field from a real browser request — Douyin’s profile query and TikTok’s post list. Two things were removed on purpose, and the removals are the lesson: the signature parameters are gone, because they are what this tool computes and pasting a stale one is the commonest way to get a 200 with an empty body; and the cookie values are placeholders, because a jar is a live login. The cookie names are real, which is the actual question somebody has when they look at that field.

The results.

  • Signed URL — ready to send, with a Read this signature back button that hands the whole thing to the Decode tab. The two forms are inverses and running one straight into the other is the most useful thing you can do with them.
  • How this query was assembled — the same result unflattened, one box per layer of the pipeline in the order the layers ran. This is the part worth reading when a request is refused, because it says which layer is missing rather than handing you one flat query string.
  • Parameters added — the signature parameters, keyed by the algorithm name.
  • Headers the signature requires — send every one of these alongside the signed query. Douyin refuses a sign-protected endpoint without them.
  • User-Agent — echoed back. Send the request with this exact string.

The layers:

Layer What it contributes
business What you put in the URL, minus msToken — which is a session value wherever it was written, and is counted in the next layer instead
session The msToken, and ms_token_source saying where it came from: url, cookies, generated or absent
signature The seal. seal_param names the parameter that actually is the seal, and roles says the same per parameter
websign Douyin only: x-secsdk-web-signature over the visitor id, with the salt and the md5 preimage

Two asymmetries in that table are the platforms’ own, not this project’s, and both are the kind of thing readers otherwise “fix”:

  • Only Douyin ever shows generated. Douyin’s signer invents an msToken when the URL carries none. TikTok’s never does, because TikTok verifies a token that is present and accepts one that is absent — a fabricated token fails where a missing one is fine. With no token, TikTok’s session layer is marked skipped with no_ms_token_supplied and an empty msToken is sent.
  • On TikTok, algorithm and the seal are different parameters. algorithm reports X-Bogus because that is the stable name of the scheme on the wire, but the X-Bogus TikTok Web sends on an HTTP request is the constant 1. X-Gnarly is the seal over the query and X-Dynosaur is the environment report it covers. A diagram drawn from algorithm alone would point at the one parameter that seals nothing, so seal_param exists to point at the right one.

Two things the signature layer will not show you, and why:

  • input_reconstructible is false and stays false. The seal is computed over the query as its own encoder produced it, and the string finally sent is built by a different encoder, so for some inputs the bytes signed are not the bytes sent. A preimage that is right for most queries would be read as documentation of the algorithm, which is worse than not offering one.
  • The websign preimage is shown, but only after being recomputed and checked against the signature beside it. An unverifiable preimage is dropped rather than published, for the same reason.

If the jar you sent carried no uifid, the websign layer is present and skipped with no_uifid_cookie. That is not a curiosity: it is why headers came back empty, and it is why a sign-protected Douyin endpoint will refuse the request naming uifid. Mint a jar on the Identity tab.

Whatever the tool returns, the checklist beside it still applies. Send the request with the exact User-Agent that was echoed back; use a TLS fingerprint that agrees with it; and send a cookie jar. The project measured this on 2026-09-08 by sending this endpoint’s own output: with all three, 2537 bytes and the expected profile; with no cookies at all, 200 and an empty body.

POST /api/v1/tools/decode — the inverse of the tab beside it, and the reason this project reversed these algorithms itself rather than vendoring somebody’s port: an implementation you own can be explained. Also pure arithmetic.

Paste a whole signed URL and every signature parameter in it is decoded against the exact string that parameter seals. That is much the most useful form: only from a whole URL is the covered string known exactly, so only then can the checks run. A single value, or a name=value pair, works too. The value is limited to 8192 characters.

Fill in the User-Agent field even when the answer seems obvious. Both platforms hash the UA into the signature, and this is how you find out whether the UA you are sending is the one you signed with.

Read it as is left on automatic by default, where the value is identified by properties no other parameter in the set has. Name it yourself when the shape is ambiguous: a_bogus, X-Bogus, X-Gnarly, X-Dynosaur, msToken, x-secsdk-web-signature, verifyFp.

Three different things arrive looking like the same table row, and taking the second for the first is the mistake this design exists to prevent. Every field says which one it is.

Kind Shown as What it means
plain Recovered Read straight out of the payload, exactly. aid, page_id, versions, counters, nonces
time Clock A clock, as both the raw number and an ISO-8601 instant
environment Environment What the SDK reported about the page it believed it was in — screen geometry, flags, canvas and WebGL hashes
digest Hash A hash, or a byte of one. The input is not recoverable; see the checks
checksum Checksum Internal redundancy, recomputed here — which is what proves the decode
opaque Unexplained Present in every capture, meaning not established. Named, not invented

So the honest summary is: recovered values come back, hashes are checked rather than reversed, and issued values have no plaintext underneath them at all. msToken and the visitor tokens are drawn or handed out by the platform; the decoder reports not_computed rather than reporting a failure to find something that was never there.

For anything hashed, the decoder checks instead of guessing. Give it a candidate — a URL that carries the query, a User-Agent — and each check comes back as one of:

Status Meaning
match Your candidate is the input this signature sealed. The exact covered string is shown, because it reproduced the value beside it
differs It is not. This is what turns “my request is refused” into “the signature I am sending was computed over a different URL”
not_supplied You did not send a candidate. Not a failure, and drawn differently from differs on purpose

Each check also reports how many bits of evidence there are, so “match” is never more confident than it should be:

Parameter Bits per check Notes
a_bogus 24 Three SM3 bytes per chain, over query, body and User-Agent
X-Bogus 16 Two bytes of a double md5
X-Dynosaur 32 A 32-bit hash over query, User-Agent and body
X-Gnarly 128 Whole md5s of query, body and User-Agent
x-secsdk-web-signature 128 A whole md5 of the rebuilt preimage
Parameter Platform What comes back
a_bogus Douyin Header magic, SDK version, two clocks, aid, page_id, a fortnight counter, the browser info string, env and detect flags, a call bucket and a tripwire — plus three digest chains
X-Bogus Douyin Constant lead, a timestamp in whole seconds, the canvas constant, three digest pairs and an internal checksum. Noted as superseded by a_bogus
X-Bogus = 1 TikTok Reported as a constant that seals nothing, not as a malformed value — TikTok Web genuinely sends the literal
X-Gnarly TikTok Sixteen fields: two checksums, env and ub codes, md5s of query, body and User-Agent, timestamp, nonces, SDK and SCM versions, call sequences
X-Dynosaur TikTok Twenty-five fields: versions, counters, the page, WebGL/canvas/device hashes, a timestamp, four 32-bit hashes
msToken either Length, padding and alphabet only. Issued, not computed. The length also identifies whose it is — a Douyin token in a TikTok request is 20 characters short and looks exactly like a signing bug until somebody counts them
x-secsdk-web-signature Douyin The digest itself, and — when the visitor id and timestamp travel with it — the salt and the exact preimage its md5 covers
verifyFp, fp, s_v_web_id Douyin The prefix, the mint time as base36 milliseconds, and the random tail. A token minted long before the request carrying it is the shape of a jar that has been sitting in a file

Notes below a result explain what was verified. checksum_verified means every input to the internal fold is itself a field of the payload and they all agree, so the whole layout came back correctly. noise_not_recoverable means some bytes are drawn per call and carry nothing — two signatures made in the same millisecond differ in most of their bytes and mean the same thing.

When a value cannot be read at all, the reason says why: malformed (with the structural test it failed appended), one_way, not_computed, or unknown_parameter.

GET /api/v1/tools/parse-url — works out what a link points at without fetching anything. Accepts a share link or the whole clipboard text with a link buried in it, which is what both apps actually put on the clipboard.

It reports the platform, what kind of resource the link names, the id, the handle where there is one, the canonical URL, whether it is a supported target at all, and whether it needs expanding.

Which id you get differs by platform, because the endpoints differ: a Douyin profile link yields a sec_user_id, a TikTok one yields the @handle, because that is what TikTok’s user-detail call accepts. Both come back under resource_id.

A short link (v.douyin.com, vm.tiktok.com) cannot be resolved without following it, and following it is a network call this endpoint promises not to make. Those come back with needs_expansion: true and no id; send them to POST /api/v1/parse instead — which is the Playground’s first catalogue entry.

POST /api/v1/tools/parse-batch — the same recognition over a pasted list, because one at a time is the wrong shape for the thing people actually have: a column out of a spreadsheet, or the output of a scrape.

Paste one item per line. Blank lines and duplicates are dropped. A line that is a share caption with a URL inside it yields the URL, not four fragments; a line with several URLs yields several.

Limit Value
Items returned 1000
Text accepted 524288 characters

If more distinct items were found than were returned, the result says truncated and the page tells you how many were parsed, rather than leaving you to count.

Each line comes back as one of:

Kind Meaning
link A recognised link, with platform, resource and id
short_link A recognised link whose target needs a network call
content_id A bare post id that checks out
bad_id A line of digits that cannot be a post id
unknown Not recognised as either

Both platforms mint post ids whose high 32 bits are the Unix second they were issued, so the table also shows when each id was issued — read out of the id itself, not out of a request. It is at or a little before the publication time, never after.

That check cannot tell a post that never existed from one that has been deleted: a plausible id is a plausible id. What it does is reject text that cannot be an id at all — which is the part worth doing for free, because the alternative is an upstream request that spends an identity to be told the same thing.

Copy the ids puts every recognised id on the clipboard, one per line, skipping the bad_id rows.

POST /api/v1/tools/identity — drives a real browser to the platform and hands back the cookies it was issued. This is the half of the work that cannot be computed: msToken on TikTok and UIFID_TEMP on Douyin are issued by the platform to a browser that loaded its page. No algorithm produces them, which is why a signature alone is not enough.

What it costs, plainly:

  • Tens of seconds. A browser has to launch, load the page and wait for its scripts to finish setting cookies.
  • identity:manage scope. Ordinary read keys do not carry it.
  • A browser-rpc service. Without DTK_BROWSER_RPC_URL configured, the call fails saying so and naming the setting. browser-rpc is the heaviest container in the stack and is an optional compose profile; see Installation.

The optional Proxy field mints behind a given egress, and is worth setting: cookies minted at one address and used from another are exactly the mismatch both platforms look for. It goes through the same security.request_proxy gate as every other proxy parameter — a tool endpoint must not be the way around that setting.

Nothing is stored. The jar is returned to you and forgotten. To add one to this instance’s own pool, use the Identities page or POST /api/v1/admin/identities/mint — see Identities and proxies.

The result is the cookie jar, the browser fingerprint it was minted with — User-Agent, browser family and major version, platform, screen, language, timezone — and the exit address it was seen from. Send all of it together. A jar used with a different User-Agent is a weaker identity than no identity.

Tab Endpoint Scope needed Network Identity spent Time
Sign a URL POST /api/v1/tools/sign douyin:read or tiktok:read none none instant
Decode POST /api/v1/tools/decode douyin:read or tiktok:read none none instant
Parse a link GET /api/v1/tools/parse-url douyin:read or tiktok:read none none instant
Batch POST /api/v1/tools/parse-batch douyin:read or tiktok:read none none instant
Mint an identity POST /api/v1/tools/identity identity:manage a real browser session one minted jar handed to you tens of seconds

All five are rate-limited like every other endpoint on this API.


/docs is the console’s own reference page. It renders Swagger UI inside the console shell, pointed at /openapi.json?lang=<language> for whichever language the console is set to, and restyled from the console’s own design tokens so it does not read as a different product bolted on. The override reaches colour, typography and radius — the widget keeps its own behaviour.

Above the widget, a card reports the document URL (copyable), how many paths and operations it declares, and which language it was generated in. If it says the document declares no operations, that is a deployment problem rather than an empty database: the api container registered no routes.

Operations arrive collapsed, models unexpanded, request durations shown, and authorization persisted across reloads. Try it out is enabled and the widget sends the browser’s credentials, so calls you make from /docs while signed in to the console are authenticated by your session cookie — no key to paste. That also means they are real calls: a Try it out against a platform endpoint spends an identity exactly as the Playground does.

Swagger UI’s assets are loaded from a public CDN (cdn.jsdelivr.net/npm/[email protected]) with a 15-second timeout. On an air-gapped deployment or behind a blocked CDN that will fail, which is expected rather than broken: the page says so and offers the server-rendered page, the raw document, and a retry, instead of leaving an empty frame.

The API serves its own documentation pages, independent of the console:

Path What it is
/swagger Swagger UI, server-rendered, with Try it out enabled and every tag collapsed
/redoc ReDoc, a read-only three-pane reference
/openapi.json The raw OpenAPI document

Both pages forward their own ?lang= to the schema URL, so /swagger?lang=zh gets the Chinese document. Language is resolved in that order: ?lang=, then Accept-Language, then the instance’s api.default_language. An unsupported ?lang= value is ignored rather than fatal, and negotiation continues with the header. Supported values are en and zh.

The console deliberately does not own /swagger. /docs is the console page, behind a session; /swagger stays credential-free for callers who have no console account, which is most consumers of a public instance. Both claiming one path meant which page you got depended on how you arrived — client-side navigation gave the console, a reload gave the bare document — and a reload is exactly what somebody does after expanding a tag.

Because /swagger, /redoc and /openapi.json need no session, anyone who can reach your instance can read its API surface. That is intentional for a service whose purpose is to be called by someone else’s program, but it is a fact to weigh before you expose the port. See Security.

The document declares both real authentication schemes, so the Authorize button works:

Scheme Where Value
X-API-Key header A key from the API keys page
dtk_session cookie Set by signing in to the console; sent automatically by a browser

Security is declared per operation rather than globally, so public endpoints are not wrongly shown as needing a key.

The document types the response envelopesuccess, data, error, meta — and not each endpoint’s data shape. That is a deliberate trade-off: responses are assembled as dictionaries, and declaring them a second time in the schema would only let the two drift. What a generated client genuinely needs is the part that never varies.


Everything on these pages is a normal API endpoint. Set a key with the right scopes first — see Users and API keys — and adjust the host if your instance is not on the default loopback address.

Terminal window
export DTK_KEY='dtk_...your key...'
export DTK_URL='http://127.0.0.1:8000'

Identify a link, without spending anything:

Terminal window
curl -s -G "$DTK_URL/api/v1/tools/parse-url" \
--data-urlencode 'url=https://www.douyin.com/video/7300000000000000000' \
-H "X-API-Key: $DTK_KEY"

Sort a pasted list of links and ids:

Terminal window
curl -s -X POST "$DTK_URL/api/v1/tools/parse-batch" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"text":"7300000000000000000\nhttps://www.douyin.com/video/7300000000000000001"}'

Sign a URL:

Terminal window
curl -s -X POST "$DTK_URL/api/v1/tools/sign" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"platform":"douyin","url":"https://www.douyin.com/aweme/v1/web/aweme/detail/?aid=6383&aweme_id=7300000000000000000"}'

Read a signature back, checking a candidate User-Agent against it:

Terminal window
curl -s -X POST "$DTK_URL/api/v1/tools/decode" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"value":"https://www.douyin.com/aweme/v1/web/aweme/detail/?aid=6383&a_bogus=...","user_agent":"Mozilla/5.0 ..."}'

Mint a guest identity and keep it yourself — needs identity:manage, and takes tens of seconds:

Terminal window
curl -s -X POST "$DTK_URL/api/v1/tools/identity" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"platform":"douyin"}'

Ask a data endpoint to explain itself — needs identity:manage and an operator role, skips the cache, and is audited:

Terminal window
curl -s -G "$DTK_URL/api/v1/douyin/video" \
--data-urlencode 'url=https://www.douyin.com/video/7300000000000000000' \
--data-urlencode 'wait=30' \
--data-urlencode 'explain=true' \
-H "X-API-Key: $DTK_KEY"

When the call succeeds inside the wait, the explanation is in the response metadata under explain. Every other outcome sends you to the task: without a wait long enough for the task to finish you get a task id, and a task that failed inside the window comes back as an error envelope whose meta holds only request_id. In both of those cases read the explanation from GET /api/v1/tasks/{task_id} as result_meta.explain.

For the full request and response contract, see the REST API guide. For the same work from a terminal, see the CLI reference.


  • Concepts — what an identity, a lease, a circuit and a signer actually are. Worth reading if the failure panel’s vocabulary is unfamiliar.
  • Console overview — the Logs page, where a request id is searched for, and the Overview board that lists every endpoint’s circuit.
  • Identities and proxies — where the pool the Playground draws from comes from, how to fix an empty one, the Scheduler page, and what to do while a breaker is open.
  • Operations — backups, notifications, retention and monitoring, and what the container logs hold that the Logs page does not.
  • Troubleshooting — for the specific failures these pages will show you.
  • Security — what a revealed cookie jar means, and who should be able to ask for one.