Imported from magnus919/agent-skills (
transistor/SKILL.md). Install upstream withnpx skills add magnus919/agent-skills --skill transistor. Copyright stays with the author (MIT).
transistor — Transistor.fm podcast hosting from the terminal
Drive a Transistor.fm account over its v1 JSON:API: shows, episodes, the
draft→publish lifecycle, per-day download analytics, private-podcast
subscribers, and webhooks. Responses are JSON:API documents; the bundled CLI
unwraps them (--json) while preserving the raw shapes agents need for jq.
Write commands are guarded: episode creation is always a draft, and
publishing goes through its own dedicated endpoint.
Setup
- Find your API key on the Transistor dashboard Account page → API Access (https://dashboard.transistor.fm/account) and export it:
export TRANSISTOR_API_KEY="<API_KEY>"
- Verify the key (GET /v1 — the authorization probe; there is no /v1/user route):
transistor user # name and time zone
transistor user --json | jq '{id, name, time_zone}'
A key carries the dashboard role of its user (owner / admin / team member)
per podcast. --help and --dry-run work without credentials. Requests are
rate-limited to 10 per 10 seconds; the CLI dies with a clear 429 message
instead of hammering.
Essential Commands
user — authorization probe
transistor user # who does this key belong to?
transistor user --json
shows / show — browse podcasts
transistor shows # newest-updated first
transistor shows --private --json # private podcasts only
transistor show --id <SHOW_ID_OR_SLUG> # full attributes incl. feed_url
transistor shows --page 1 --per 20 --json
Show ids and slugs are interchangeable on most show-scoped routes. Show
resources carry no counts fields — count via episodes --show ... --json,
then meta.totalCount.
episodes / episode — browse episodes
transistor episodes # newest first, all shows
transistor episodes --show <SHOW_ID> --status draft # drafts for one show
transistor episodes --show <SHOW_ID> --per 50 --page 1 --json
transistor episode --id <EPISODE_ID> --include show # compound doc + parent show
--include show adds included[] (the JSON:API compound document); every
episode item in --json output already carries show_id resolved from
relationships. --limit works as an alias for --per for old scripts.
episode-create / episode-update — drafts and metadata
transistor episode-create --show <SHOW_ID> --title "Ep 12: Roasting" \
--season 2 --number 4 --audio-url "https://uploads.example.com/ep12.mp3"
transistor episode-update --id <EPISODE_ID> --title "New title"
transistor episode-update --id <EPISODE_ID> --audio-url "<AUDIO_URL>" # attach audio
episode-create ALWAYS produces a draft (status: "draft",
published_at: null) — it never publishes. episode-update changes
metadata or attaches audio and never touches publishing state.
episode-publish — the lifecycle switch
transistor episode-publish --id <EPISODE_ID> # publish now
transistor episode-publish --id <EPISODE_ID> --status scheduled \
--published-at "2026-09-03 09:00:00" # schedule
transistor episode-publish --id <EPISODE_ID> --status draft # unpublish
Hits PATCH /v1/episodes/<EPISODE_ID>/publish with
episode[status]=draft|scheduled|published — the documented dedicated
endpoint. The CLI refuses to publish an episode whose media_url is still
empty (an unplayable item would hit every subscriber's feed); pass
--force to override.
authorize-upload — local audio (max 5GB)
transistor authorize-upload --filename ep12.mp3 # plan only
transistor authorize-upload --filename ep12.mp3 --file ./ep12.mp3
Returns (and, with --file, performs) the signed PUT; the printed
audio_url is what you attach with episode-create/episode-update. The
signed URL expires (~600 s in the docs' example).
analytics / episode-analytics — downloads per day
transistor analytics --show <SHOW_ID> # last 14 days
transistor analytics --show <SHOW_ID> \
--start-date 01-08-2026 --end-date 28-08-2026 --json
transistor episode-analytics --id <EPISODE_ID> --json
Dates are dd-mm-yyyy and must come in pairs. Analytics attributes are
per-day downloads[] arrays, not totals; the CLI sums them into
downloads_total and keeps the raw array.
subscribers — private podcast audience
transistor subscribers --show <SHOW_ID> --json
transistor subscriber-create --show <SHOW_ID> --email "listener@example.com"
transistor subscriber-batch --show <SHOW_ID> --email "a@example.com" --email "b@example.com"
transistor subscriber-delete --show <SHOW_ID> --email "a@example.com" # or --id
webhooks — push instead of poll
transistor webhooks --show <SHOW_ID>
transistor webhook-create --show <SHOW_ID> --event episode_published \
--url "https://example.com/hooks/transistor"
transistor webhook-delete --id <WEBHOOK_ID>
Events: episode_created, episode_published, subscriber_created,
subscriber_deleted. Cap: 50 per account. With a 10 req / 10 s limit,
webhooks beat polling for freshness.
Global flags
transistor --json shows # flags work in any position
transistor --dry-run episodes --show <SHOW_ID> # request plan, zero network
transistor --force episode-publish --id <EPISODE_ID> # skip the audio guard
transistor --quiet shows # suppress non-essential output
transistor --verbose episodes # detailed stderr logging
--dry-run emits {"dry_run": true, "method", "path", "params"} (write
commands add the exact body that would be sent — bracket keys and all),
so you can verify a plan before touching the API. --help and --dry-run
never require credentials.
Pipeline recipes
Create, attach audio, publish (the core workflow)
export TRANSISTOR_API_KEY="<API_KEY>"
SHOW=$(transistor shows --json | jq -r '.shows[0].id') # string id
AUDIO=$(transistor authorize-upload --filename ep12.mp3 --file ./ep12.mp3 --json | jq -r '.audio_url')
EP=$(transistor episode-create --show "$SHOW" --title "Ep 12" \
--audio-url "$AUDIO" --json | jq -r '.id') # draft id
transistor episode-publish --id "$EP" # dedicated endpoint
transistor episode --id "$EP" --json | jq '{status, media_url, published_at}'
Each stage's output feeds the next: shows → string id,
authorize-upload → string audio_url, episode-create → string draft
id, episode-publish → final status. Stage 2 is skippable when the
audio already has a public URL (pass it straight to episode-create).
Draft triage: what is not out yet?
transistor episodes --show <SHOW_ID> --status draft --json \
| jq -r '.episodes[] | [.id, .title, (if .media_url == "" then "no-audio" else "ready" end)] | @tsv'
# publish the ready ones (rate limit: 10 req / 10 s — add sleep 1 between calls)
Weekly downloads report
transistor shows --json | jq -r '.shows[].id' | while read -r S; do
transistor analytics --show "$S" --json \
| jq -r --arg id "$S" '[$id, (.downloads_total|tostring)] | @tsv'
sleep 1
done
JSON and jq
--json keys are stable snake_case wrappers around the JSON:API document:
shows/episodes/subscribers/webhooks (arrays with meta attached),
flat objects for single resources, dry_run/method/path/params/body
for plans. Attributes keep Transistor's own names — status, season,
number, duration (seconds), media_url, share_url, published_at,
feed_url — so jq selectors transfer directly to raw curl against
api.transistor.fm. Collection pagination surfaces as
meta.currentPage/meta.totalPages/meta.totalCount. Example:
transistor episodes --show <SHOW_ID> --json | jq -r '.episodes[] | [.id, .title, .status] | @tsv'. For compound documents the CLI resolves
relationships (show_id) and prints included show summaries in human
mode; with raw curl, match included[] by type and
relationships.show.data.id.
Known Gotchas
- Publishing is a separate endpoint, never a side effect — POST
/episodes and PATCH /episodes/:id cannot change
status. If an episode stays draft, the missing step isPATCH /v1/episodes/<ID>/publishwithepisode[status]=published. (The pre-thickening CLI had no publish path at all.) - The user probe is
GET /v1—/v1/userand/v1/authorizationare 404s, and the user resource has no email attribute (name and time_zone only). - Pagination is
pagination[page]+pagination[per](defaults 0 and 10; docs' examples request page 1).pagination[limit]andpage[number]are silently ignored — loops using them re-read page 1 forever. Loop whilemeta.currentPage < meta.totalPages. - Show resources carry no counts — derive episode/subscriber counts
from filtered listings'
meta.totalCount. - Analytics are per-day arrays, not totals — sum
attributes.downloads(the CLI providesdownloads_total); date bounds are dd-mm-yyyy and come in pairs; do not parse the row date format (docs' examples are inconsistent between sections). - Show creation is dashboard-only — there is no POST /v1/shows;
show-updateis the only show write. - Rate limit 10 req / 10 s — a 429 blocks access for 10 seconds. No retry headers; back off, batch subscriber imports, cache responses, and use webhooks for freshness. Transistor explicitly forbids using the API as a website back end (parse the RSS feed for that).
episode[published_at]uses the show's time zone (a show attribute), not UTC; scheduling and backdating both ride the publish endpoint.- Audio processing is asynchronous — watch
audio_processing/processing_failureafter attaching audio; publishing an unprocessed or failed file pushes silence to subscribers. - Signed upload URLs expire (~600 s in the docs' example): authorize,
PUT with the returned
content_type, attach promptly. - Error bodies are not formally specified — the CLI handles JSON:API
errors[]arrays and bare{"message": ...}objects, flattening either to one stderr line; 401 (bad key), 403 (role), 404 (bad id/route), 429 (rate limit) have distinct hints.
When to use
Use this skill for anything that reads or drives a Transistor.fm account through its API: verifying API access, browsing shows/episodes (including drafts and compound documents), running the episode lifecycle (create → attach audio → publish/schedule/unpublish), pulling download analytics windows, importing or revoking private-podcast subscribers, and registering webhooks.
When not to use
Do not use this skill for other podcast hosts (Buzzsprout, Libsyn, Megaphone, Spotify for Creators — use their own APIs/tooling); for audio production or editing (ffmpeg and DAW territory); for generic RSS feed parsing or website rendering (parse the feed XML directly — Transistor says the API is not a back-end data source); for creating new shows (the API cannot — the dashboard does); or for platform-level distribution questions (Apple/Spotify submission is a dashboard and RSS concern).
Reference Files
| File | Use it for |
|---|---|
| references/auth-and-basics.md | x-api-key auth, key location and role scoping, the JSON:API envelope (data/attributes/relationships/included[]) with jq patterns, pagination params, error surfaces |
| references/endpoint-catalog.md | Every route's method, path, and parameters (shows, episodes, publish, uploads, analytics, subscribers, webhooks) plus routes that do not exist |
| references/episode-publish-lifecycle.md | The draft/scheduled/published state machine, the exact publish request/response shapes, create→audio→publish recipes in CLI and curl, authorize-upload detour |
| references/gotchas-and-recipes.md | Symptom → cause → fix field guide (404 user route, silent pagination, 429 storms...) and multi-step workflows (bulk scheduling, analytics reports, subscriber import, webhooks) |
Available Scripts and Prerequisites
scripts/transistor— the bundled Python CLI (--json,--dry-run,--force,--quiet,--verbose,--helpeverywhere). Imports only the standard library andrequests; sends write bodies exactly as documented (bracket-key form fields).scripts/test_transistor.py— offline test suite (pytest + unittest compatible); all HTTP mocked with canned JSON:API documents, zero network egress, no live-call cases (Transistor is a keyed API).- Requires Python 3.8+,
requests, andTRANSISTOR_API_KEYfor live commands (Account page → API Access). No service is started by this skill.