How to get a Twitch clip as JSON (signed MP4, curator vs channel)
Twitch Helix will give you a clip if you register an app and hold an App Access Token. GET /v1/twitch/clip takes a clip URL and returns the public clip as JSON — curator vs channel, a signed MP4, and the parsed playback token — for 1 credit, no Twitch client id.
The clip below was captured on October 10, 2026 from shroud's topClips[].
A clip is not a channel
A channel URL or username on this path is 400 — that belongs on /v1/twitch/profile. The clip slug from twitch.tv/shroud/clip/… or clips.twitch.tv/… is the input.
{
"id": "2358589597",
"slug": "TangentialBillowingJalapenoYee-ccA2oB1pEYh4CHWJ",
"title": "F Shroud",
"createdAt": "2023-06-02T11:41:50Z",
"durationSeconds": 28,
"views": 563163,
"game": "Diablo IV",
"videoOffsetSeconds": 48930,
"channel": {
"handle": "shroud",
"displayName": "shroud",
"followers": 11296125,
"isPartner": true
},
"curator": {
"handle": "pato_bolado",
"displayName": "Pato_Bolado"
}
}
curator is who cut the clip. channel is the broadcaster. They are different accounts on this clip. There is no flat broadcaster twin.
The call
JavaScript:
const res = await fetch(
"https://api.captapi.com/v1/twitch/clip?url=" +
encodeURIComponent(
"https://www.twitch.tv/shroud/clip/TangentialBillowingJalapenoYee-ccA2oB1pEYh4CHWJ",
),
{ headers: { Authorization: `Bearer ${process.env.CAPTAPI_KEY}` } },
);
const { data } = await res.json();
// Unsigned /nauth/ MP4s return 401. Play the signed URL.
console.log(data.title, data.views, data.signedVideoUrl);
console.log(data.playbackAccessToken.expiresAt);
Python:
import httpx, os
r = httpx.get(
"https://api.captapi.com/v1/twitch/clip",
params={"url": "https://www.twitch.tv/shroud/clip/TangentialBillowingJalapenoYee-ccA2oB1pEYh4CHWJ"},
headers={"Authorization": f"Bearer {os.environ['CAPTAPI_KEY']}"},
timeout=30,
)
data = r.json()["data"]
print(data["title"], data["views"])
print(data["signedVideoUrl"])
print(data["playbackAccessToken"]["expiresAt"])
The trap: the unsigned MP4 is 401
The payload ships both:
videoUrl— the raw/nauth/…/1080/index.mp4signedVideoUrl— the same path with?sig=&token=
On this capture the token decoded to expires: 1791702627 / expiresAt: "2026-10-11T07:10:27Z", about twenty hours after fetchedAt. Fetch the clip when you need to play it. Don't store signedVideoUrl and expect it next week. The same expiry class as Instagram and TikTok CDNs — how to read oe= / expire= / this token.
videoQualities[] repeats 1080 / 720 / 480 / 360 with url and signedUrl per rung. playbackAccessToken is parsed fields (signature, expires, expiresAt, clipSlug, deviceId, version) — not an escaped JSON string.
The clip also ships embedUrl (https://clips.twitch.tv/embed?clip=<slug>), thumbnail on static-cdn.jtvnw.net, language: "en", isFeatured: false, isPublished: true, and gameBoxArtUrl. relatedClips[] is from the same channel when Twitch attaches the rail — no second request.
Profile, VODs, schedule
shroud's profile on the same session: isLive: false (so no stream object — offline already said it), followers: 11296125, topClips[] of 10, and a 6-row schedule[] preview. The canonical schedule is /v1/twitch/user-schedule (1 credit; empty schedule is 0). Segments use startAt / endAt, not past-tense startedAt. One recurring row carried firstOccurrenceAt: "2025-11-24T19:00:00Z"; skip rows where canceledUntil is set. Anonymous GQL does not expose timezone or vacation mode.
/v1/twitch/user-videos?filterBy=ARCHIVE returned three VODs, nextCursor: "2894780361" (last video id on the page). The anonymous window stops at 100 matching videos — Twitch rejects deeper after-cursors with IntegrityCheckFailed. Flat 2 credits.
The clip endpoint page has the token shape and the 400-on-channel-URL rule. A free key is 100 credits — a profile, a clip, and a VOD page.