REST API v1
The TikTok data API reference.
4endpoints returning plain JSON: resolve a link to watermark-free media, search posts by keyword, list a video's comments, and page through a profile. Bearer-token auth, predictable error codes, no SDK required.
Base URL
https://tikklip.com/api/v1Authenticate with Authorization: Bearer <your key>. Keys are created in the dashboard and are rate limited to 10 requests per second.
OpenAPI 3.1 spec — import it into Postman, Insomnia, or a code agent to generate a client.
Monthly request allowance
The REST API is included on the plans below. Every response counts as one request; a failure caused by us is never counted.
| Plan | Requests / month | Team seats |
|---|---|---|
| Agency | 10,000 | 3 |
| Enterprise | 50,000 | 10 |
Endpoints
GET /api/v1/resolve
Resolve one TikTok URL to its watermark-free media and metadata.
- url*
- Any TikTok video or photo URL, including short links.
curl -H "Authorization: Bearer $TIKKLIP_KEY" \ "https://tikklip.com/api/v1/resolve?url=https://www.tiktok.com/@user/video/123"
{
"awemeId": "123",
"title": "…",
"author": { "uniqueId": "user", "nickname": "User" },
"stats": { "plays": 1000, "likes": 50 },
"videoUrl": "https://…",
"coverUrl": "https://…"
}GET /api/v1/user-posts
One page of a profile's posts. Paginate with the returned cursor.
- handle
- TikTok @handle. Costs the same one unit as any other request — we resolve it to a numeric id internally and cache that for 24 hours.
- userId
- The id echoed by a previous response. Passing it skips our internal handle lookup, so the call returns faster — it costs the same one unit either way.
- cursor
- Opaque cursor from the previous response. Starts at "0".
- count
- Items per page, max 35.
curl -H "Authorization: Bearer $TIKKLIP_KEY" \ "https://tikklip.com/api/v1/user-posts?handle=user&count=35"
{
"items": [ { "awemeId": "…", "title": "…", "stats": { … } } ],
"cursor": "35",
"hasMore": true,
"userId": "6789…"
}GET /api/v1/search
One page of keyword or hashtag video search results.
- keyword*
- Search term or hashtag (without the #).
- cursor
- Offset from the previous response. Starts at "0".
- count
- Items per page, max 20. Values above 20 are clamped — TikTok returns nothing above that.
curl -H "Authorization: Bearer $TIKKLIP_KEY" \ "https://tikklip.com/api/v1/search?keyword=cooking&count=20"
{
"items": [ { "awemeId": "…", "title": "…" } ],
"cursor": "20",
"hasMore": true
}GET /api/v1/comments
One page of a video's top-level comments.
- url
- TikTok video URL. Costs the same one unit as any other request — we resolve it to a numeric aweme id internally and cache that for 24 hours.
- awemeId
- The id echoed by a previous response. Passing it skips our internal URL lookup, so the call returns faster — it costs the same one unit either way.
- cursor
- Offset from the previous response. Starts at "0".
- count
- Comments per page, max 50.
curl -H "Authorization: Bearer $TIKKLIP_KEY" \ "https://tikklip.com/api/v1/comments?url=https://www.tiktok.com/@user/video/123"
{
"items": [ { "commentId": "…", "text": "…", "likes": 12 } ],
"cursor": "50",
"hasMore": true,
"awemeId": "123"
}Error handling
Errors
Every failure returns { "error": { "code", "message" } }. Branch on code, not status.
| Code | Status | Meaning |
|---|---|---|
| unauthorized | 401 | Missing, malformed, unknown, or revoked key. |
| forbidden | 403 | Your plan doesn't include API access. |
| bad_request | 400 | A required parameter is missing or invalid. |
| not_found | 404 | The handle, video, or URL doesn't resolve. |
| rate_limited | 429 | More than 10 requests in one second. Retry after the Retry-After header. |
| quota_exhausted | 429 | Your monthly budget is spent. It resets on the 1st. |
| upstream_error | 502 | TikTok or our provider failed. Retry. |
Webhooks
Instead of polling, get a signed POST the moment a job finishes, carrying the row count, the truncation reason, and short-lived signed export links. Add an endpoint in Settings on Agency or Enterprise.
Events
- job.completed
- A scrape job finished. Carries row count, truncation reason, and signed export URLs.
- job.failed
- A scrape job failed. Carries the error message; no export URLs.
- webhook.test
- Sent when an endpoint is created and on demand. Not subscribable, never retried.
Retries
8 attempts over roughly 20 hours. Delivery is at-least-once and unordered — use the id field for idempotency. Any 2xx acknowledges an event; anything else is retried with ±20% jitter.
| Attempt | Sent after |
|---|---|
| 1 | immediately |
| 2 | 30s |
| 3 | 2m |
| 4 | 10m |
| 5 | 45m |
| 6 | 2h |
| 7 | 5h |
| 8 | 12h |
Verifying a signature
Signatures follow the Standard Webhooks spec, so any off-the-shelf receiver library works. Sign the raw body — never a re-serialized copy — and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req) {
const raw = await req.text(); // BEFORE any JSON parsing
const id = req.headers.get("webhook-id");
const ts = req.headers.get("webhook-timestamp");
const header = req.headers.get("webhook-signature");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return new Response("stale", { status: 400 });
const key = Buffer.from(process.env.TIKKLIP_WEBHOOK_SECRET.replace("whsec_", ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${raw}`).digest("base64");
// Split on spaces and accept if ANY signature matches — this is what makes
// secret rotation non-breaking.
const ok = header.split(" ").some((part) => {
const [v, sig] = part.split(",");
if (v !== "v1" || !sig) return false;
const a = Buffer.from(sig), b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
});
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(raw);
// Branch on event.type from the BODY — the tikklip-event header is advisory.
return new Response("ok");
}Constraints
- Endpoints must be https and publicly reachable. Private, loopback, link-local, and reserved addresses are refused — any port is fine.
- Redirects are not followed. A 3xx is recorded as a failed delivery; publish the final URL.
- We publish no static egress IPs. Verify signatures rather than allow-listing addresses.
- Export links in the payload expire in one hour and are re-minted on every attempt, so a late retry still carries working links.
- After 20 consecutive failed events spanning more than 24 hours an endpoint is disabled and we email you. Events are dropped while an endpoint is disabled — re-enable it in Settings.
API questions
- How do I authenticate?
- Create a key in Settings, then send it as `Authorization: Bearer tk_live_…` on every request. Keys are shown once at creation and stored only as a hash — we cannot recover one for you. Revoking a key takes effect immediately.
- How is a request counted?
- Every request costs exactly one unit of your monthly budget, whether you pass `handle` or `userId`, `url` or `awemeId`. `X-RateLimit-Remaining` drops by 1 either way. When a handle or URL needs resolving, that's an upstream lookup on our side, not an extra unit on yours — and we cache the result for 24 hours. Passing the `userId` or `awemeId` that every response echoes back just skips that internal lookup, so the call returns faster.
- How do I paginate?
- Start with `cursor=0`. Each response returns a `cursor` and a `hasMore` flag; pass the cursor back verbatim until `hasMore` is false. Cursors are opaque strings — do not parse or increment them yourself.
- What are the rate limits?
- 10 requests per second per key, and your plan's monthly budget. Successful responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for the monthly budget. Both limits return HTTP 429, but the `code` differs: `rate_limited` means retry in a second, `quota_exhausted` means retry next month.
- Does the API share my dashboard quotas?
- No. Your API budget is entirely separate from your dashboard profile scrapes, search exports, and comment exports. Spending one never touches the other.
- Can an API key create more API keys?
- No. Key management is only available to a signed-in session in the dashboard. A leaked key cannot mint another, and it cannot see your billing.
Get an API key
Create an account, pick a plan with API access, and generate a key from Settings. Keys are hashed at rest and can be revoked at any time.