2026-09-04 17:34:03 -05:00
# 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
2026-09-06 06:04:59 -05:00
`false` to hide docs in production — section 32). This same document is
also published as a browsable page at `/wiki/api/` (see `mkdocs.yml` ).
2026-09-04 17:34:03 -05:00
All responses are JSON, built from Pydantic response schemas
(`app/schemas/` ), never from serialized ORM objects (section 18). Errors use
a consistent envelope:
```json
{ "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` ). |
2026-09-06 06:15:39 -05:00
## Accounts
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | `/api/v1/auth/register` | `{"email", "password", "display_name"?}` | Creates an account and logs it in — returns an access token immediately, same as `/login` . `password` is 8-128 characters (length only; NIST 800-63B recommends against complexity rules). 409 if the email is already registered (matched case-insensitively). |
| POST | `/api/v1/auth/login` | `{"email", "password"}` | Returns `{"access_token", "token_type": "bearer"}` . A nonexistent email and a wrong password both return the identical 401 `invalid_credentials` — distinguishing them would let a client enumerate registered emails, including via response timing (see `app/core/auth.py:burn_time_like_a_password_check` ). |
| GET | `/api/v1/auth/me` | — | Requires `Authorization: Bearer <access_token>` . Returns the caller's own `{"id", "email", "display_name", "created_at"}` . |
Access tokens are JWTs signed with `AURORA_SECRET_KEY` , valid for 24 hours,
with no refresh-token flow yet — a client re-authenticates (logs in again)
2026-09-06 10:47:41 -05:00
once a token expires.
## Ratings and watchlist
All of these require `Authorization: Bearer <access_token>` and are scoped
to the caller — there is no way to read or modify another user's ratings
or watchlist. `{aurora_id}` is a movie or series id.
| Method | Path | Body | Notes |
|---|---|---|---|
| PUT | `/api/v1/movies/{aurora_id}/rating` | `{"value"}` | Rates a movie, 0.5-10.0. Upsert — rating the same movie again replaces the previous value rather than erroring. Returns `{"value", "created_at", "updated_at"}` . 404 if the movie doesn't exist, 422 if `value` is out of range. |
| DELETE | `/api/v1/movies/{aurora_id}/rating` | — | Removes the caller's rating, if any. 204. |
| PUT / DELETE | `/api/v1/series/{aurora_id}/rating` | `{"value"}` | Same as above, for a series. |
| POST | `/api/v1/movies/{aurora_id}/watchlist` | — | Adds a movie to the caller's watchlist. Idempotent — adding an already-watchlisted movie is a no-op, not a 409. 204. |
| DELETE | `/api/v1/movies/{aurora_id}/watchlist` | — | Removes it, if present. 204. |
| POST / DELETE | `/api/v1/series/{aurora_id}/watchlist` | — | Same as above, for a series. |
| GET | `/api/v1/account/ratings/movies` | — | Paginated list of the caller's rated movies. Each result is a normal movie summary plus `"rating"` . |
| GET | `/api/v1/account/ratings/series` | — | Same, for series. |
| GET | `/api/v1/account/watchlist/movies` | — | Paginated list of the caller's watchlisted movies (movie summaries, no rating field). |
| GET | `/api/v1/account/watchlist/series` | — | Same, for series. |
2026-09-06 06:15:39 -05:00
2026-09-04 17:34:03 -05:00
## Search
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/search?q=...&limit=10` | Searches movies, series, people, collections, companies. `limit` (1-50) caps results **per type** . |
```json
GET /api/v 1 /search?q=the% 20 matrix
{
"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 |
|---|---|
2026-09-05 09:01:11 -05:00
| GET | `/api/v1/movies` |
2026-09-04 17:34:03 -05:00
| 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` |
2026-09-05 08:16:18 -05:00
| GET | `/api/v1/movies/{aurora_id}/recommendations` |
2026-09-06 04:59:02 -05:00
| GET | `/api/v1/movies/{aurora_id}/watch-providers` |
2026-09-06 04:45:09 -05:00
| GET | `/api/v1/movies/now-playing` |
| GET | `/api/v1/movies/upcoming` |
2026-09-06 05:18:02 -05:00
| GET | `/api/v1/movies/trending` |
2026-09-04 17:34:03 -05:00
2026-09-05 09:01:11 -05:00
`GET /api/v1/movies` browses/discovers without a search query — the
complement to `/search` (section 17, additive beyond the spec's minimum
2026-09-06 04:40:32 -05:00
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:
2026-09-05 09:01:11 -05:00
```json
{
"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 } ]
}
```
2026-09-04 17:34:03 -05:00
`GET /api/v1/movies/{aurora_id}` (cached, see `docs/architecture.md#caching` ):
```json
{
"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" }],
2026-09-06 04:40:32 -05:00
"alternate_titles" : [{ "title" : "Matrix" , "country_code" : "US" , "type" : null }],
2026-09-04 17:34:03 -05:00
"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" }],
2026-09-05 20:08:26 -05:00
"collection" : { "id" : "aur_collection_..." , "name" : "The Matrix Collection" , "poster_path" : "https://..." },
2026-09-04 17:34:03 -05:00
"images" : { "posters" : [ ... ], "backdrops" : [ ... ], "logos" : [ ... ] },
"videos" : [{ "id" : "aur_vid_..." , "type" : "trailer" , "site" : "YouTube" , "url" : "https://www.youtube.com/watch?v=..." , "is_official" : true }],
2026-09-06 04:07:18 -05:00
"certification" : "R" ,
"release_dates" : [
{ "country_code" : "US" , "release_date" : "1999-03-31" , "release_type" : "theatrical" , "certification" : { "system" : "MPA" , "rating" : "R" }, "note" : null }
],
2026-09-04 17:34:03 -05:00
"external_ids" : { "tmdb" : "603" , "imdb" : "tt0133093" },
"created_at" : "..." ,
"updated_at" : "..."
}
```
2026-09-06 04:07:18 -05:00
`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"` .
2026-09-06 04:40:32 -05:00
`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.
2026-09-05 08:16:18 -05:00
`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:
```json
{
"id" : "aur_movie_01m1q7p1c..." ,
"recommendations" : [
{ "id" : "aur_movie_..." , "type" : "movie" , "title" : "The Matrix Reloaded" , "release_date" : "2003-05-15" , "poster_path" : "https://..." }
]
}
```
2026-09-06 04:59:02 -05:00
`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:
```json
{
"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.
2026-09-06 05:18:02 -05:00
`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.
2026-09-06 04:45:09 -05:00
`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.
2026-09-04 17:34:03 -05:00
## Series / Seasons / Episodes
| Method | Path |
|---|---|
2026-09-05 09:01:11 -05:00
| GET | `/api/v1/series` |
2026-09-04 17:34:03 -05:00
| GET | `/api/v1/series/{aurora_id}` |
| GET | `/api/v1/series/{aurora_id}/seasons` |
2026-09-05 09:52:08 -05:00
| GET | `/api/v1/series/{aurora_id}/credits` |
| GET | `/api/v1/series/{aurora_id}/images` |
| GET | `/api/v1/series/{aurora_id}/videos` |
2026-09-06 04:59:02 -05:00
| GET | `/api/v1/series/{aurora_id}/watch-providers` |
2026-09-06 05:18:02 -05:00
| GET | `/api/v1/series/trending` |
2026-09-04 17:34:03 -05:00
| GET | `/api/v1/seasons/{aurora_id}` |
| GET | `/api/v1/episodes/{aurora_id}` |
2026-09-05 09:01:11 -05:00
`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.
2026-09-04 17:34:03 -05:00
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
2026-09-05 09:52:08 -05:00
"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.
2026-09-04 17:34:03 -05:00
## People
| Method | Path |
|---|---|
2026-09-05 10:32:16 -05:00
| GET | `/api/v1/people` |
2026-09-04 17:34:03 -05:00
| GET | `/api/v1/people/{aurora_id}` |
2026-09-05 09:19:52 -05:00
| 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) |
2026-09-04 17:34:03 -05:00
2026-09-05 10:32:16 -05:00
`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` .
2026-09-04 17:34:03 -05:00
## Collections / Companies
| Method | Path |
|---|---|
2026-09-05 20:08:26 -05:00
| GET | `/api/v1/collections` |
2026-09-04 17:34:03 -05:00
| GET | `/api/v1/collections/{aurora_id}` |
2026-09-05 19:08:14 -05:00
| GET | `/api/v1/companies` |
2026-09-04 17:34:03 -05:00
| GET | `/api/v1/companies/{aurora_id}` |
2026-09-05 19:08:14 -05:00
`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.
2026-09-05 20:08:26 -05:00
`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
2026-09-06 00:05:10 -05:00
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.
2026-09-05 20:08:26 -05:00
2026-09-06 04:40:32 -05:00
## 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.
2026-09-05 08:37:27 -05:00
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.
2026-09-05 09:34:31 -05:00
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.
2026-09-04 17:34:03 -05:00
## Administrative (section 17: "separately protected")
Require header `X-API-Key: <one of AURORA_ADMIN_API_KEYS>` . Never exposed to
2026-09-05 20:49:55 -05:00
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.
2026-09-04 17:34:03 -05:00
| 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. |
2026-09-05 02:03:25 -05:00
| 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). |
2026-09-04 19:43:56 -05:00
| 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. |
2026-09-05 08:37:27 -05:00
| 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. |
2026-09-04 17:34:03 -05:00
| GET | `/api/v1/admin/ingest/jobs/{job_id}` | — | Poll job status: `pending` → `running` → `succeeded` /`failed` /`retrying` . |
2026-09-06 05:18:02 -05:00
| 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. |
2026-09-04 17:34:03 -05:00
## 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
2026-09-05 09:01:11 -05:00
- `GET /movies` and `GET /series` (browse/discover) paginate with
`page` /`page_size` , offset-based rather than the Aurora ID cursor scheme
`docs/database.md#aurora-ids-vs-primary-keys` describes, since sorting by
popularity/rating/date doesn't line up with creation-order cursors. Other
list-shaped responses (recommendations, a collection's `movies` , a
person's `credits` ) 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.