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.
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/:ticketBase 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 prefix | Handled by |
|---|---|
/api/auth/login | MovieMini 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 |
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:
/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>
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.
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.
Catalog browsing & search
Browse downloadable movies and TV shows publicly, then authenticate to retrieve CM and OrangePlay source details.
/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.
/v1/catalog/{type}/{id}Send a Bearer token to retrieve provider-grouped CM and OrangePlay choices. Provider URLs and credentials are never returned.
/api/movies/popular/api/movies/latest/api/movies/top-rated/api/tv/popular/api/search?q=…&type=movie/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()
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.
/v1/media/movie/{id}/download-ticketPath parameter
| Name | Type | Description |
|---|---|---|
id | integer | Numeric movie ID from the catalog |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
quality | string | Yes | Exact quality string: 480p, 720p, 1080p, or 4k |
provider | string | Yes | Download provider: cm or orangeplay |
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 }
TV episode download ticket
Issue a short-lived download URL for a specific TV episode. Season and episode numbers are mandatory non-negative integers.
/v1/media/tv/{id}/download-ticketPath parameter
| Name | Type | Description |
|---|---|---|
id | integer | Numeric TV show ID from the catalog |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
quality | string | Yes | Exact quality string: 480p, 720p, 1080p, or 4k |
provider | string | Yes | Download provider: cm or orangeplay |
seasonNumber | integer | Yes | Season number (≥ 0) |
episodeNumber | integer | Yes | Episode number within the season (≥ 0) |
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()
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.
/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)
| Header | Description |
|---|---|
Content-Length | Exact file size in bytes |
Content-Type | MIME type (e.g. video/mp4) |
Content-Disposition | Suggested filename as attachment; filename="…" |
Accept-Ranges | Always bytes—ranged requests are supported |
curl -I "https://download.api.moviemini.cc/v1/downloads/<ticket>"
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.
/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)
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>
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.
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
| Header | Value / purpose |
|---|---|
X-Request-Id | UUID identifying this request for support and logging |
Cache-Control | private, no-store — responses must not be cached |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | no-referrer |
Content-Security-Policy | default-src 'none'; … |
CORS headers — cross-origin requests only
| Header | Value |
|---|---|
Access-Control-Allow-Origin | Echoed from Origin when the origin is on the allow-list |
Access-Control-Allow-Methods | GET,HEAD,POST,OPTIONS |
Access-Control-Allow-Headers | Authorization,Content-Type,Range |
Access-Control-Expose-Headers | Accept-Ranges,Content-Length,Content-Range,Content-Disposition,X-Request-Id |
Vary | Origin |
403 ORIGIN_DENIED. CORS
preflight (OPTIONS) is handled automatically with a
204 response.
Download-specific headers (HEAD and GET)
| Header | Description |
|---|---|
Accept-Ranges | bytes — always present on download responses |
Content-Length | Byte count for this response (chunk size for 206, full size for 200) |
Content-Type | MIME type of the media file (e.g. video/mp4) |
Content-Disposition | attachment; filename="…" — safe, sanitised filename |
Content-Range | Present only on 206 responses: bytes start-end/total |
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 status | code | Cause |
|---|---|---|
| 401 | AUTH_REQUIRED | No Authorization header provided |
| 401 | INVALID_SESSION | JWT is invalid, expired, or signed with the wrong secret |
| 403 | ORIGIN_DENIED | Cross-origin request from an origin not on the allow-list |
| 404 | MEDIA_UNAVAILABLE | Title ID not found, quality not available, or media type invalid |
| 404 | NOT_FOUND | Endpoint path does not exist |
| 410 | TICKET_EXPIRED | Ticket is invalid, expired, or outside its active window |
| 429 | DOWNLOAD_LIMIT | Per-user or global concurrent-download limit reached |
| 429 | DAILY_QUOTA_EXCEEDED | Free daily movie or episode quota reached |
| 429 | TICKET_BYTE_BUDGET | Committed and in-flight bytes exceed this ticket's budget |
| 416 | RANGE_NOT_SATISFIABLE | Malformed range, multi-range, range out of bounds, or full file exceeds per-request limit |
| 503 | PROVIDER_UNAVAILABLE | Upstream media provider is temporarily unreachable |
| 500 | INTERNAL_ERROR | Unexpected server error |
code field and a human-readable message.
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
| Constraint | Behaviour |
|---|---|
| Ticket reuse | A claimed ticket may serve bounded retries or ranges during its short active session. |
| Ticket expiry | Tickets expire after a short window (see expires_in). Expired tickets are rejected. |
| Range count | Exactly one byte range per request. Multi-range is rejected. |
| Quality values | Must be exactly 480p, 720p, 1080p, or 4k. |
| Request body | JSON request body is limited to 16 KiB. |
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.
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 theX-Request-Idfor 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 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)
/v1/media/tv/{id}/download-ticket and include
provider, seasonNumber, and episodeNumber
in the JSON body alongside quality. Everything else is identical.