REST API Reference¶
Base URL: https://api-v5.anchor.fm
The anchor.fm internal REST API handles episode creation, metadata read/write, show settings, and media upload. It predates and coexists with the GraphQL API — Anchor's acquisition by Spotify left this backend intact.
Common Headers¶
All requests require:
POST / PUT requests additionally require:
Content-Type: application/json
Origin: https://creators.spotify.com
Referer: https://creators.spotify.com/
Common Query Parameters¶
| Parameter | Description |
|---|---|
isMumsCompatible=true |
Required on every endpoint. Never omit. |
returnWebIds=true |
Recommended on /overview. Omitting it may drop some fields. |
ID System¶
The anchor.fm API uses anchor numeric IDs (integers), not the Spotify string IDs visible in the S4C UI. You need to convert between them before calling most endpoints.
| ID type | Example | Used in |
|---|---|---|
| Spotify Episode ID | "1xouj0WrH2klavXKzDWZbq" |
S4C UI, GraphQL |
| Anchor Episode ID | 123456789 |
anchor.fm REST endpoints |
| Spotify Show ID | "YOUR_SHOW_ID" |
S4C UI, GraphQL |
| Station ID (numeric) | YOUR_STATION_ID |
anchor.fm REST endpoints |
1-A. Spotify Episode ID → Anchor Numeric ID¶
Translates a Spotify Episode ID into the numeric ID required by all other episode endpoints.
Response¶
1-B. Get Episode Overview¶
Returns the current metadata for an episode.
Response fields¶
| Field | Type | Description |
|---|---|---|
title |
string | Episode title |
description |
string | Description HTML |
isPublished |
boolean | Whether the episode is published |
publishOn |
string | null | Scheduled publish time (UTC ISO 8601). Present only for scheduled episodes |
userId |
integer | Account user ID — required for update calls |
podcastEpisodeType |
string | "full" / "trailer" / "bonus" |
podcastEpisodeIsExplicit |
boolean | Explicit content flag |
Episode state matrix¶
isPublished |
publishOn |
State |
|---|---|---|
false |
present | Scheduled |
true |
— | Published |
false |
absent | Draft |
Draft episodes return 403
Calling this endpoint on a draft episode (isPublished: false, no
publishOn) returns HTTP 403 Forbidden — even in a live browser
session. The S4C UI fetches draft data via GraphQL instead. If you need
to update a draft episode's metadata, keep the original values in memory
before calling the update endpoint.
1-C. Update Episode (Title / Description / Publish Date)¶
Updates episode metadata. You can change the title, description, episode type, explicit flag, and scheduled publish time in a single call.
Request body¶
{
"userId": YOUR_USER_ID,
"title": "Episode title",
"description": "<p>Episode description HTML</p>",
"episodeType": "full",
"isPublished": false,
"podcastEpisodeIsExplicit": false,
"publishOn": "2026-05-31T21:00:00.000Z",
"wizardDraftedToPublishOn": "2026-05-31T21:00:00.000Z"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
userId |
integer | Yes | Copy directly from the /overview response |
title |
string | Yes | Omitting it risks the server setting a blank title |
description |
string | Yes | HTML format |
episodeType |
string | Yes | "full" / "trailer" / "bonus" |
isPublished |
boolean | Yes | Pass the current value from /overview to preserve state |
podcastEpisodeIsExplicit |
boolean | Yes | Same as above |
publishOn |
string | No | UTC ISO 8601. Required when scheduling |
wizardDraftedToPublishOn |
string | No | Set to the same value as publishOn |
Response¶
HTTP 200 on success:
{
"description": "<p>Episode description HTML</p>",
"title": "Episode title",
"didChangePublishState": true,
"shareUrl": "https://creators.spotify.com/pod/show/..."
}
Scheduling behaviour
Sending isPublished: false together with a publishOn value causes
the server to automatically treat it as isPublished: true (scheduled
for future publication). This is the correct way to schedule an episode.
Published episodes lock publishOn
Once an episode is published (isPublished: true), you cannot change
publishOn via this endpoint. Use the S4C UI or Playwright to modify
the publish date of a live episode.
publishOn timezone
Always specify publishOn in UTC. To publish at 06:00 JST, use the
previous day at 21:00 UTC.
payload = {
"userId": overview["userId"],
"title": "New episode title",
"description": "<p>New description</p>",
"episodeType": overview.get("podcastEpisodeType", "full"),
"isPublished": overview.get("isPublished", False),
"podcastEpisodeIsExplicit": overview.get("podcastEpisodeIsExplicit", False),
"publishOn": "2026-06-01T21:00:00.000Z",
"wizardDraftedToPublishOn": "2026-06-01T21:00:00.000Z",
}
r = requests.post(
f"https://api-v5.anchor.fm/v3/episodes/{anchor_id}/update?isMumsCompatible=true",
json=payload,
headers=headers_post,
)
curl -s -X POST \
-H "Authorization: Bearer $BEARER" \
-H "Content-Type: application/json" \
-H "Origin: https://creators.spotify.com" \
-H "Referer: https://creators.spotify.com/" \
-d '{"userId":YOUR_USER_ID,"title":"New title","description":"<p>Desc</p>","episodeType":"full","isPublished":false,"podcastEpisodeIsExplicit":false,"publishOn":"2026-06-01T21:00:00.000Z","wizardDraftedToPublishOn":"2026-06-01T21:00:00.000Z"}' \
"https://api-v5.anchor.fm/v3/episodes/${ANCHOR_ID}/update?isMumsCompatible=true"
1-D. Get Region¶
Returns the user account's region. Called automatically when loading the episode detail page.
Example response: { "region": "JP" }
1-E. Get Sponsored Content Status¶
Returns whether the episode contains sponsored content.
Response: { "containsSponsoredContent": false }
1-F. Get Cover Art Colors¶
Returns the color palette extracted from an image URL. Called when rendering cover art in the UI.
Request body¶
1-G. Get Episode Form Data (Batch)¶
You can supply multiple episodeUri[] parameters to batch-fetch form data.
Called when the episode list page loads.
1-H. Get Show Metadata¶
Returns show-level information: title, description, categories, cover art, and distribution flags. Called when the show settings page loads.
The STATION_ID is the anchor numeric station ID (distinct from the user
ID). You can obtain it via the ID conversion endpoint (1-L).
Response fields¶
| Field | Type | Description |
|---|---|---|
rssFeedUrl |
string | RSS feed URL |
podcastName |
string | Show title |
podcastDescription |
string | Show description |
podcastAuthorName |
string | Author name |
podcastAuthorEmail |
string | Author email |
podcastImage |
string | Cover art URL (original size) |
podcastImage400 |
string | Cover art URL (400 px) |
podcastCategory |
string | Category in "Arts\|Design" format |
podcastLanguage |
string | Language code (e.g. "ja") |
language |
string | Same as podcastLanguage |
isDistributedOnSpotify |
boolean | Whether the show is on Spotify |
isRssFeedDisabled |
boolean | Whether the RSS feed is disabled |
hasPublishedEpisode |
boolean | Whether at least one episode is published |
distributionEligibilityForPaywalls |
string | Paywall eligibility ("eligible" etc.) |
isStationOver30DaysOld |
boolean | Whether the show is older than 30 days |
isStationMarkedAsSpam |
boolean | Whether the show is flagged as spam |
1-I. Get Partner IDs¶
Returns partner IDs related to monetisation (ads, Fan Support, etc.). Called when the show settings page loads.
1-J. Get User Verification Status¶
Returns the account's identity verification state.
1-K. Onboarding Status¶
GET /v3/onboarding/redirectStatus/{USER_ID}/?isMumsCompatible=true
GET /v3/onboarding/steps/{USER_ID}/?userId={USER_ID}&isMumsCompatible=true
Returns onboarding progress for new users. Has no practical effect on established accounts.
1-L. Spotify Show ID → Anchor Numeric IDs¶
Converts a Spotify Show ID into the anchor.fm internal numeric IDs needed by most other endpoints.
Response¶
| Field | Description |
|---|---|
stationId |
The numeric ID used in episode list, metadata, and other endpoints |
userId |
The numeric user account ID |
webStationId |
The URL slug (vanity ID) |
1-M. Update Episode List Index¶
Synchronises the episode list index. Called automatically by the browser when the episode list page loads.
Response: { "success": true }
1-N. Get Show Form Data¶
Response: { "formUrl": "" }
1-O. Get Distribution Settings¶
Returns the show's distribution and RSS status. Called when the episode list page loads.
Response fields¶
| Field | Type | Description |
|---|---|---|
canEnableRss |
boolean | Whether RSS distribution can be enabled |
externalUrls |
object | E.g. {"spotify": "https://open.spotify.com/show/..."} |
spotifyDistributionStatus |
string | Spotify distribution status |
spotifyShowUri |
string | Spotify Show URI |
platforms |
array | List of active distribution platforms |
vanitySlug |
string | URL slug |
isEnterprisePodcast |
boolean | Enterprise plan flag |
1-P. Get User Settings¶
Example response¶
{
"termsOfServiceVersion": 1,
"agreedToMinimumRequiredTermsOfService": true,
"contactabilityStatus": "optedOut",
"countryRule": "needsResponseOptInSelected"
}
1-Q. Update Sponsored Content Status¶
Sets whether the episode contains sponsored content. Called automatically when saving an episode.
Request body¶
| Parameter | Type | Required | Description |
|---|---|---|---|
containsSponsoredContent |
boolean | Yes | Whether the episode includes sponsored content |
publishOn |
integer | Yes | Publish time as a Unix timestamp in seconds (not ISO 8601) |
Warning
publishOn here is a Unix timestamp (integer seconds), not an ISO 8601
string. Passing an ISO 8601 string will silently corrupt the value.
1-R. Sync Monetisation Lifecycle¶
Synchronises and fetches the monetisation state (e.g. Spotify Partner Program). Called when the episode list page loads.
Key response fields¶
| Field | Description |
|---|---|
products.SPOTIFY_PARTNER_PROGRAM.lifecycleState |
Partner program state |
products.SPOTIFY_PARTNER_PROGRAM.pendingRequirements |
Outstanding requirements |
1-S. Create a New Episode¶
Creates an empty episode record. This is the first step in the episode wizard — it allocates the episode ID before audio or metadata are set.
Request body¶
| Parameter | Type | Description |
|---|---|---|
hourOffset |
integer | User timezone offset (JST = -9) |
Response¶
Three-step episode creation
POST /v3/stations/{STATION_ID}/episodes— allocate the episode record (this endpoint)- Upload audio/video via the media upload flow (6-A through 6-D)
POST /v3/episodes/{ANCHOR_ID}/update(1-C) — set title, description, and publish settings
1-T. Update Show Metadata¶
Updates show-level metadata: title, description, author, category, language, and explicit flag. Called when the podcast settings page is saved.
Request body¶
{
"podcastName": "Your Podcast Title",
"podcastDescription": "Show description",
"podcastAuthorName": "Author Name",
"podcastCategory": "Arts|Design",
"podcastLanguage": "ja",
"podcastEpisodeIsExplicit": false
}
| Parameter | Type | Required | Description |
|---|---|---|---|
podcastName |
string | Yes | Show title (max 100 characters) |
podcastDescription |
string | No | Show description (max 600 characters) |
podcastAuthorName |
string | No | Author name (max 80 characters) |
podcastCategory |
string | No | Category in "Arts\|Design" format; fetch valid values from /v3/settings/podcast-category/options |
podcastLanguage |
string | No | Language code (e.g. "ja") |
podcastEpisodeIsExplicit |
boolean | No | Explicit content flag |
HTTP 200 returns the updated metadata object.
1-U. Update Vanity Slug¶
Updates the show's custom URL slug.
Request body¶
Warning
webStationId must be globally unique. Using an already-taken slug may
return 409 Conflict.
1-V. Image Upload Flow (Cover Art / Episode Thumbnail)¶
Cover art and episode thumbnails share the same upload pipeline but use different endpoints. The pipeline is separate from the media upload pipeline (6-A through 6-D).
1-V-1. Get a Signed Upload URL¶
Show cover art:
Episode thumbnail:
Response:
{
"url": "https://storage.googleapis.com/anchor-image-storage-production/{UUID}/{FILENAME}?X-Goog-Algorithm=...",
"uploadId": "a46e21a4-1d2d-1686-1355-905e191a3bef"
}
1-V-2. PUT the Image to Google Cloud Storage¶
No auth headers needed — the signature is embedded in the URL.
1-V-3. Notify Upload Completion¶
Show cover art:
Body:{} or empty.
Episode thumbnail:
Body:
1-V-4. Poll for Completion¶
Show cover art:
Episode thumbnail:
Poll until status equals "completed" (typically a few seconds).
1-W. Monetisation Endpoints¶
Called from the monetisation page (/pod/show/{SHOW_ID}/monetize/...):
GET /v4/paywalls/{STATION_ID}/lifecycle-state?isMumsCompatible=true
GET /v6/wallet/{STATION_ID}/availability?isMumsCompatible=true
GET /v6/wallet/{STATION_ID}/?isMumsCompatible=true
GET /v3/paywalls/{STATION_ID}/?isMumsCompatible=true
These endpoints return paywall and wallet state. Used together with 1-R.
1-X. Show Settings Page Endpoints¶
Called from the podcast settings page (/pod/show/{SHOW_ID}/podcast/edit):
GET /v3/stations/{STATION_ID}/episodePage?isMumsCompatible=true
GET /v3/station/{STATION_ID}/episodes/featured?isMumsCompatible=true
GET /v3/stations/{STATION_ID}/hostRecommendations?isMumsCompatible=true
These return "Start Here" featured episode and host recommendation settings.
1-Y. Distribution Page Endpoints¶
Called from the distribution page (/pod/show/{SHOW_ID}/podcast/distribution):
GET /v3/onboarding/redirectStatus/{STATION_ID}/?isMumsCompatible=true
GET /v3/onboarding/steps/{STATION_ID}/?userId={USER_ID}&isMumsCompatible=true
GET /v3/stations/status?isMumsCompatible=true
/v3/stations/status returns a list of all shows associated with the
currently authenticated user.
Known Quirks¶
description field — server-side transformations¶
When the server stores the description field, it automatically applies
these transformations:
| Input | Stored as |
|---|---|
<br> |
<br /> |
<a href="..."> |
<a href="..." rel="ugc noopener noreferrer" target="_blank"> |
If you compare the raw value from /overview against the value you
originally posted, they will not match exactly. Normalise both sides before
comparing.
publishOn must be an ISO 8601 string — not a Unix timestamp¶
Passing an integer (e.g. the year 2026) as publishOn in the update body
will produce a timestamp like "1970-01-01T00:33:46.000Z". Always use a
full ISO 8601 string such as "2026-06-01T21:00:00.000Z".
Media Upload Flow (Audio / Video)¶
See Recipes — Media upload for the full end-to-end Python example using endpoints 6-A through 6-D.
Supported formats¶
mp3, m4a, aifc, aiff, ogg, wav, flac, mp4, mov
File size limits¶
| Type | Limit |
|---|---|
| Video | 4 GB |
| Audio | 500 MB |
6-A. Get a Signed Upload URL¶
GET /v3/episodes/{ANCHOR_ID}/upload/signedUrl?filename={FILENAME}&type={MIME_TYPE}&isMumsCompatible=true
| Parameter | Example | Description |
|---|---|---|
filename |
episode.mp4 |
File name to upload |
type |
video/mp4 |
MIME type |
Response:
{
"uploadId": "9e1fdfce-4485-6bde-e582-3b7c84978d55",
"url": "https://storage.googleapis.com/anchor_exclusive_media_upload_production/...",
"headers": {}
}
6-B. PUT the File to Google Cloud Storage¶
Save the ETag response header — you need it in the next step.
6-C. Notify Upload Completion¶
Request body (video):
{
"userId": YOUR_STATION_ID,
"uploadType": "video",
"origin": "episode-media:upload",
"caption": "episode.mp4",
"isExtractedFromVideo": true,
"isMultipartUpload": true,
"parts": [{ "partNumber": 1, "etag": "4d49fffd0ed1c6a291577ac47d26b997" }],
"uploadId": "9e1fdfce-4485-6bde-e582-3b7c84978d55",
"episodeId": 120525547,
"stationId": YOUR_STATION_ID
}
| Parameter | Type | Description |
|---|---|---|
uploadType |
string | "video" for video files; "default" for audio files (not "audio") |
isExtractedFromVideo |
boolean | true for mp4/mov; false for audio-only |
parts |
array | List of ETags from the GCS PUT response |
Warning
For audio files, use uploadType: "default", not "audio". This
was confirmed against a live session in May 2026.
6-D. Poll for Media Validation¶
| Response field | Description |
|---|---|
status |
"processing" / "completed" / "failed" |
mediaType |
"video" / "audio" |
durationInMilliseconds |
Duration in ms |
validationStatus |
Validation result |
Poll until status is "completed".