# TTVE API — full reference Version 1.0.0. Base URL: `https://ttve.ir/api/v1`. TTVE is a video hosting platform: you upload a source file, it is transcoded into an HLS ladder, and you embed a player or read watch analytics. This document describes the whole public API in one fetch. The same content is available as [OpenAPI 3.1](https://ttve.ir/api/v1/openapi.json). ## Authentication Every request carries a workspace API key as a bearer token: ``` Authorization: Bearer ttve_live_... ``` Mint a key in the dashboard under Settings → API keys. It is displayed once and stored only as a hash, so a lost key is replaced, not recovered. A key is bound to exactly one workspace. There is no workspace or tenant parameter anywhere in this API, and a key cannot read, write, or even confirm the existence of anything in another workspace — a request for another workspace's resource returns 404, not 403. Keys belong on a server. This API deliberately sends no CORS headers: an API that advertised itself as browser-callable would invite integrators to ship their keys to the client, where every visitor can read them. Keys cannot mint keys. There is no endpoint here that creates or revokes a credential, so a leaked read-only key cannot escalate itself. ## Scopes A key carries a subset of these. A request whose scope is not granted returns 403 naming the missing scope. - `videos:read` — خواندن ویدئوها - `videos:write` — ویرایش و حذف ویدئوها - `uploads:write` — بارگذاری ویدئوی جدید - `collections:read` — خواندن مجموعه‌ها - `collections:write` — ویرایش مجموعه‌ها - `subtitles:write` — مدیریت زیرنویس‌ها - `analytics:read` — خواندن آمار ## Response shape Success bodies always carry `"ok": true` alongside the payload: ```json { "ok": true, "items": [], "total": 0, "page": 1, "pageSize": 12, "pageCount": 1 } ``` Failures always carry `"ok": false` and an error object: ```json { "ok": false, "error": { "code": "quota_exceeded", "message": "The workspace has no storage left on its current plan.", "messageFa": "...", "docs": "https://ttve.ir/developers#error-quota_exceeded" } } ``` Branch on `code`, never on message text — `message` is English for you and `messageFa` is Persian and safe to show an end user, and both may be reworded. Two conventions worth knowing before you write a parser: - Timestamps are ISO-8601 strings in UTC, never epoch numbers. - Byte counts are decimal **strings**, not numbers, because they can exceed what a JSON number survives. Parse them as big integers if you do arithmetic. ## Errors | code | HTTP | meaning | | --- | --- | --- | | `unauthenticated` | 401 | Missing or invalid API key. Send it as: Authorization: Bearer ttve_live_… | | `forbidden` | 403 | This API key is not allowed to perform that action. | | `validation` | 400 | The request body or query string failed validation. | | `not_found` | 404 | No such resource in this workspace. | | `conflict` | 409 | That change conflicts with the resource's current state. | | `not_ready` | 409 | The video is still processing and cannot be published yet. | | `too_large` | 413 | The upload exceeds the maximum allowed size. | | `quota_exceeded` | 413 | The workspace has no storage left on its current plan. | | `internal` | 500 | Something went wrong on our side. Try again. | ## Rate limits Limits are per key, per minute. Every response reports the current state: - `X-RateLimit-Limit` — requests allowed in the window - `X-RateLimit-Remaining` — requests left - `X-RateLimit-Reset` — unix seconds when the window resets A 429 also sets `Retry-After` in seconds. Honour it; retrying sooner just burns the next window too. ## Uploading a video The bytes do not travel through this API — you get a one-shot URL and PUT to it directly. Three steps: **1. Reserve the slot.** This checks the workspace's remaining storage against the size you declare, creates the video record, and returns a URL. ```bash curl -X POST https://ttve.ir/api/v1/uploads \ -H "Authorization: Bearer ttve_live_..." \ -H "Content-Type: application/json" \ -d '{"title":"Episode 1","filename":"ep1.mp4","contentType":"video/mp4","sizeBytes":184320000}' ``` The `uploadUrl` you get back is issued by TTVE, **not** by object storage. It is a sealed capability bound to that exact storage key, content type, and byte size, and it expires. Even when the deployment stores objects in S3, the bytes are proxied — so do not expect an S3 presigned URL and do not try to sign your own. **2. PUT the whole file** to `uploadUrl`, sending the `headers` from step 1 verbatim. One request; there is no multipart or resumable form in v1. **3. Complete.** This verifies the stored byte count and the file's magic bytes, then queues transcoding. ```bash curl -X POST https://ttve.ir/api/v1/uploads/{videoId}/complete \ -H "Authorization: Bearer ttve_live_..." ``` Step 3 is idempotent — a retry after a timeout will not queue the job twice. Then poll `GET /api/v1/videos/{id}` until `status` is `READY` (or `FAILED`, with a `failureReason`). Processing time scales with duration and resolution. A video cannot be made `PUBLIC` before it is `READY`. Accepted source types: `video/mp4`, `video/quicktime`, `video/webm`, `video/x-matroska`. If `title` or `description` holds non-ASCII text (Persian, Arabic, emoji, …) and it comes back from the API as `??????`, that happened before the request left your machine, not on this end — this API is Unicode end-to-end. Some shells (Git Bash and `cmd` on Windows, in particular) mangle non-ASCII bytes in an inline `-d`/`--data` argument before curl even sends it. Write the JSON body to a file and send it with `--data-binary @file.json` instead. ## Playing a video `GET /api/v1/videos/{id}/playback` returns a signed HLS master playlist plus the quality ladder and subtitle tracks. Every URL in that response expires — if the media host answers 403, fetch the playback manifest again rather than retrying the URL. For embedding, use `embedUrl` in an iframe. That path renders the full TTVE player, including whatever branding, call-to-action, and lead-capture form the workspace has configured; the API response deliberately does not include that configuration. ## Endpoints ### `GET /api/v1/me` Describe the calling key. Scope: `(any key)`. Returns the workspace this key belongs to, the scopes it was granted, the plan limits, and current usage. Call this first to confirm a key works and to discover what it may do. ### `GET /api/v1/usage` Storage, bandwidth, and view usage against plan limits. Scope: `analytics:read`. ### `GET /api/v1/videos` List videos in the workspace. Scope: `videos:read`. Paginated and filterable. Soft-deleted videos are never returned. ### `GET /api/v1/videos/{id}` Fetch one video with its assets. Scope: `videos:read`. Includes rendition ladder, thumbnails, subtitles, and collection membership. Signed asset URLs in the response are short-lived and must not be cached. Path parameters: - `id` — The video UUID. ### `PATCH /api/v1/videos/{id}` Update a video's metadata or visibility. Scope: `videos:write`. Only the fields you send are changed. Visibility cannot be raised until the video reaches status READY. Path parameters: - `id` — The video UUID. ### `DELETE /api/v1/videos/{id}` Delete a video. Scope: `videos:write`. Removes the video from every collection, releases its storage quota, and deletes the stored objects. This is not reversible through the API. Path parameters: - `id` — The video UUID. ### `GET /api/v1/videos/{id}/playback` Signed playback manifest for a video. Scope: `videos:read`. Returns the HLS master playlist URL, the quality ladder, and subtitle tracks. Every URL is signed and expires; on a 403 from the media host, call this again. Path parameters: - `id` — The video UUID. ### `PUT /api/v1/videos/{id}/collections` Replace a video's collection membership. Scope: `collections:write`. Path parameters: - `id` — The video UUID. ### `GET /api/v1/videos/{id}/subtitles` List a video's subtitle tracks. Scope: `videos:read`. Path parameters: - `id` — The video UUID. ### `POST /api/v1/videos/{id}/subtitles` Upload a subtitle track. Scope: `subtitles:write`. Send multipart/form-data. WebVTT and SubRip are accepted; SubRip is converted to WebVTT. Path parameters: - `id` — The video UUID. Form fields (multipart/form-data): - `file` (required) — A .vtt or .srt file, at most 512 KB. - `language` (required) — BCP-47 tag, e.g. fa or en. - `label` (required) — Human-readable track name. - `isDefault` — Make this the default track. ### `PATCH /api/v1/videos/{id}/subtitles/{subtitleId}` Rename or re-tag a subtitle track. Scope: `subtitles:write`. Path parameters: - `id` — The video UUID. - `subtitleId` — The subtitle UUID. ### `DELETE /api/v1/videos/{id}/subtitles/{subtitleId}` Delete a subtitle track. Scope: `subtitles:write`. Path parameters: - `id` — The video UUID. - `subtitleId` — The subtitle UUID. ### `GET /api/v1/videos/{id}/thumbnails` List a video's thumbnails. Scope: `videos:read`. Path parameters: - `id` — The video UUID. ### `POST /api/v1/videos/{id}/thumbnails` Choose which thumbnail represents the video. Scope: `videos:write`. Selects one of the frames the processing pipeline generated. Uploading a custom image is dashboard-only in v1. Path parameters: - `id` — The video UUID. ### `POST /api/v1/uploads` Start an upload and get a one-shot URL. Scope: `uploads:write`. Step 1 of 3. Reserves quota, creates the video record, and returns a short-lived URL. PUT the entire file to that URL, then call the complete endpoint. The URL is issued by TTVE, not by object storage, and is bound to this exact file size and content type. ### `POST /api/v1/uploads/{id}/complete` Finish an upload and queue processing. Scope: `uploads:write`. Step 3 of 3. Verifies the stored byte count and the file's magic bytes, then queues transcoding. Safe to retry: calling it twice does not queue the job twice. Path parameters: - `id` — The video UUID returned by the upload step. ### `GET /api/v1/collections` List collections. Scope: `collections:read`. ### `POST /api/v1/collections` Create a collection. Scope: `collections:write`. ### `GET /api/v1/collections/{id}` Fetch one collection with its videos in order. Scope: `collections:read`. Path parameters: - `id` — The resource UUID. ### `PATCH /api/v1/collections/{id}` Rename a collection or change its description. Scope: `collections:write`. Path parameters: - `id` — The resource UUID. ### `DELETE /api/v1/collections/{id}` Delete a collection. Scope: `collections:write`. The videos inside are unlinked, not deleted. Path parameters: - `id` — The resource UUID. ### `POST /api/v1/collections/{id}/videos` Add a video to a collection. Scope: `collections:write`. Path parameters: - `id` — The resource UUID. ### `DELETE /api/v1/collections/{id}/videos` Remove a video from a collection. Scope: `collections:write`. Path parameters: - `id` — The resource UUID. ### `PUT /api/v1/collections/{id}/order` Reorder the videos in a collection. Scope: `collections:write`. Send every video id currently in the collection. A partial list is rejected. Path parameters: - `id` — The resource UUID. ### `GET /api/v1/analytics/videos/{id}` Watch analytics for one video. Scope: `analytics:read`. Path parameters: - `id` — The video UUID. ### `GET /api/v1/analytics/workspace` Watch analytics across the workspace. Scope: `analytics:read`. ## MCP TTVE also speaks the Model Context Protocol over Streamable HTTP at `https://ttve.ir/api/mcp`, so an agent can drive the platform without an SDK. - Authenticate the same way: `Authorization: Bearer ttve_live_...` - Supported protocol revisions: `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26` - POST only; GET and DELETE return 405 - `tools/list` returns only the tools the key's scopes permit Tools available: - `get_workspace_info` — Identify the TTVE workspace this connection acts on, along with its plan limits, current storage/bandwidth usage, and the permissions granted. Call this first when you need to know what you are allowed to do. - `get_workspace_usage` — Report how much storage, bandwidth, and monthly views the workspace has consumed against its plan limits. - `list_videos` — List videos in the TTVE workspace, with optional title search and filters for processing status and visibility. Returns one page at a time; use `page` to continue. - `get_video` — Fetch full detail for one TTVE video: status, duration, dimensions, tags, renditions, thumbnails, subtitles, collections, and its embed URL. - `update_video` — Change a TTVE video's title, description, tags, or visibility. Send only the fields you want to change. Publishing (visibility PUBLIC) fails until processing finishes. - `delete_video` — Permanently delete a TTVE video and its stored files. Not reversible — confirm with the user before calling. - `get_playback_info` — Get signed HLS playback URLs for a TTVE video, plus its quality ladder, subtitle tracks, and iframe embed URL. - `create_upload` — Start a TTVE video upload. Returns a one-shot URL to PUT the file to, and the video id. This tool does NOT transfer the file: after calling it, PUT the raw bytes to `uploadUrl` with the returned headers, then call complete_upload with the same video id. - `complete_upload` — Finish a TTVE upload after the file has been PUT, and queue it for processing. Poll get_video until status is READY. - `list_collections` — List the collections (playlists) in the TTVE workspace. - `create_collection` — Create a new TTVE collection (playlist) to group videos. - `get_collection` — Fetch one TTVE collection and the videos it contains, in playback order. - `add_video_to_collection` — Add a TTVE video to a collection. Adding a video that is already in it succeeds unchanged. - `get_video_analytics` — Get watch analytics for one TTVE video: views, unique viewers, watch time, completion rate, a daily series, and an engagement curve showing where viewers drop off. - `get_workspace_analytics` — Get workspace-wide watch analytics for TTVE: totals, a daily series, and the top videos by views. Note that `create_upload` does not move bytes. It returns a URL for you to PUT to, exactly as the HTTP flow above describes.