Builds docs/ into a static site via mkdocs + mkdocs-material in its own Docker build stage (docs-builder), so mkdocs itself never ends up in the runtime image -- only the rendered HTML does, mounted at /wiki alongside the existing /docs OpenAPI schema (unconditionally, unlike /docs, since this is curated content meant to be public regardless of AURORA_API_DOCS_ENABLED). docs/aurora-core-spec.md (the internal build spec, referenced by "section N" comments throughout the codebase) is excluded from the built site entirely via exclude_docs, not just left out of the nav -- it was never meant for public consumption. Co-Authored-By: Claude Sonnet 5 <[email protected]>
16 KiB
Aurora Core — API
Base path: /api/v1. Interactive OpenAPI docs are available at /docs in
any environment where AURORA_API_DOCS_ENABLED=true (default; set to
false to hide docs in production — section 32). This same document is
also published as a browsable page at /wiki/api/ (see mkdocs.yml).
All responses are JSON, built from Pydantic response schemas
(app/schemas/), never from serialized ORM objects (section 18). Errors use
a consistent envelope:
{ "error": { "code": "movie_not_found", "message": "No such movie: aur_movie_..." } }
Health
| Method | Path | Notes |
|---|---|---|
| GET | /health and /api/v1/health |
Liveness only. |
| GET | /ready and /api/v1/ready |
Checks PostgreSQL (always) and Redis (if REDIS_REQUIRED=true). |
Search
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/search?q=...&limit=10 |
Searches movies, series, people, collections, companies. limit (1-50) caps results per type. |
GET /api/v1/search?q=the%20matrix
{
"query": "the matrix",
"movies": [ { "id": "aur_movie_...", "type": "movie", "title": "The Matrix", "release_date": "1999-03-30", "poster_path": "https://..." } ],
"series": [],
"people": [],
"collections": [ { "id": "aur_collection_...", "name": "The Matrix Collection" } ],
"companies": [],
"total_results": 2
}
Movies
| Method | Path |
|---|---|
| GET | /api/v1/movies |
| GET | /api/v1/movies/{aurora_id} |
| GET | /api/v1/movies/{aurora_id}/credits |
| GET | /api/v1/movies/{aurora_id}/images |
| GET | /api/v1/movies/{aurora_id}/videos |
| GET | /api/v1/movies/{aurora_id}/recommendations |
| GET | /api/v1/movies/{aurora_id}/watch-providers |
| GET | /api/v1/movies/now-playing |
| GET | /api/v1/movies/upcoming |
| GET | /api/v1/movies/trending |
GET /api/v1/movies browses/discovers without a search query — the
complement to /search (section 17, additive beyond the spec's minimum
endpoint list). Query params: genre (an Aurora genre id), keyword (an
Aurora keyword id), year (primary release year), status (e.g.
released), sort_by (popularity default | release_date |
vote_average | title), sort_dir (desc default | asc), page
(default 1), page_size (default 20, max 50). An invalid filter value (a
malformed genre/keyword id, an unknown status) is a 422, not a
silently-empty result. Response:
{
"page": 1,
"page_size": 20,
"total_results": 142,
"total_pages": 8,
"results": [ { "id": "aur_movie_...", "type": "movie", "title": "The Matrix", "release_date": "1999-03-30", "poster_path": "https://...", "popularity": 45.5, "vote_average": 8.2 } ]
}
GET /api/v1/movies/{aurora_id} (cached, see docs/architecture.md#caching):
{
"id": "aur_movie_01m1q7p1c...",
"type": "movie",
"title": "The Matrix",
"original_title": "The Matrix",
"overview": "...",
"tagline": "Welcome to the Real World",
"status": "released",
"original_language": "en",
"release_date": "1999-03-30",
"runtime": 136,
"budget": 63000000,
"revenue": 463517383,
"popularity": 45.5,
"vote_average": 8.2,
"vote_count": 24000,
"adult": false,
"genres": [{ "id": "aur_genre_...", "name": "Action" }],
"keywords": [{ "id": "aur_keyword_...", "name": "dystopia" }],
"alternate_titles": [{ "title": "Matrix", "country_code": "US", "type": null }],
"cast": [{ "id": "aur_credit_...", "person": { "id": "aur_person_...", "name": "Keanu Reeves", "profile_path": null }, "character": "Neo", "billing_order": 0 }],
"crew": [{ "id": "aur_credit_...", "person": { "id": "aur_person_...", "name": "Lana Wachowski" }, "department": "Directing", "job": "Director" }],
"companies": [{ "id": "aur_company_...", "name": "Village Roadshow Pictures", "company_type": "production_company" }],
"collection": { "id": "aur_collection_...", "name": "The Matrix Collection", "poster_path": "https://..." },
"images": { "posters": [...], "backdrops": [...], "logos": [...] },
"videos": [{ "id": "aur_vid_...", "type": "trailer", "site": "YouTube", "url": "https://www.youtube.com/watch?v=...", "is_official": true }],
"certification": "R",
"release_dates": [
{ "country_code": "US", "release_date": "1999-03-31", "release_type": "theatrical", "certification": { "system": "MPA", "rating": "R" }, "note": null }
],
"external_ids": { "tmdb": "603", "imdb": "tt0133093" },
"created_at": "...",
"updated_at": "..."
}
certification is a convenience pick — the US (MPA) rating, TMDb's own default badge — out of the full release_dates list, which carries every country/type/rating TMDb reports (release_type one of premiere | theatrical | digital | physical | tv, collapsed from TMDb's six numeric codes). A rating body name (system, e.g. "MPA"/"BBFC") is only known for a handful of major markets — everywhere else falls back to the country code itself rather than fabricating a body TMDb never named. Series detail carries the same two fields, sourced from TMDb's content_ratings instead — since that endpoint gives no date at all, each rating is anchored to the show's first_air_date with release_type: "tv".
alternate_titles ("Also Known As") carries every translated/regional title TMDb tracks, each with the country it applies to (country_code, nullable — TMDb sometimes omits it) and an optional type (e.g. "working title"). Series detail carries the same field.
GET /api/v1/movies/{aurora_id}/recommendations returns TMDb's ranked
recommendations, but only the ones already ingested into Aurora — a
recommendation pointing at a movie Aurora hasn't seen yet is silently
dropped rather than fabricated as a stub (section 15), so this list can be
sparse (or empty) for a young dataset and fills in as more movies get
ingested:
{
"id": "aur_movie_01m1q7p1c...",
"recommendations": [
{ "id": "aur_movie_...", "type": "movie", "title": "The Matrix Reloaded", "release_date": "2003-05-15", "poster_path": "https://..." }
]
}
watch_providers (embedded on movie/series detail as shown above, or its own
GET .../watch-providers) is grouped by country then by monetization type —
mirroring TMDb's own watch/providers response shape directly, since that's
the shape a client porting from TMDb already expects:
{
"id": "aur_movie_01m1q7p1c...",
"results": {
"US": {
"link": "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=US",
"flatrate": [{ "id": "aur_watch_provider_...", "name": "Netflix", "logo_path": "https://...", "display_priority": 0 }],
"rent": [],
"buy": [],
"ads": [],
"free": []
}
}
}
This data comes from JustWatch via TMDb, and carries its own attribution requirement separate from TMDb's general one — TMDb's terms require sourcing watch-provider data back to JustWatch specifically if you display it, not just crediting TMDb itself. Read TMDb's current terms for the exact required wording/logo before showing this in the frontend.
GET /api/v1/movies/trending mirrors TMDb's own /trending/movie/{window}
ranking rather than Aurora computing trending itself from popularity deltas
-- that would require re-syncing the entire catalog on a schedule, which
isn't feasible at any real scale, whereas TMDb already computes this well
from signals (real view/search velocity) Aurora has no access to. window
(day default | week), page, page_size — same response shape as
GET /movies. Refreshed by a worker cron job twice a day (00:00 and
12:00 UTC-ish — see WorkerSettings.cron_jobs in app/workers/worker.py);
POST /api/v1/admin/refresh-trending (admin-protected) triggers the same
refresh on demand, useful right after a fresh deploy so this isn't empty
until the next scheduled tick. Unlike recommendations' never-fabricate-
a-stub rule, a refresh actually ingests each trending item — there are only
~20 per window (TMDb's first page), cheap enough to fully ingest rather
than drop anything Aurora hasn't seen yet. GET /series/trending is the
same idea for series.
GET /api/v1/movies/now-playing and GET /api/v1/movies/upcoming use the
release_dates data above rather than release_date/year: region
(ISO 3166-1 alpha-2, default US), page, page_size — same
MovieListOut response shape as GET /movies, no genre/keyword/sort_by.
"Now playing" is movies with a release_type: "theatrical" date in
region within roughly the last month; "upcoming" is the same but in the
future, soonest first. A movie can carry more than one theatrical-type row
in the same region (a limited release followed by a wide release both map
to "theatrical") — these collapse to one entry per movie (latest date for
now-playing, soonest for upcoming) rather than appearing twice.
Series / Seasons / Episodes
| Method | Path |
|---|---|
| GET | /api/v1/series |
| GET | /api/v1/series/{aurora_id} |
| GET | /api/v1/series/{aurora_id}/seasons |
| GET | /api/v1/series/{aurora_id}/credits |
| GET | /api/v1/series/{aurora_id}/images |
| GET | /api/v1/series/{aurora_id}/videos |
| GET | /api/v1/series/{aurora_id}/watch-providers |
| GET | /api/v1/series/trending |
| GET | /api/v1/seasons/{aurora_id} |
| GET | /api/v1/episodes/{aurora_id} |
GET /api/v1/series is the series equivalent of GET /api/v1/movies
above, with sort_by values popularity (default) | first_air_date |
vote_average | title (release_date/year become first_air_date/
year filtered against it) — otherwise the same params, response shape,
and validation behavior.
Series detail includes networks and companies as separate lists (a TV
network and a production company are both Company rows distinguished by
company_type — see docs/database.md), plus creators (series-level
"Creator" credits) alongside cast/crew. /credits, /images, and
/videos are the same sub-resources broken out on their own, mirroring the
movie endpoints above (parity that was missing until now) — /credits
carries the same cast/crew/creators three-way split as series detail.
People
| Method | Path |
|---|---|
| GET | /api/v1/people |
| GET | /api/v1/people/{aurora_id} |
| GET | /api/v1/people/{aurora_id}/credits (movie and series filmography; guest-star episode credits are aggregated to one row per series rather than one per episode, but carry both episode_count and the individual episodes breakdown, sorted by season/episode number) |
GET /api/v1/people is the browse/discover equivalent for people. No
genre/year/status here — known_for_department (exact match, e.g.
Acting, Directing, Writing) is the one categorical filter that
actually means something for a cast/crew list, and sort_by is just
popularity (default) or name. Otherwise the same page/page_size
pagination and response shape as /movies and /series.
Collections / Companies
| Method | Path |
|---|---|
| GET | /api/v1/collections |
| GET | /api/v1/collections/{aurora_id} |
| GET | /api/v1/companies |
| GET | /api/v1/companies/{aurora_id} |
GET /api/v1/companies is the browse/discover equivalent for companies:
company_type (production_company | network | studio |
distributor) and country (ISO 3166-1 alpha-2) filters, page/
page_size pagination. No sort_by — companies have no popularity or
date field to sort by, so results are always alphabetical by name.
GET /api/v1/collections is the same idea for collections/franchises, but
with no filter at all (no categorical field like company_type exists for
a collection) — just page/page_size, always alphabetical by name.
Genres
| Method | Path |
|---|---|
| GET | /api/v1/genres |
A directory for the genre filter GET /movies and GET /series already
accept, which takes an Aurora genre id — this is how a client discovers
what those ids actually are, since nothing else exposes them on their own
(they otherwise only ever appear embedded in a movie/series response's own
genres list). No filter, no sort_by — always alphabetical by name, same
as collections. page_size defaults to 50 (max 200) rather than the 20/50
used elsewhere, since genres are a small, fixed reference set (TMDb has
~19-20 total) that a client typically wants in full for a filter dropdown,
not paginated the way a catalog of millions of movies needs to be.
Keywords
| Method | Path |
|---|---|
| GET | /api/v1/keywords |
A directory for the keyword filter GET /movies and GET /series accept,
same idea as GET /genres above. No filter, no sort_by — always
alphabetical by name. Unlike genres, keywords are not a small fixed set (a
full TMDb catalog has tens of thousands) — page_size still defaults to 50
(max 200) for consistency with genres, but a client browsing keywords
should expect to paginate, not fetch the whole directory at once.
Collection detail's movies list only includes movies already ingested
into Aurora — POST /api/v1/admin/ingest/collection links a franchise's
member movies purely by external-id lookup, the same never-fabricate-a-stub
rule recommendations follows, so a franchise fills in as more of its
movies get ingested rather than all at once.
Company detail also carries movies and series lists — everything that
company is credited on (any role: production, distribution, network),
ordered by release/first-air date. No never-fabricate-a-stub caveat here:
a MovieCompany/SeriesCompany row is only ever created as a side effect
of actually ingesting that movie or series, so there's no separate
"company's known filmography" TMDb ever needs linking after the fact.
Administrative (section 17: "separately protected")
Require header X-API-Key: <one of AURORA_ADMIN_API_KEYS>. Never exposed to
public clients — add a rule in the host webserver's vhost config for
api.amdbs.net (e.g. an Apache <Location>/LiteSpeed rewrite blocking
/api/v1/admin/* from outside a trusted IP range) in production if you
want network-level isolation on top of the API key.
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /api/v1/admin/ingest/movie |
{"tmdb_id": "603"} |
Enqueues an arq job; returns {"job_id": "...", "status": "enqueued"} immediately (section 15: ingestion never runs inline in a request). |
| POST | /api/v1/admin/ingest/person |
{"tmdb_id": "6384"} |
Same, for a person profile. |
| POST | /api/v1/admin/ingest/series |
{"tmdb_id": "1396", "include_seasons": false} |
Ingests the series and season stubs (number, title, air date, poster) — no episodes yet. With include_seasons: true, also enqueues an ingest/season job for every season once the series finishes (opt-in: some shows have dozens of seasons, each a separate TMDb call). |
| POST | /api/v1/admin/ingest/season |
{"tmdb_id": "1396", "season_number": 1} |
Ingests one season's full episode list. Requires the series to already be ingested (ingest/series first) — fails with a clear error otherwise. |
| POST | /api/v1/admin/ingest/collection |
{"tmdb_id": "2344"} |
Ingests a franchise's own metadata (overview, poster/backdrop) and links its already-ingested member movies (see "Collections / Companies" above). Movie ingestion also lazily creates a bare name-only stub the first time it sees a belongs_to_collection reference — this is what actually fills it in. |
| GET | /api/v1/admin/ingest/jobs/{job_id} |
— | Poll job status: pending → running → succeeded/failed/retrying. |
| POST | /api/v1/admin/refresh-trending |
— | Manually triggers the same refresh the twice-daily cron job runs (see "Trending" above). Not tracked via ingest/jobs — check GET /movies/trending/GET /series/trending directly instead of polling a job id. |
Errors
| Status | When |
|---|---|
| 404 | Entity not found, or the ID is well-formed but the wrong entity type (aur_person_... passed to /movies/{id}) |
| 422 | Request validation failure (e.g. missing/invalid query params) |
| 429 | Rate limit exceeded (Retry-After header set) |
| 401 | Missing/invalid X-API-Key on an admin endpoint |
| 503 | Admin API called with AURORA_ADMIN_API_KEYS unset, or the job queue is unavailable |
Known limitations
GET /moviesandGET /series(browse/discover) paginate withpage/page_size, offset-based rather than the Aurora ID cursor schemedocs/database.md#aurora-ids-vs-primary-keysdescribes, since sorting by popularity/rating/date doesn't line up with creation-order cursors. Other list-shaped responses (recommendations, a collection'smovies, a person'scredits) still return everything with no pagination at all — fine at today's scale, but the natural next thing to revisit if any of them grow large.