Follow a Twitch channel
Point Podcast-Server at https://www.twitch.tv/<login> and the channel's VODs become downloadable items, the same way a YouTube channel does.
This needs credentials: Twitch's API refuses anonymous calls. Without them nothing breaks — the Twitch integration is simply not registered, and a Twitch URL is not recognised.
This guide assumes a running install, wrapped in your own overlay as described in Deploy to production. For what the other sources accept, see Supported sources.
1. Create a Twitch application
Credentials identify your server, not a Twitch account: there is no login flow, no consent screen, and the server never acts on behalf of a user.
- Sign in on dev.twitch.tv/console/apps with any Twitch account (two-factor authentication must be enabled on it — Twitch refuses to create an application otherwise).
- Register Your Application, and fill in:
- Name — anything unique across Twitch, e.g.
podcast-server-<yourname>. - OAuth Redirect URLs —
http://localhost. The field is mandatory but unused: the client credentials grant never redirects anywhere. - Category — Application Integration.
- Client Type — Confidential. This is what allows the client credentials grant.
- Name — anything unique across Twitch, e.g.
- Open the application with Manage, copy the Client ID, then New Secret and copy the secret — it is shown once.
Generating a new secret immediately invalidates the previous one, so do it when you are ready to paste it somewhere.
2. Check the credentials before wiring them in
Thirty seconds here saves debugging the server later:
export TWITCH_CLIENT_ID=<your client id>
export TWITCH_CLIENT_SECRET=<your client secret>
# negotiate an app access token — this is exactly what the server does
export TWITCH_TOKEN=$(curl -s -X POST 'https://id.twitch.tv/oauth2/token' \
-d "client_id=$TWITCH_CLIENT_ID" \
-d "client_secret=$TWITCH_CLIENT_SECRET" \
-d 'grant_type=client_credentials' | jq -r .access_token)
# resolve a channel, the call the finder makes
curl -s 'https://api.twitch.tv/helix/users?login=twitchdev' \
-H "Client-Id: $TWITCH_CLIENT_ID" \
-H "Authorization: Bearer $TWITCH_TOKEN" | jq '.data[0] | {id, display_name, description}'A working pair prints the channel. A bad one answers {"status":400,"message":"invalid client"} on the first call, and $TWITCH_TOKEN ends up as null.
The token is valid for about 58 days. The server negotiates one lazily and reuses it, so it does not spend a round trip per call.
3. Add them to your overlay
Both values live in the podcast-server secret, next to the YouTube key:
# my-podcast-server/kustomization.yaml
secretGenerator:
- name: podcast-server
behavior: merge
literals:
- api.twitch.client-id=<your client id>
- api.twitch.client-secret=<your client secret>
type: Opaquekubectl apply -k my-podcast-server/
kubectl -n podcast-server rollout status deployment/backendNo manual restart is needed: kustomize suffixes generated secrets with a hash of their content, so changing a literal produces a new secret name, which updates the Deployment and rolls the pod on its own.
Keep the secret out of git
Your overlay is a git repository like any other. Committing the client secret there publishes it. Use sops or external-secrets rather than a plain literal, as suggested in the Going further section of the production guide.
4. Choose which videos are indexed
Optional — by default every video of the channel is indexed. The setting is an environment variable on the backend Deployment, so it goes through a patch rather than the secret:
# my-podcast-server/kustomization.yaml
patches:
- target:
kind: Deployment
name: backend
patch: |
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: PODCASTSERVER_API_TWITCH_VIDEO_TYPES
value: highlight,uploadPODCASTSERVER_API_TWITCH_VIDEO_TYPES takes a comma-separated list:
| value | what it covers |
|---|---|
all | everything (the default) |
archive | automatic recordings of past streams — the ones Twitch deletes after 7 to 60 days |
highlight | clips the streamer saved from a broadcast |
upload | videos uploaded directly to the channel |
highlight,upload indexes only what never expires. all combined with anything else collapses to all, and any other value stops the startup with an explicit message. It applies to every Twitch podcast — it is not a per-podcast setting.
5. Walk through it
Below, $PODCAST_SERVER is the domain you configured — the same host the web app answers on. Both /api and /actuator are routed to the backend by the bundled Ingress.
export PODCAST_SERVER=https://podcast.mydomain.localTwitch is offered as a type. It appears as soon as the integration is registered, and is absent otherwise:
curl -s "$PODCAST_SERVER/api/v1/podcasts/types" | jq '.content[].key'The channel is detected. In the web app, Add a podcast → paste https://www.twitch.tv/twitchdev → title, description and cover fill in, typed Twitch. The same thing from the shell:
curl -s -X POST "$PODCAST_SERVER/api/v1/podcasts/find" \
-H 'Content-Type: text/plain' -d 'https://www.twitch.tv/twitchdev' | jqThe VODs are listed. Create the podcast, then update it — the ⋮ menu in the web app, or:
curl -s "$PODCAST_SERVER/api/v1/podcasts/$PODCAST_ID/update"Items appear with their title, publication date and cover. A channel that only streams and never highlights anything may legitimately have none: Twitch deletes stream archives after 7 days (14 for Affiliates, 60 for Partners and Prime). Pick a channel with highlights or uploads — twitchdev is a reliable one.
A VOD downloads. Hit download on an item and watch it complete as an .mp4.
A live broadcast produces nothing. Point at a channel that is streaming right now: the in-progress broadcast must not become an item, and re-running the update must not change the podcast signature. The item shows up on the first update after the stream ends.
Health reports the token. /actuator/health gains a twitch entry:
curl -s "$PODCAST_SERVER/actuator/health" | jq '.status, .components.twitch'Before anything hits Twitch it reads {"token": "absent", "reason": "no token negotiated yet"} and is UP. After a successful call it turns to {"token": "present", …} with the obtention and expiry dates. With wrong credentials it goes DOWN and the aggregate endpoint answers 503 — while /actuator/health/liveness and /actuator/health/readiness stay UP, so a Twitch outage never restarts the pod.
Troubleshooting
| What you see | What it means |
|---|---|
Twitch missing from /api/v1/podcasts/types | No credentials reached the server — one of them is empty or blank. Both must be set for anything Twitch to be registered. |
No twitch entry in /actuator/health | Same cause. The indicator only exists when the integration does. |
twitch is DOWN with invalid client | The client id and secret don't match. Regenerating the secret invalidates the old one — check you pasted the new one. |
| Pasting a Twitch URL fails while Twitch is unconfigured | Expected. Unrecognised URLs fall through to the RSS finder, which fails on an HTML page — the same as pasting any other non-feed URL. |
| The channel is detected but has no item | Either its archives have expired, or it is streaming right now and the in-progress broadcast is deliberately skipped. |
Not covered
Clips (/<login>/clip/<slug>) and collections are out of scope, as is recording live streams as they happen.