API Documentation¶
Django Cast provides a comprehensive REST API for content management, media handling, and theme customization. The API supports both traditional Django session authentication and enables headless CMS usage.
Overview¶
The API is available at /api/ and includes:
Media management (audio, video, images)
Content access via Wagtail API
Faceted search capabilities
Theme management
Comment moderation data export
Authentication¶
Authenticated endpoints use Django session authentication (for example /api/videos/,
/api/audios/, and /api/comment_training_data/).
Browsable-API login/logout routes are project-specific. In the example project they are:
/api-auth/login//api-auth/logout/
Public endpoints (no authentication required) include:
Podlove audio format
Player configuration
Theme listing and theme update
Wagtail pages and images
Facet counts
Endpoints¶
Media Management¶
Videos
List videos:
GET /api/videos/
Retrieve or delete a video:
GET /api/videos/{id}/
DELETE /api/videos/{id}/
Upload video file:
POST /api/upload_video/
Example response:
{
"id": 1,
"url": "http://localhost:8000/api/videos/1/",
"original": "http://localhost:8000/media/cast_videos/demo.mp4",
"poster": "http://localhost:8000/media/cast_videos/poster/poster_abcd.jpg",
"poster_thumbnail": "http://localhost:8000/media/images/poster.width-300.jpg"
}
Audio
List audio files:
GET /api/audios/
Retrieve or delete audio:
GET /api/audios/{id}/
DELETE /api/audios/{id}/
Podlove player format (public):
GET /api/audios/podlove/{id}/
GET /api/audios/podlove/{id}/post/{post_id}/
Returns audio data formatted for the Podlove Web Player, including chapters,
transcripts, and a top-level contributors list. Contributors are derived
from non-blank transcript speaker/voice labels in first-appearance
order; the Podlove Web Player resolves transcript segment speakers against
them and renders their names:
"contributors": [
{"id": "Speaker 1", "name": "Speaker 1"}
]
Audio list/detail responses include these serializer fields:
{
"id": 12,
"name": "Episode 12",
"file_formats": "m4a mp3",
"url": "http://localhost:8000/api/audios/12/",
"podlove": "http://localhost:8000/api/audios/podlove/12/",
"mp3": "http://localhost:8000/media/cast_audio/episode-12.mp3"
}
Player configuration:
GET /api/audios/player_config/
GET /api/audios/player_config/?color_scheme=dark
Returns player theme and configuration settings. The color_scheme query
parameter accepts light or dark and can be used by themes with
client-side color mode switching.
Content Access¶
Wagtail Pages API
Access page content with filtering:
GET /api/wagtail/pages/
GET /api/wagtail/pages/{id}/
Standard Wagtail filters:
type: Filter by page type (e.g.,cast.Post,cast.Episode)child_of: Filter to only include direct children of the page with this IDdescendant_of: Filter to include all descendants of the page with this IDtranslation_of: Filter to only include translations of the page with this IDfields: Comma-separated list of fields to include in responseorder: Ordering field (prefix with-for descending)slug: Filter by slug (exact match)show_in_menus: Filter pages shown in menussearch: Full-text search (note: when usinguse_post_filter=true, this uses Django Cast’s enhanced search)limit: Number of results per pageoffset: Number of results to skip
Django Cast custom filters (requires use_post_filter=true):
use_post_filter: Set totrueto enable Django Cast’s enhanced filteringdate_facets: Filter by year-month (format:YYYY-MM)category_facets: Filter by category slugtag_facets: Filter by tag slugdate_after: Filter posts after date (format:YYYY-MM-DD)date_before: Filter posts before date (format:YYYY-MM-DD)o: Ordering by visible_date (use-visible_datefor descending)
Example requests:
Standard Wagtail filtering:
GET /api/wagtail/pages/?type=cast.Post&child_of=4
Enhanced Django Cast filtering:
GET /api/wagtail/pages/?type=cast.Post&use_post_filter=true&category_facets=tech&date_after=2024-01-01
Filter posts by month:
GET /api/wagtail/pages/?type=cast.Post&use_post_filter=true&date_facets=2024-03
Combined filters:
GET /api/wagtail/pages/?type=cast.Post&child_of=4&use_post_filter=true&tag_facets=python&date_facets=2024-03
Images API
Access images:
GET /api/wagtail/images/
GET /api/wagtail/images/{id}/
Search and Discovery¶
Facet Counts
List blogs that expose the detail endpoint:
GET /api/facet_counts/
If cast is mounted at /cast/, this becomes /cast/api/facet_counts/.
Example list response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 4,
"url": "http://localhost:8000/cast/api/facet_counts/4/"
}
]
}
Detail endpoint supports two response modes:
Legacy mode (default)¶
Request:
GET /api/facet_counts/{blog_id}/
Optional filter params are passed through the same filterset as the blog list:
search, date_after, date_before, date_facets, category_facets, tag_facets, o.
Example:
GET /api/facet_counts/4/?search=python&tag_facets=django
Legacy response shape:
{
"id": 4,
"url": "http://localhost:8000/cast/api/facet_counts/4/",
"facet_counts": {
"date_facets": [
{"slug": "2026-02", "name": "2026-02", "count": 2}
],
"category_facets": [
{"slug": "til", "name": "Today I Learned", "count": 1}
],
"tag_facets": [
{"slug": "django", "name": "django", "count": 1}
]
}
}
Modal mode¶
Request:
GET /api/facet_counts/{blog_id}/?mode=modal
Supported modal selection params:
search, date_facets, category_facets, tag_facets, o.
Example:
GET /api/facet_counts/4/?mode=modal&search=python&tag_facets=django&category_facets=til
Modal response shape:
{
"mode": "modal",
"result_count": 1,
"groups": {
"date_facets": {
"selected": "",
"all_count": 1,
"options": [
{"slug": "2026-02", "name": "2026-02", "count": 1},
{"slug": "2026-01", "name": "2026-01", "count": 0}
]
},
"tag_facets": {
"selected": "django",
"all_count": 2,
"options": [
{"slug": "django", "name": "django", "count": 1},
{"slug": "python", "name": "python", "count": 1}
]
},
"category_facets": {
"selected": "til",
"all_count": 2,
"options": [
{"slug": "til", "name": "Today I Learned", "count": 1},
{"slug": "weeknotes", "name": "WeekNotes", "count": 1}
]
}
}
}
Notes:
Only
mode=modalenables modal responses; unknown modes fall back to legacy output.Group keys are limited to configured facet groups from
CAST_FILTERSET_FACETSintersected with:date_facets,tag_facets,category_facets.Legacy mode uses conjunctive counts (fully filtered result set).
Modal mode uses disjunctive per-group counts (temporarily excludes the group being counted).
optionsinclude zero-count values so modal UIs can keep disabled choices visible.ois accepted for URL-state parity but does not change modal counts.date_after/date_beforeare part of the list filterset, but are not currently applied in modal mode.
Theme Management¶
List available themes:
GET /api/themes/
Update selected theme:
POST /api/update_theme/
Content-Type: application/json
{"theme_slug": "bootstrap5"}
The theme-update endpoint is session-based and does not require authentication with django-cast’s default DRF configuration. It validates the submitted theme slug and stores the selection using the same theme-selection flow as the frontend theme switcher.
Content Editing (Editor API)¶
The editor API lives under /api/editor/ and is intended for trusted
clients — scripts, agents, or headless tools — that need to create draft
content without direct database access. It is authentication-mechanism
agnostic: any DRF authentication class that populates request.user works.
Django-cast ships session authentication by default. Editor endpoints require
an authenticated user with Wagtail admin access and then apply Wagtail page or
collection permissions for the requested object.
List editable parents:
GET /api/editor/parents/
Returns the Blog and Podcast pages the authenticated user has
add-child permission for. Requires authentication; unauthenticated requests are
rejected (403 Forbidden under the default session authentication, 401 for
authentication schemes that supply a WWW-Authenticate challenge).
Example response:
[
{"id": 4, "title": "My Blog", "type": "cast.Blog",
"api_url": "/api/editor/posts/"},
{"id": 7, "title": "My Podcast", "type": "cast.Podcast",
"api_url": "/api/editor/episodes/"}
]
api_url is the create endpoint hint for that parent’s primary content type:
a cast.Blog points at /api/editor/posts/ and a cast.Podcast points
at /api/editor/episodes/. Podcasts also accept plain posts; clients that
want a post under a podcast can still POST /api/editor/posts/ with that
podcast as the parent.
Podcast-level feed settings such as itunes_type are not edited through the
editor API. Manage them on the podcast page in Wagtail or through the Django
admin; editor API episode endpoints only manage episode-level publishing
metadata.
Create a draft post:
POST /api/editor/posts/
Content-Type: application/json
Creates a draft Post under the chosen parent page. The page is saved as a
Wagtail revision and is not published by this endpoint. Passing
"publish": true is rejected; use the explicit publish action after review.
Podcast episodes have their own create/read/update endpoints with the same shape
plus episode-specific fields; see Episode endpoints below.
Referenced media — cover_image and inline image, gallery,
audio, and video blocks — must be choosable by the caller. Missing and
inaccessible media references are reported the same way (not_found) so the
API does not leak objects outside the caller’s permissions. paragraph HTML
is validated through Wagtail’s rich-text block, the same path the admin uses on
save.
Request fields:
parent(required):{"id": <page id>}— must be aBlogorPodcastthe caller may add to.title(required): page title.slug(optional): URL slug; auto-derived fromtitleif omitted.visible_date(optional): ISO 8601 datetime string.cover_image(optional):{"id": <image id>, "alt_text": "…"}.tags(optional): list of tag name strings.categories(optional): list ofPostCategoryIDs.overview(required): ordered list of body blocks (see below). Send[]when creating a post that only populatesdetail.detail(optional): ordered list of body blocks.publish(optional): must befalseor absent.
Body block types accepted in overview and detail:
[
{"type": "heading", "value": "Notes"},
{"type": "paragraph", "value": "<p>Rich-text HTML.</p>"},
{"type": "code", "value": {"language": "python",
"source": "print('hi')"}},
{"type": "image", "value": {"id": 456}},
{"type": "gallery", "value": [{"id": 456}, {"id": 789}]},
{"type": "audio", "value": {"id": 42}},
{"type": "video", "value": {"id": 43}}
]
Stored body blocks that this API version cannot safely edit are returned as
{"type": "unsupported", "value": {"stored_type": "...", "position":
"detail.0"}} placeholders. This includes custom or unsupported block types
such as embed, and supported block types whose stored value cannot be
represented as an editable authoring block, such as deleted or inaccessible
media references, empty or malformed galleries, or malformed code blocks. To
preserve one of these blocks while replacing a section, send the placeholder
back with the same stored_type and position. The placeholder may move to
a different index in the submitted section; position identifies the original
stored block to preserve. Omitting a placeholder removes that stored block as
part of the full-section replacement.
Full create request example:
{
"parent": {"id": 123},
"title": "Weeknotes 2026-25",
"slug": "weeknotes-2026-25",
"visible_date": "2026-06-19T18:00:00+02:00",
"cover_image": {"id": 456, "alt_text": "Notebook and laptop on a desk"},
"tags": ["weeknotes"],
"categories": [],
"overview": [
{"type": "heading", "value": "Notes"},
{"type": "paragraph", "value": "<p>Shipped the first draft.</p>"},
{"type": "code", "value": {"language": "python",
"source": "print(\"hello\")"}},
{"type": "gallery", "value": [{"id": 456}, {"id": 789}]}
],
"detail": [
{"type": "video", "value": {"id": 43}}
],
"publish": false
}
Success response (201 Created):
{
"id": 987,
"type": "cast.Post",
"title": "Weeknotes 2026-25",
"slug": "weeknotes-2026-25",
"parent": {"id": 123},
"visible_date": "2026-06-19T18:00:00+02:00",
"tags": ["weeknotes"],
"categories": [],
"cover_image": {"id": 456, "alt_text": "Notebook and laptop on a desk"},
"overview": [
{"type": "heading", "value": "Notes"},
{"type": "paragraph", "value": "<p>Shipped the first draft.</p>"},
{"type": "code", "value": {"language": "python",
"source": "print(\"hello\")"}},
{"type": "gallery", "value": [{"id": 456}, {"id": 789}]}
],
"detail": [
{"type": "video", "value": {"id": 43}}
],
"latest_revision_id": 6543,
"live": false,
"status": "draft",
"preview_url": "/admin/pages/987/view_draft/",
"edit_url": "/admin/pages/987/edit/",
"api_url": "/api/editor/posts/987/"
}
Read a draft post:
GET /api/editor/posts/{id}/
Returns editable metadata, normalized overview and detail block lists
(same structure as the create request), revision metadata, and admin URLs for
an existing draft. Missing sections are returned as empty lists. Requires the
caller to have edit permission for the page.
Use this endpoint — not the public Wagtail pages API — to read back a draft:
the Wagtail pages API returns only live pages and does not expose authoring
source or revision IDs.
Preview a draft post:
GET /api/editor/posts/{id}/preview/
Returns the latest editable revision rendered through django-cast’s normal
Wagtail preview path. On success the response is the full themed page with
Content-Type: text/html; charset=utf-8 rather than a JSON envelope, so a
client can load it directly in a browser or iframe. The endpoint renders the
same latest revision that GET, PATCH, and the publish action operate on:
an unpublished draft when one exists, otherwise the current live revision.
Errors still use the existing editor JSON envelopes (application/json), for
example not_found for a missing post or permission_denied when the
caller cannot edit the page. Preview is a read action and requires no token
scope, but the caller must still be authenticated, have Wagtail admin access,
and have edit permission for the page.
Update a draft post:
PATCH /api/editor/posts/{id}/
Content-Type: application/json
Creates a new Wagtail draft revision for an existing Post. The request
must include the latest_revision_id returned by GET or the previous
write response as base_revision_id. Omitted fields are left unchanged; a
provided overview or detail replaces that whole section, including when
the supplied value is an empty list. Omitted sections and unsupported/custom
top-level body sections are preserved. At least one mutable field besides
base_revision_id is required; publish: false does not count as a
mutable field and publish: true is rejected as unsupported.
Update fields:
base_revision_id(required): optimistic concurrency token.title(optional): page title.slug(optional): URL slug; must remain unique under the same parent.visible_date(optional): ISO 8601 datetime string.cover_image(optional):{"id": <image id>, "alt_text": "…"}, ornullto clear the draft cover image.tags(optional): full replacement list of tag name strings.categories(optional): full replacement list ofPostCategoryIDs.overview(optional): full replacement ordered list of body blocks.detail(optional): full replacement ordered list of body blocks.publish(optional): must befalseor absent.
Example update request:
{
"base_revision_id": 6543,
"title": "Updated draft title",
"overview": [
{"type": "paragraph", "value": "<p>Updated draft text.</p>"}
]
}
A stale base revision returns 409 Conflict:
{
"code": "revision_conflict",
"detail": "The page has a newer revision than the submitted base revision.",
"current_revision_id": 6550,
"submitted_base_revision_id": 6543,
"edit_url": "/admin/pages/987/edit/"
}
Publish a draft post:
POST /api/editor/posts/{id}/publish/
Publishes the latest draft revision for an existing Post through Wagtail’s
revision publishing path. The caller must be authenticated, have Wagtail admin
access, be able to edit the target page through the editor API, and have Wagtail
publish permission for that page. The action does not accept a request body and
does not use If-Match or base_revision_id in this API version; clients
should GET the post immediately before presenting a publish action when they
need to confirm the latest draft content.
The success response is the normal editor post shape plus publish metadata:
{
"id": 987,
"type": "cast.Post",
"title": "Weeknotes 2026-25",
"slug": "weeknotes-2026-25",
"parent": {"id": 123},
"visible_date": "2026-06-19T18:00:00+02:00",
"tags": ["weeknotes"],
"categories": [],
"cover_image": null,
"overview": [
{"type": "paragraph", "value": "<p>Ready.</p>"}
],
"detail": [],
"latest_revision_id": 6543,
"live": true,
"status": "live",
"preview_url": "/admin/pages/987/view_draft/",
"edit_url": "/admin/pages/987/edit/",
"api_url": "/api/editor/posts/987/",
"published_revision_id": 6543,
"public_url": "/blog/weeknotes-2026-25/"
}
Publishing a live page that has unpublished draft changes publishes the latest
draft revision. Publishing a page that is already live with no unpublished draft
returns 409 Conflict:
{
"code": "no_unpublished_draft",
"detail": "This post is already live and has no unpublished draft revision."
}
Permission failures use the standard permission_denied envelope. Missing
posts use the standard not_found envelope.
Episode endpoints
Podcast episodes have dedicated draft create/read/update endpoints that mirror
the post endpoints. Episode is a Post subclass, so the body
(overview/detail), tags, categories, cover_image,
visible_date, slug, revision/base_revision_id conflict detection, the
draft-only publish guard, and the structured error envelopes all behave
exactly as documented for posts above. The endpoints are:
POST /api/editor/episodes/
GET /api/editor/episodes/{id}/
PATCH /api/editor/episodes/{id}/
POST /api/editor/episodes/{id}/publish/
GET /api/editor/episodes/{id}/preview/
The parent must be a cast.Podcast; a cast.Blog or any other parent is
rejected with a validation_error on parent. These endpoints serve
episodes only — a non-episode page id returns the not_found envelope.
Create and update stay draft-only and reject publish: true like posts;
publishing happens through the explicit episode publish action below.
Beyond the shared post fields, episodes accept and return these episode-specific fields:
podcast_audio(optional):{"id": <audio id>}referencing acast.Audiothe caller may choose, ornullonPATCHto clear it. Missing or inaccessible audio collapses to the neutralnot_foundshape atpodcast_audio.id. It is optional on a draft but required to publish (see the publish action below).episode_number(optional): positive integer, ornullonPATCHto clear it.episode_type(optional): one offull,trailer,bonus, or an empty string (which omits the feed tag, equivalent tofull).season(optional):{"id": <season id>}referencing acast.Seasonthat must belong to the parent podcast, ornullonPATCHto clear it. A foreign-podcast season is avalidation_erroronseason.keywords(optional): iTunes keyword string.explicit(optional): one of1(yes),2(no),3(clean); defaults to1.block(optional): boolean; blocks the episode from iTunes.
Example create request:
{
"parent": {"id": 7},
"title": "Episode 12",
"slug": "episode-12",
"tags": ["interview"],
"overview": [
{"type": "heading", "value": "Show notes"}
],
"podcast_audio": {"id": 321},
"episode_number": 12,
"episode_type": "full",
"season": {"id": 3},
"explicit": 1,
"block": false
}
The response is the standard editor draft shape (id, type of
cast.Episode, the shared post fields, preview_url/edit_url, and an
api_url of /api/editor/episodes/{id}/) plus the episode-specific fields
above. PATCH requires base_revision_id, preserves omitted fields and
sections, and keeps the empty-update guard, exactly like posts; parent is
immutable.
The episode publish action mirrors the post publish action:
POST /api/editor/episodes/{id}/publish/
It publishes the latest draft revision through Wagtail’s revision publishing
path, requires Wagtail admin access plus publish permission for the page, takes
no request body, and returns the editor episode shape plus published_revision_id
and public_url. Publishing an episode that is already live with no
unpublished draft returns the no_unpublished_draft conflict, just like posts.
Publishing requires a non-null podcast_audio (the same rule the Wagtail admin
enforces). A publish request for an episode without podcast_audio is rejected
with a validation_error envelope (required at podcast_audio) and the
episode stays unpublished. Because an Episode is a Post,
POST /api/editor/posts/{id}/publish/ also enforces this gate when its id
resolves to an episode, so the audio requirement cannot be bypassed through the
post publish endpoint.
The episode preview endpoint mirrors post preview: it returns the latest
editable episode revision as full themed text/html on success, uses the
editor JSON error envelopes on failure, requires edit permission, and requires
no token scope. Passing a plain post id to the episode preview endpoint returns
the standard not_found envelope.
Error envelopes
Validation errors (400 Bad Request):
{
"code": "validation_error",
"errors": {
"title": [{"code": "required", "message": "This field is required."}],
"overview.3.value.1.id": [
{"code": "not_found", "message": "Image 789 does not exist."}
]
}
}
Field paths in errors follow dot notation into the request body so that
clients can locate and repair individual fields without guessing.
Permission errors (403 Forbidden):
{"code": "permission_denied", "detail": "…"}
Permission denials tied to a parent page, such as create-under-page checks, may
also include parent_id:
{"code": "permission_denied", "detail": "…", "parent_id": 123}
Missing posts (404 Not Found):
{"code": "not_found", "detail": "Post not found."}
Whole-request editor media failures use flat error bodies. Current media
upload codes include no_upload_collection (403), post_save_permission_denied
(403), probe_timeout (422), probe_failed (422), rate_limited (429), and
cleanup_failed (500). Form and field problems use the validation envelope;
non_field_errors is the reserved key for request-wide validation errors.
Editor media endpoints
Editor media endpoints use the shared pagination envelope and accept only their
documented query parameters. Unsupported parameters, such as tags= instead
of repeatable tag=, return validation_error with
unsupported_parameter.
List selectable media:
GET /api/editor/media/images/?q=desk&tag=weeknotes
GET /api/editor/media/audios/?q=episode&tag=weeknotes
GET /api/editor/media/videos/?q=demo&tag=weeknotes
The list endpoints return only objects the caller may choose. q is a
deterministic database filter: image and video titles are searched with
icontains; audio title and subtitle are searched with icontains.
Repeatable tag filters match exact stored tag names and are ANDed. Results
are ordered newest first (images by created_at, audio/video by created),
then by descending ID. Media file fields use relative URLs; pagination links
may be absolute. Relative media URLs assume the application serves or proxies
media from the same origin; deployments whose storage requires absolute signed
CDN URLs should provide a same-origin media path for editor API consumers.
Image result shape:
{
"id": 456,
"type": "wagtailimages.Image",
"title": "Desk photo",
"file": "/media/original_images/desk.jpg",
"width": 1600,
"height": 900,
"collection": {"id": 1, "name": "Root"},
"tags": ["weeknotes"],
"edit_url": "/admin/images/456/"
}
Audio result shape:
{
"id": 42,
"type": "cast.Audio",
"title": "Episode audio",
"subtitle": "Interview mix",
"transcript_diarization_mode": "inherit",
"file_formats": "m4a mp3",
"mp3": "/media/cast_audio/episode.mp3",
"m4a": "/media/cast_audio/episode.m4a",
"oga": null,
"opus": null,
"collection": {"id": 1, "name": "Root"},
"tags": ["weeknotes"],
"edit_url": "/admin/castaudio/edit/42/"
}
Video result shape:
{
"id": 43,
"type": "cast.Video",
"title": "Demo clip",
"original": "/media/cast_videos/demo.mp4",
"poster": "/media/cast_videos/poster/demo.jpg",
"collection": {"id": 1, "name": "Root"},
"tags": ["weeknotes"],
"edit_url": "/admin/castvideo/edit/43/"
}
Upload media:
POST /api/editor/media/images/
POST /api/editor/media/audios/
POST /api/editor/media/videos/
Content-Type: multipart/form-data
Image uploads accept title, file, tags, and optional collection.
Audio uploads accept title, subtitle, transcript_diarization_mode,
one of m4a/mp3/oga/opus, tags, chaptermarks, and
optional collection. transcript_diarization_mode=enabled is rejected in
this slice; use inherit or disabled. Video uploads accept title,
original, optional poster, tags, and optional collection.
If collection is omitted and exactly one usable upload collection exists,
that collection is selected. If none exists, upload returns
no_upload_collection. If more than one exists, upload returns a validation
error on collection with code ambiguous. A supplied missing or unusable
collection returns collection_permission_denied on collection; malformed
collection values return invalid.
Audio and video uploads share a one-in-flight lock per authenticated user; a
second concurrent audio/video upload returns rate_limited. The lock uses
the default Django cache and requires a shared cache backend, such as Redis or
Memcached, to be process-wide in multi-worker deployments; Django’s
LocMemCache only protects a single process. Configure
CAST_EDITOR_MEDIA_UPLOAD_LOCK_SECONDS when deployments need a longer or
shorter owner-token TTL. Editor audio duration probing and optional chapter
extraction run inside a cumulative request-path budget controlled by
CAST_EDITOR_MEDIA_PROBE_SECONDS. Required audio probing timeout returns
probe_timeout and the failed object/files are cleaned up. Required audio
probing failures such as invalid ffprobe output or an unavailable ffprobe
binary return probe_failed and are also cleaned up. Optional chapter
extraction failures are logged and save the audio without extracted chapter
marks; malformed individual chapter entries are skipped while valid entries are
kept. Video poster generation is optional display enrichment; timeout or
ffmpeg/ffprobe failure during poster generation does not fail the upload, and
upload can succeed with poster: null.
Discover upload collections:
GET /api/editor/media/collections/?type=image
GET /api/editor/media/collections/?type=audio
GET /api/editor/media/collections/?type=video
The type parameter is required and must be one of image, audio, or
video. Responses use the same pagination envelope. Items include id,
name, display-only breadcrumb, and ancestors from root to parent.
Discovery returns candidate upload targets; custom permission policies can
still make a saved audio/video object fail the defensive post-save choose check.
Draft-only and Wagtail permissions
Create and update requests are draft-only. Create requests save Wagtail draft
revisions, so newly-created pages are returned with live: false and do not
appear in the public Wagtail pages API until explicitly published. Updating an
already-live page creates an unpublished draft revision and returns
status: "draft" while live stays true. Use
POST /api/editor/posts/{id}/publish/ to publish the latest draft revision.
Authorization uses standard Wagtail page permissions:
GET /api/editor/parents/— lists pages where the caller has add-child permission.POST /api/editor/posts/— requires add-child permission on the selected parent.GET /api/editor/posts/{id}/— requires edit permission for the page.GET /api/editor/posts/{id}/preview/— requires edit permission for the page and returns rendered HTML on success.PATCH /api/editor/posts/{id}/— requires edit permission for the page.POST /api/editor/posts/{id}/publish/— requires edit and publish permission for the page.POST /api/editor/episodes/— requires add-child permission on the selectedPodcastparent.GET /api/editor/episodes/{id}/— requires edit permission for the episode.GET /api/editor/episodes/{id}/preview/— requires edit permission for the episode and returns rendered HTML on success.PATCH /api/editor/episodes/{id}/— requires edit permission for the episode.POST /api/editor/episodes/{id}/publish/— requires edit and publish permission for the episode, and a non-nullpodcast_audio.
Pagination¶
List endpoints support pagination:
Default page size: 40
Maximum page size: 200
Query parameters:
page,pageSize
Example:
GET /api/videos/?page=2&pageSize=20
Response format:
{
"count": 100,
"next": "http://example.com/api/videos/?page=3",
"previous": "http://example.com/api/videos/?page=1",
"results": [...]
}
File Uploads¶
The file upload endpoint accepts multipart/form-data:
const formData = new FormData();
formData.append('original', fileInput.files[0]);
fetch('/api/upload_video/', {
method: 'POST',
body: formData,
credentials: 'include' // Include session cookie
});
Unsupported media extensions, content types, container signatures, or files
over the configured upload size limit are rejected with 400 Bad Request and
field-level JSON errors. Audio uploads are handled by the Wagtail admin and
chooser views rather than a public DRF upload endpoint.
Error Handling¶
The API returns standard HTTP status codes:
200 OK: Success201 Created: Resource created400 Bad Request: Invalid request data401 Unauthorized: Authentication required403 Forbidden: Permission denied404 Not Found: Resource not found500 Internal Server Error: Server error
Error responses include a detail message:
{
"detail": "Authentication credentials were not provided."
}
Using the API¶
JavaScript Example¶
Fetching posts with facets:
async function fetchPosts(categorySlug, page = 1) {
const response = await fetch(
`/api/wagtail/pages/?type=cast.Post&child_of=4&use_post_filter=true&category_facets=${categorySlug}&page=${page}`,
{ credentials: 'include' }
);
return await response.json();
}
Python Client Example¶
Using the API from Python with httpx:
import httpx
# Create client for session persistence
with httpx.Client() as client:
# Request modal facet counts for a specific blog
response = client.get(
"https://example.com/api/facet_counts/4/",
params={
"mode": "modal",
"search": "python",
"tag_facets": "django",
},
)
facet_data = response.json()
Headless CMS Usage¶
Django Cast can function as a headless CMS by:
Using the Wagtail Pages API to fetch content
Implementing a frontend application (React, Vue, etc.)
Optionally using theme packages like
cast-vue
Example Vue.js integration:
// Fetch blog posts
const posts = await fetch('/api/wagtail/pages/?type=cast.Post')
.then(r => r.json());
// Get facet counts for filtering
const facets = await fetch('/api/facet_counts/1/')
.then(r => r.json());
Performance Considerations¶
Use field limiting to reduce payload size:
?fields=title,slug,dateImplement client-side caching for static content
Use pagination for large result sets
Facet counts are optimized at the repository level
Security Notes¶
All media endpoints filter by authenticated user
CSRF protection is enabled for state-changing operations
Media uploads are validated for type and size before metadata extraction
Images API includes null byte protection
Comment Moderation¶
Export training data for spam filter:
Returns comment data for training the Naive Bayes spam classifier.
This endpoint is restricted to staff users. Anonymous and authenticated non-staff users receive
403 Forbidden.