tikklip

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/v1

Authenticate 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.

PlanRequests / monthTeam seats
Agency10,0003
Enterprise50,00010

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.

CodeStatusMeaning
unauthorized401Missing, malformed, unknown, or revoked key.
forbidden403Your plan doesn't include API access.
bad_request400A required parameter is missing or invalid.
not_found404The handle, video, or URL doesn't resolve.
rate_limited429More than 10 requests in one second. Retry after the Retry-After header.
quota_exhausted429Your monthly budget is spent. It resets on the 1st.
upstream_error502TikTok 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.

AttemptSent after
1immediately
230s
32m
410m
545m
62h
75h
812h

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

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.

See plans