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.
What these three pages are for
Section titled “What these three pages are for”| 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 Playground (/playground)
Section titled “The Playground (/playground)”The three panes
Section titled “The three panes”The page fills the viewport as three resizable panes rather than one scrolling column:
- Endpoints — the catalogue, and below it this session’s history.
- Request — the form for the selected endpoint.
- 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.
The endpoint catalogue
Section titled “The endpoint catalogue”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.
Building a request
Section titled “Building a request”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.
Sending, and reading the status strip
Section titled “Sending, and reading the status strip”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.
The response panels
Section titled “The response panels”On success, four collapsible panels:
- Normalized result — the payload with every
rawisland stripped out. This is the stable contract every client sees. - Raw response — the platform’s own payload, present only when
include_rawwas 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
metaobject. If the payload is a page with a cursor andhas_moreis not false, a Fetch the next page button appears here; it fillscursorin and sends again, which spends another request. - Copy-ready call — see Copy-ready snippets.
When a call fails
Section titled “When a call fails”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 card
Section titled “The signature card”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.
The serving identity and its cookie jar
Section titled “The serving identity and its cookie jar”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.
The upstream curl
Section titled “The upstream curl”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:
- 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.
- 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.
- The jar has to be the identity’s own. The platform answers a session. A
correct signature with no cookies gets
200and 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.
Using it in Postman
Section titled “Using it in Postman”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,httpxand 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.
Copy-ready snippets
Section titled “Copy-ready snippets”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.
This session’s history
Section titled “This session’s history”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.
The Building blocks page (/tools)
Section titled “The Building blocks page (/tools)”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.
Sign a URL
Section titled “Sign a URL”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.36Start 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 anmsTokenwhen 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’ssessionlayer is marked skipped withno_ms_token_suppliedand an emptymsTokenis sent. - On TikTok,
algorithmand the seal are different parameters.algorithmreportsX-Bogusbecause that is the stable name of the scheme on the wire, but theX-BogusTikTok Web sends on an HTTP request is the constant1.X-Gnarlyis the seal over the query andX-Dynosauris the environment report it covers. A diagram drawn fromalgorithmalone would point at the one parameter that seals nothing, soseal_paramexists to point at the right one.
Two things the signature layer will not show you, and why:
input_reconstructibleisfalseand 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
websignpreimage 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.
Decode
Section titled “Decode”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.
What “decode” means here
Section titled “What “decode” means here”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 |
What each parameter yields
Section titled “What each parameter yields”| 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.
Parse a link
Section titled “Parse a link”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.
Mint an identity
Section titled “Mint an identity”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:managescope. Ordinary read keys do not carry it.- A
browser-rpcservice. WithoutDTK_BROWSER_RPC_URLconfigured, the call fails saying so and naming the setting.browser-rpcis 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.
What each tab costs
Section titled “What each tab costs”| 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.
The API docs page (/docs)
Section titled “The API docs page (/docs)”The embedded reference
Section titled “The embedded reference”/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.
/swagger and /redoc
Section titled “/swagger and /redoc”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.
Authenticating from the bare pages
Section titled “Authenticating from the bare pages”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 envelope — success, 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.
Calling the same things from a script
Section titled “Calling the same things from a script”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.
export DTK_KEY='dtk_...your key...'export DTK_URL='http://127.0.0.1:8000'Identify a link, without spending anything:
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:
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:
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:
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:
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:
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.
Where to go next
Section titled “Where to go next”- 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.