API reference · v1

Documentation for the complete download flow.

Authentication, catalog discovery, exact-quality tickets, metadata, streaming, resume behavior and controlled errors—in one place.

Introduction

MovieMini API

A CM and OrangePlay exact-quality download API backed by the existing MovieMini catalog. Search movies and TV shows, issue a short-lived ticket for the exact quality and provider you need, then stream or resume bytes directly. Provider URLs and credentials never leave the server.

MovieMini API runs at api.moviemini.cc. The gateway owns login, public discovery, documentation, health, ticket, and download routes. Caddy routes the remaining /api/* catalog calls and /img/* poster requests to the existing MovieMini backend service.

The gateway resolves the requested quality from the read-only catalog, mints a just-in-time provider URL, and proxies the bytes—so callers never see the upstream provider URL or any signing credential.

Account access only. Accounts are admin-generated; public registration is unavailable. Sign in with POST /api/auth/login to obtain a session JWT, then pass it as Authorization: Bearer <token> on every subsequent request.

API surface:

GET  /
GET  /openapi.json
GET  /health/live
GET  /health/ready

POST /api/auth/login          (MovieMini API gateway)
GET  /api/search              (existing MovieMini backend)
GET  /api/:type/:id           (existing MovieMini backend)
GET  /v1/catalog              (movie/TV discovery)
GET  /v1/catalog/:type/:id    (authenticated provider detail)

POST /v1/media/movie/:id/download-ticket
POST /v1/media/tv/:id/download-ticket
HEAD /v1/downloads/:ticket
GET  /v1/downloads/:ticket
Getting started

Base URL & versioning

All requests go to the same host. Catalog and authentication endpoints live under /api; discovery, ticket issuance, and downloads are versioned under /v1.

https://api.moviemini.cc

The OpenAPI machine-readable description is available at /openapi.json.

Path prefixHandled by
/api/auth/loginMovieMini API gateway
/api/*Existing MovieMini backend (proxied, except login; registration blocked)
/img/*Existing MovieMini backend (proxied)
/v1/*MovieMini API gateway
/, /openapi.json, /health/*MovieMini API gateway
Security

Authentication

Provider details and ticket endpoints require a valid MovieMini session JWT supplied as a Bearer token. Public catalog cards do not require login. Ticket URLs themselves act as short-lived bearer credentials for download endpoints—no additional header is needed when fetching them.

Sign in with an existing MovieMini account:

POST /api/auth/login

The response contains a signed HS256 JWT. Include it in the Authorization header on every catalog and ticket request:

Authorization: Bearer <your-session-jwt>
No public registration. Accounts are admin-generated. There is no self-service sign-up through this API.

When the gateway issues a download ticket it returns a download_url. That URL contains the ticket credential in the path segment and does not require an Authorization header—it is the bearer credential. Treat it with the same care as a password: it is short-lived, media-specific, and carries your byte budget.

Ticket expiry. The expires_in value (seconds) in the ticket response tells you how long the URL remains valid. Issue a fresh ticket if you have not started the download before it expires.
Browsing & search

Catalog browsing & search

Browse downloadable movies and TV shows publicly, then authenticate to retrieve CM and OrangePlay source details.

GET /v1/catalog

The /v1/catalog endpoint provides movie and TV discovery directly through the gateway. Use type (movie or tv), q, page, and limit query parameters. The narrow public response contains only title IDs, names, years, cached poster paths, media types, and quality labels.

GET /v1/catalog/{type}/{id}

Send a Bearer token to retrieve provider-grouped CM and OrangePlay choices. Provider URLs and credentials are never returned.

GET /api/movies/popular
GET /api/movies/latest
GET /api/movies/top-rated
GET /api/tv/popular
GET /api/search?q=…&type=movie
GET /api/{type}/{id}

type is movie or tv. The detail response includes the numeric id you pass to the ticket endpoint. Request only a quality listed for that title by the catalog.

Example search:

curl -s "https://api.moviemini.cc/api/search?q=dune&type=movie"   -H "Authorization: Bearer <your-session-jwt>"
const res = await fetch(
  'https://api.moviemini.cc/api/search?q=dune&type=movie',
  { headers: { Authorization: 'Bearer <your-session-jwt>' } }
);
const data = await res.json();
import requests
r = requests.get(
    'https://api.moviemini.cc/api/search',
    params={'q': 'dune', 'type': 'movie'},
    headers={'Authorization': 'Bearer <your-session-jwt>'},
)
data = r.json()
Ticket issuance

Movie download ticket

Issue a short-lived download URL for a specific movie at an exact quality. The ticket carries a bounded byte budget and expires automatically.

POST /v1/media/movie/{id}/download-ticket

Path parameter

NameTypeDescription
idintegerNumeric movie ID from the catalog

Request body (JSON)

FieldTypeRequiredDescription
qualitystringYesExact quality string: 480p, 720p, 1080p, or 4k
providerstringYesDownload provider: cm or orangeplay
Both fields are required. Exact quality strings are 480p, 720p, 1080p, and 4k. Provider must be explicitly cm or orangeplay; there is no default. Any other value returns a 404 error. Check the catalog response to confirm which qualities and providers are available for a given title before issuing a ticket.

Successful response — 201 Created

{
  "download_url": "https://download.api.moviemini.cc/v1/downloads/<ticket>",
  "expires_in": 60
}

expires_in is the number of seconds until the ticket becomes invalid. Begin the download before it expires.

curl -s -X POST   "https://api.moviemini.cc/v1/media/movie/12345/download-ticket"   -H "Authorization: Bearer <your-session-jwt>"   -H "Content-Type: application/json"   -d '{"quality":"1080p","provider":"cm"}'
const res = await fetch(
  'https://api.moviemini.cc/v1/media/movie/12345/download-ticket',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer <your-session-jwt>',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ quality: '1080p', provider: 'cm' }),
  }
);
const { download_url, expires_in } = await res.json();
import requests
r = requests.post(
    'https://api.moviemini.cc/v1/media/movie/12345/download-ticket',
    headers={'Authorization': 'Bearer <your-session-jwt>'},
    json={'quality': '1080p', 'provider': 'cm'},
)
ticket = r.json()  # { download_url, expires_in }
Ticket issuance

TV episode download ticket

Issue a short-lived download URL for a specific TV episode. Season and episode numbers are mandatory non-negative integers.

POST /v1/media/tv/{id}/download-ticket

Path parameter

NameTypeDescription
idintegerNumeric TV show ID from the catalog

Request body (JSON)

FieldTypeRequiredDescription
qualitystringYesExact quality string: 480p, 720p, 1080p, or 4k
providerstringYesDownload provider: cm or orangeplay
seasonNumberintegerYesSeason number (≥ 0)
episodeNumberintegerYesEpisode number within the season (≥ 0)
All four fields are required for TV. Omitting provider, seasonNumber, or episodeNumber causes the gateway to treat the value as invalid and return a 404. Provider must be explicitly cm or orangeplay.

Successful response — 201 Created

{
  "download_url": "https://download.api.moviemini.cc/v1/downloads/<ticket>",
  "expires_in": 60
}
curl -s -X POST   "https://api.moviemini.cc/v1/media/tv/7890/download-ticket"   -H "Authorization: Bearer <your-session-jwt>"   -H "Content-Type: application/json"   -d '{"quality":"720p","provider":"orangeplay","seasonNumber":1,"episodeNumber":3}'
const res = await fetch(
  'https://api.moviemini.cc/v1/media/tv/7890/download-ticket',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer <your-session-jwt>',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ quality: '720p', provider: 'orangeplay', seasonNumber: 1, episodeNumber: 3 }),
  }
);
const { download_url, expires_in } = await res.json();
import requests
r = requests.post(
    'https://api.moviemini.cc/v1/media/tv/7890/download-ticket',
    headers={'Authorization': 'Bearer <your-session-jwt>'},
    json={'quality': '720p', 'provider': 'orangeplay', 'seasonNumber': 1, 'episodeNumber': 3},
)
ticket = r.json()
Downloads

HEAD — read download metadata

Send a HEAD request to a ticket URL to read the content type, exact byte size, and filename before committing to a full download. No bytes are transferred and the ticket is not consumed.

HEAD /v1/downloads/{ticket}

No request body or Authorization header is needed—the ticket in the URL path is the credential.

Response headers on success (200)

HeaderDescription
Content-LengthExact file size in bytes
Content-TypeMIME type (e.g. video/mp4)
Content-DispositionSuggested filename as attachment; filename="…"
Accept-RangesAlways bytes—ranged requests are supported
curl -I "https://download.api.moviemini.cc/v1/downloads/<ticket>"
Use HEAD to obtain the total file size before computing byte ranges for a resumable download.
Downloads

GET — full download

Retrieve the full media file. Once the first GET claims a ticket, it enters a short active session that can serve bounded retries or ranges until its active deadline or byte budget is reached.

GET /v1/downloads/{ticket}

No request body or Authorization header is needed.

If the file exceeds the server's per-request byte limit, the gateway returns 416 Range Not Satisfiable with the message "Full object exceeds request byte limit; request a byte range". In that case use the Range header to download in chunks (see Single-byte ranges & resume).

On success the gateway responds with 200 OK and streams bytes directly. Response headers include Content-Length, Content-Type, Content-Disposition, and Accept-Ranges: bytes.

curl -L -o movie.mp4   "https://download.api.moviemini.cc/v1/downloads/<ticket>"
const res = await fetch(
  'https://download.api.moviemini.cc/v1/downloads/<ticket>'
);
// res.body is a ReadableStream
const buffer = await res.arrayBuffer();
// or pipe res.body to a writable stream
import requests
with requests.get(
    'https://download.api.moviemini.cc/v1/downloads/<ticket>',
    stream=True,
) as r:
    with open('movie.mp4', 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024 * 1024):
            f.write(chunk)
Downloads

Single-byte ranges & resume

The gateway supports the HTTP Range header for resumable downloads and chunked transfer. Only a single byte range per request is accepted; multi-range requests are rejected.

Append a Range header to any GET request:

Range: bytes=<start>-<end>
Range: bytes=<start>-
Range: bytes=-<suffix-length>

On a valid range the gateway responds with 206 Partial Content and sets the Content-Range response header:

HTTP/1.1 206 Partial Content
Content-Range: bytes <start>-<end>/<total>
Content-Length: <chunk-size>
One range only. Multi-range syntax such as bytes=0-499,1000-1499 is not supported. The server returns 416 Range Not Satisfiable with code RANGE_NOT_SATISFIABLE for malformed or multi-range values.

Resume pattern

If a download is interrupted, resume from the last successfully received byte while the ticket remains in its active window. Issue a fresh ticket if that window has expired:

# Interrupted after receiving 52428800 bytes of a 1073741824-byte file:
curl -L   -H "Range: bytes=52428800-"   -o movie_part.mp4   "https://download.api.moviemini.cc/v1/downloads/<ticket>"

A ticket may serve more than one range during its active session, but every request is constrained by the ticket byte budget and per-user concurrency limit. Do not assume it is a permanent URL.

Reference

Response headers

Every response from the MovieMini API gateway includes a common set of security and diagnostic headers. Download responses additionally carry content-description headers.

Common headers — all responses

HeaderValue / purpose
X-Request-IdUUID identifying this request for support and logging
Cache-Controlprivate, no-store — responses must not be cached
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policyno-referrer
Content-Security-Policydefault-src 'none'; …

CORS headers — cross-origin requests only

HeaderValue
Access-Control-Allow-OriginEchoed from Origin when the origin is on the allow-list
Access-Control-Allow-MethodsGET,HEAD,POST,OPTIONS
Access-Control-Allow-HeadersAuthorization,Content-Type,Range
Access-Control-Expose-HeadersAccept-Ranges,Content-Length,Content-Range,Content-Disposition,X-Request-Id
VaryOrigin
Origins not on the allow-list receive 403 ORIGIN_DENIED. CORS preflight (OPTIONS) is handled automatically with a 204 response.

Download-specific headers (HEAD and GET)

HeaderDescription
Accept-Rangesbytes — always present on download responses
Content-LengthByte count for this response (chunk size for 206, full size for 200)
Content-TypeMIME type of the media file (e.g. video/mp4)
Content-Dispositionattachment; filename="…" — safe, sanitised filename
Content-RangePresent only on 206 responses: bytes start-end/total
Reference

Error model & status codes

All error responses share the same JSON envelope. The request_id field matches the X-Request-Id response header and should be included in support requests.

Error envelope

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description.",
    "request_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }
}

Status codes and error codes

HTTP statuscodeCause
401AUTH_REQUIREDNo Authorization header provided
401INVALID_SESSIONJWT is invalid, expired, or signed with the wrong secret
403ORIGIN_DENIEDCross-origin request from an origin not on the allow-list
404MEDIA_UNAVAILABLETitle ID not found, quality not available, or media type invalid
404NOT_FOUNDEndpoint path does not exist
410TICKET_EXPIREDTicket is invalid, expired, or outside its active window
429DOWNLOAD_LIMITPer-user or global concurrent-download limit reached
429DAILY_QUOTA_EXCEEDEDFree daily movie or episode quota reached
429TICKET_BYTE_BUDGETCommitted and in-flight bytes exceed this ticket's budget
416RANGE_NOT_SATISFIABLEMalformed range, multi-range, range out of bounds, or full file exceeds per-request limit
503PROVIDER_UNAVAILABLEUpstream media provider is temporarily unreachable
500INTERNAL_ERRORUnexpected server error
Ticket-specific rejection codes (expired ticket, quota exceeded, already claimed, etc.) are returned as 4xx responses with a descriptive code field and a human-readable message.
Reference

Quotas & limits

The gateway enforces per-request byte limits, ticket-level byte reservations, daily quotas, and concurrent-download limits. Large files may need ranged requests.

If a full-file GET would exceed the server's configured maxRequestBytes limit, the request is rejected with 416 Range Not Satisfiable and the message:

Full object exceeds request byte limit; request a byte range

In this case, use HEAD to obtain the total file size, then download in chunks using the Range header while the ticket remains active.

Similarly, if a Range request would exceed maxRequestBytes, it is rejected with 416 Range Not Satisfiable and the message:

Range exceeds request byte limit

Key constraints

ConstraintBehaviour
Ticket reuseA claimed ticket may serve bounded retries or ranges during its short active session.
Ticket expiryTickets expire after a short window (see expires_in). Expired tickets are rejected.
Range countExactly one byte range per request. Multi-range is rejected.
Quality valuesMust be exactly 480p, 720p, 1080p, or 4k.
Request bodyJSON request body is limited to 16 KiB.
Security guidance

Security guidance

Follow these practices to keep your integration secure. Ticket URLs are bearer credentials—they grant access to a specific media file without any additional authentication.

Ticket URLs are bearer credentials. Anyone with a ticket URL can start the download within the expiry window. Do not log, share, or embed them in client-side code that can be cached or inspected.

Session JWTs

  • Store your session JWT securely (e.g. an environment variable or a secrets manager). Never commit it to source control or expose it in client-side code.
  • The JWT is HS256-signed with the MovieMini session secret. It is verified server-side on every ticket request.
  • Keep tokens out of URLs and logs, and follow the existing MovieMini account recovery process if one is exposed.

Ticket URLs

  • Issue tickets immediately before starting a download; do not cache or pre-generate them in bulk.
  • A claimed ticket remains usable only for its short active session and bounded byte budget. An expired ticket is rejected with a 4xx error.
  • Never log the full download_url—log only the X-Request-Id for tracing.

Provider URL privacy

The gateway deliberately never exposes upstream provider URLs or signing credentials in any response. All provider resolution and byte forwarding happen server-side. This is by design and will not change.

CORS

Cross-origin requests are restricted to an explicit allow-list. If your origin is not on the list, the gateway returns 403 ORIGIN_DENIED. Contact the platform team to add an approved origin.

End-to-end

End-to-end integration example

A complete walkthrough: sign in, search the catalog, issue a ticket, read metadata, and stream the file in chunks.

The example below downloads a 1080p movie in a single ranged chunk after checking the file size with HEAD. Replace the placeholder values with real credentials and IDs from your account.

# 1. Start with the JWT returned by the existing MovieMini login
TOKEN="<your-session-jwt>"

# 2. Search the catalog
curl -s "https://api.moviemini.cc/api/search?q=dune&type=movie"   -H "Authorization: Bearer $TOKEN"

# 3. Issue a movie ticket (replace 12345 with the real ID; set provider to cm or orangeplay)
TICKET_URL=$(curl -s -X POST   "https://api.moviemini.cc/v1/media/movie/12345/download-ticket"   -H "Authorization: Bearer $TOKEN"   -H "Content-Type: application/json"   -d '{"quality":"1080p","provider":"cm"}' | jq -r '.download_url')

# 4. Read metadata
curl -I "$TICKET_URL"

# 5. Download; ranged retries can reuse the ticket while it remains active
curl -L -o movie.mp4 "$TICKET_URL"
const BASE = 'https://api.moviemini.cc';

// 1. Use the JWT returned by the existing MovieMini login
const token = '<your-session-jwt>';
const auth = { Authorization: `Bearer ${token}` };

// 2. Search, inspect the response, and choose a movie ID
const searchRes = await fetch(`${BASE}/api/search?q=dune&type=movie`, { headers: auth });
const searchResults = await searchRes.json();
const movieId = 12345;

// 3. Issue ticket — provider must be 'cm' or 'orangeplay'
const ticketRes = await fetch(`${BASE}/v1/media/movie/${movieId}/download-ticket`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({ quality: '1080p', provider: 'cm' }),
});
const { download_url } = await ticketRes.json();

// 4. HEAD — get file size
const headRes = await fetch(download_url, { method: 'HEAD' });
const totalBytes = Number(headRes.headers.get('content-length'));

// 5. Download one range; retry while the ticket remains active
const dlRes = await fetch(download_url, {
  headers: { Range: `bytes=0-${totalBytes - 1}` },
});
// dlRes.body is a ReadableStream — pipe to a file writer
import requests

BASE = 'https://api.moviemini.cc'

# 1. Use the JWT returned by the existing MovieMini login
token = '<your-session-jwt>'
auth = {'Authorization': f'Bearer {token}'}

# 2. Search, inspect the response, and choose a movie ID
search_results = requests.get(
    f'{BASE}/api/search', params={'q': 'dune', 'type': 'movie'}, headers=auth
).json()
movie_id = 12345

# 3. Issue ticket — provider must be 'cm' or 'orangeplay'
ticket = requests.post(
    f'{BASE}/v1/media/movie/{movie_id}/download-ticket',
    headers=auth,
    json={'quality': '1080p', 'provider': 'cm'},
).json()
download_url = ticket['download_url']

# 4. HEAD — get file size
head = requests.head(download_url)
total = int(head.headers['Content-Length'])

# 5. Download in a single range; retry while the ticket remains active
with requests.get(
    download_url,
    headers={'Range': f'bytes=0-{total - 1}'},
    stream=True,
) as r:
    r.raise_for_status()
    with open('movie.mp4', 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024 * 1024):
            f.write(chunk)
TV episode pattern: Replace step 3 with a POST to /v1/media/tv/{id}/download-ticket and include provider, seasonNumber, and episodeNumber in the JSON body alongside quality. Everything else is identical.
Account access

Login to download.

Use the email and password issued by MovieMini. Public signup is not available.