Full skill-builder rebuild of transistor per issue #407: - scripts/transistor (renamed from transistor-cli): 19 commands covering the verified API surface - user probe (GET /v1; the /v1/user route does not exist), shows/episodes with corrected pagination[page]/pagination[per], the dedicated episode publish endpoint (PATCH /v1/episodes/:id/publish with episode[status]=draft|scheduled|published), authorize-upload flow, the three real analytics routes with downloads[] array summing, subscriber management incl. batch, and webhooks. Write bodies are form-encoded bracket keys exactly as documented; dry-run plans carry method/path/params/body; publish guard refuses audio-less episodes. Fixed stale claims: /analytics/show -> /v1/analytics/..., totals -> downloads arrays, pagination[limit] -> pagination[per], user email -> name/time_zone, dropped invented episodes_count/subscribers_count and POST /v1/shows (show creation is dashboard-only). - scripts/test_transistor.py: 50 offline tests (pytest + unittest green, proxy-trap clean) covering help, argument errors, dry-run plans, canned JSON:API compound-document parsing (data/attributes/relationships/ included[]), write-path body shapes, publish guard, create->audio-> publish pipeline, and HTTP error signatures. - references/: auth+JSON:API envelope with jq patterns, endpoint catalog, publish lifecycle with documented request/response shapes, gotchas field guide + worked recipes; all cited to live-verified sources. - evals/evals.json: 8 schema-v1 cases incl. two should-not-trigger probes. - SKILL.md rewritten (308 lines), README refreshed, root README blurb and generated catalogs synced (marketplace.json + llms.txt descriptions). Publish-body shape reconciliation: the contract's data.id+data.type JSON:API PATCH premise was falsified by current official docs (verified 2026-08-29) and the flimzy/transistor Go SDK; implemented reality escalated in handoff (see library/transistor-api-facts.md). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
8.2 KiB
Transistor API: Authentication, JSON:API Envelope, and Request Basics
Everything in this file is from the official API reference (developers.transistor.fm) and Transistor's own support pages, verified live at authoring time. Transistor.fm's public API is v1 and speaks JSON:API on responses; there is exactly one authentication mode.
Authentication
- Every request carries an HTTP header
x-api-keywhose value is the API key. There is no OAuth, no bearer token, and no signing on the REST API. - Keys are created, viewed, and reset in the Transistor Dashboard's Account page, in the section marked API Access (https://dashboard.transistor.fm/account). Transistor's support article "Does Transistor have an API?" (updated 2026-07) names exactly this location; the bundled CLI prints it on every auth error.
- A key grants whatever the associated dashboard user can see: access to podcasts and episodes follows the user's podcast role — owner, admin, or regular team member. There are no narrower per-key scopes: a leaked key is as powerful as its user. Treat it like a password; reset it from the same Account page if it leaks.
- The authorization probe is
GET /v1— it returns the authenticateduserresource and nothing else. There is no/v1/userand no/v1/authorizationroute; older tutorials that call/v1/userget a 404. Theuserresource hasname,time_zone,image_url, and timestamps — it has no email attribute.
curl https://api.transistor.fm/v1 -H "x-api-key: <API_KEY>"
Rate limits
- 10 requests per 10 seconds. Exceeding the limit returns HTTP
429and access is blocked for 10 seconds; after that requests flow again. - No rate-limit headers (
Retry-Afteretc.) are documented — don't parse for them; just back off on 429. - Transistor explicitly states the API is not meant to be the main data
source for a website or app back end; pull data once, cache it, and parse
the public RSS feed XML when you would otherwise hammer the API. For
push-style updates, webhooks (see the endpoint catalog) exist for
episode_created,episode_published,subscriber_created, andsubscriber_deleted.
The JSON:API envelope
Responses are JSON:API documents. Learn four keys and every endpoint is readable:
| Key | Shape | Meaning |
|---|---|---|
data |
object (single resource) or array (collections) | The primary resource(s) of the response |
attributes |
object inside a resource | The resource's fields (title, status, media_url, ...) |
relationships |
object of {"<name>": {"data": {"id", "type"}}} |
Links to related resources by id and type |
included |
array (only when requested with include[]) |
The full related resources — a "compound document" |
- Resource
typevalues:user,show,episode,subscriber,show_analytics,episodes_analytics,episode_analytics,audio_upload,webhook. - Single-resource responses wrap one object:
{"data": {"id": ..., "type": "episode", "attributes": {...}, "relationships": {...}}}. - Collection responses wrap an array plus pagination under
meta:{"data": [...], "meta": {"currentPage", "totalPages", "totalCount"}}. - Ids are strings even when numeric ("3056098"); analytics ids may be slugs ("the-caffeine-show"). Keep ids as strings end to end.
included[]appears only when you ask for it.GET /v1/episodes/3056098?include[]=showreturns the episode plus the parent show inincluded, matched viadata.relationships.show.data.id.
jq patterns for the envelope
# Single resource: unwrap data.attributes
curl -s https://api.transistor.fm/v1/episodes/<EPISODE_ID> -H "x-api-key: <API_KEY>" \
| jq '.data.attributes | {title, status, published_at}'
# Collection: titles plus ids, one per line
curl -s 'https://api.transistor.fm/v1/episodes?show_id=<SHOW_ID>' -H "x-api-key: <API_KEY>" \
| jq -r '.data[] | [.id, .attributes.title, .attributes.status] | @tsv'
# Compound document: pull the parent show's title out of included[]
curl -s 'https://api.transistor.fm/v1/episodes/<EPISODE_ID>?include[]=show' -H "x-api-key: <API_KEY>" \
| jq --arg id "$(curl -s ... | jq -r '.data.relationships.show.data.id')" \
'.included[] | select(.type == "show" and .id == $id) | .attributes.title'
# Simpler: match included[] by type when only one show was included
... | jq '.included[] | select(.type == "show") | .attributes.title'
# Pagination loop values live in meta
... | jq '{page: .meta.currentPage, last: .meta.totalPages, total: .meta.totalCount}'
The bundled CLI does this unwrapping for --json output: collections come
back as {"episodes": [...], "meta": {...}} with each item flattened to the
fields agents actually need (including show_id from relationships), and
single resources as one flat object.
Pagination
- Page-based, two parameters:
pagination[page](documented default0; the doc examples explicitly request page1) andpagination[per](default10). - Every collection returns
meta.currentPage,meta.totalPages, andmeta.totalCount. Loop whilecurrentPage < totalPages, incrementing the page — do not assume the first page is0or1, readmeta. - There is no cursor, no
page[number]/page[size]JSON:API-style spelling, and nopagination[limit]— unknown params are silently ignored, which is exactly how scripts that "paginate" withpagination[limit]re-read the first page forever.
Sparse fieldsets and compound documents
Any endpoint accepts JSON:API's standard extras:
- Sparse fieldsets:
fields[episode][]=title&fields[episode][]=media_urlreturns only those attributes (smaller payloads, faster loops). - Include related resources:
include[]=showon an episode,include[]=showon analytics,include[]=episodeon episode analytics. Combine both:include[]=show&fields[show][]=title&fields[show][]=feed_url.
Request bodies: form-encoded bracket keys (documented), JSON accepted
- The reference intro says endpoints accept JSON or form-encoded request
bodies. Every documented mutation example uses form-encoded bracket keys:
episode[show_id]=...,episode[title]=...,show[title]=...,subscriber[email]=...,episode[status]=published. - The docs publish no JSON-body equivalent examples, so the bracket-key shapes above are the contract to copy. The bundled CLI sends form-encoded bodies byte-compatible with the documented curl examples.
- Required-vs-optional matters:
episode[show_id]is the only required field on episode creation;episode[status]is required on the publish endpoint;show_idis required on subscribers/webhooks listings.
Error surfaces
Responses use standard HTTP codes. The reference does not document a formal error schema, so program defensively:
401— key missing/invalid → checkx-api-keyand the Account page.403— key valid, role insufficient (owner/admin needed for some operations on a shared podcast).404— id/slug not found (and remember:/v1/useris not a route).422— validation errors (e.g. badepisode[status]value).429— rate limit (10 requests / 10 s window).
Error bodies seen in practice are JSON; the bundled CLI accepts either a
JSON:API-style errors[] array or a bare {"message": ...} object and
flattens whichever it gets into one stderr line.
Sources
- https://developers.transistor.fm/ (introduction, JSON:API conformance, authentication, rate limits, sparse fieldsets/include[] sections; all endpoint examples) — fetched live 2026-08-29 (HTTP 200)
- https://developers.transistor.fm/#authentication (header name, Account Area key management, owner/admin/team-member access levels)
- https://developers.transistor.fm/#ratelimits (10 requests / 10 s, 429 + 10 s block, caching/RSS guidance)
- https://developers.transistor.fm/#get-v1 (GET /v1 user resource example)
- https://developers.transistor.fm/#resources (type list; User resource fields — no email)
- https://support.transistor.fm/en/article/does-transistor-have-an-api-1b24sjo/ (API key location: Account page → API Access) — fetched live 2026-08-29
- https://support.transistor.fm/en/article/what-automations-are-possible-with-transistor-bi27am/ (supported automations, show-creation limitation) — fetched live 2026-08-29