The Video object
A video is a hosted media asset managed by Transcodely. You give it a source — a public URL, a direct upload, or a managed transcoding job — and Transcodely transcodes it into an adaptive rendition ladder, stores the output, and serves it over a CDN with a ready-to-embed player. Each video tracks its own lifecycle, visibility, renditions, storage size, and encoding cost.
Base path: transcodely.v1.VideoService Requires: an app-scoped API key (ak_), or the X-Organization-ID header for portal sessions. The target app must have hosting enabled (AppService.EnableHosting) — calls against an app without hosting fail with failed_precondition.
The fastest way in is Create a video from a URL: one call turns a public URL into a hosted, embeddable video.
Attributes
| Attribute | Type | Description |
|---|---|---|
id | string | Unique identifier. Prefixed with vid_. |
object | string | Resource type discriminator. Always "video". |
app_id | string | Parent app that owns the video. Prefixed with app_. |
source | string | How the video was created. One of upload (direct upload or URL ingest) or job (produced by a managed transcoding job). |
status | string | Current lifecycle status. One of uploading, processing, ready, error, archived, deleted. |
visibility | string | Access control. One of public, unlisted, private. |
title | string | Optional display title. Omitted if unset. |
description | string | Optional description. Omitted if unset. |
tags | array of strings | Free-form tags for categorization (max 20). |
input_size_bytes | integer | Size of the uploaded source file, in bytes. Populated after an upload is verified; absent for URL ingest. |
input_content_type | string | MIME type of the uploaded source file (e.g. video/mp4). |
job_id | string | Transcoding job that produced (or is producing) this video. Prefixed with job_. |
preset | string | Preset ID or slug used to drive the auto-transcode. Omitted when the app’s default auto-profile ladder is used. |
playback_url | string | HLS manifest URL served over the CDN. Computed per request for ready videos and signed + expiring on apps with CDN token auth enabled — see Playback, embeds, and posters. |
embed_url | string | Hosted player page URL, {player_base}/v/{id}. Present only for ready, non-private videos when a player base URL is configured. |
embed_code | string | Ready-to-paste responsive <iframe> snippet (16:9, fluid width). Present under the same conditions as embed_url. |
poster_url | string | Poster image URL. Same signing and per-request rules as playback_url. Absent when the video has no generated poster. |
duration_seconds | number | Media duration in seconds. Populated after processing. |
renditions | array of objects | Encoded renditions available for playback. See The rendition object. |
output_size_bytes | integer | Total size of all stored renditions, in bytes. |
encoding_cost | number | Cost of the transcoding job that produced this video, in EUR. Omitted until the job completes. |
created_at | string | ISO 8601 timestamp. |
updated_at | string | ISO 8601 timestamp. |
ready_at | string | ISO 8601 timestamp of when the video reached ready. Omitted until then. |
{
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "ready",
"visibility": "public",
"title": "Product demo",
"description": "Q3 product walkthrough",
"tags": ["marketing", "demo"],
"input_size_bytes": 132710400,
"input_content_type": "video/mp4",
"job_id": "job_a1b2c3d4e5f6",
"preset": "web_1080p_standard",
"playback_url": "https://cdn.example.com/vid_a1b2c3d4e5f6g7/hls/master.m3u8",
"embed_url": "https://play.transcodely.com/v/vid_a1b2c3d4e5f6g7",
"embed_code": "<div style="position:relative;padding-top:56.25%"><iframe src="https://play.transcodely.com/v/vid_a1b2c3d4e5f6g7" title="Product demo" loading="lazy" style="position:absolute;inset:0;width:100%;height:100%;border:0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div>",
"poster_url": "https://cdn.example.com/vid_a1b2c3d4e5f6g7/hls/poster.jpg",
"duration_seconds": 128.5,
"renditions": [
{ "id": "ren_a1b2c3d4e5f6g7", "resolution": "1080p", "codec": "h264", "bitrate_kbps": 5000, "width": 1920, "height": 1080, "size_bytes": 80530636 },
{ "id": "ren_b2c3d4e5f6g7h8", "resolution": "720p", "codec": "h264", "bitrate_kbps": 2800, "width": 1280, "height": 720, "size_bytes": 45088768 }
],
"output_size_bytes": 125619404,
"encoding_cost": 0.03,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:03:30Z",
"ready_at": "2026-05-24T10:03:30Z"
}Statuses
| Status | Description |
|---|---|
uploading | A presigned upload URL has been issued and the file has not been finalized yet. |
processing | The source (uploaded file or ingested URL) is being probed and transcoded. |
ready | Transcoding finished and the video is available for playback. |
error | Processing failed. The video is not playable. |
archived | Not actively served; storage is retained. |
deleted | Soft-deleted and pending storage cleanup. |
uploading, processing, and archived are non-terminal; ready, error, and deleted are terminal.
Visibility
Visibility controls who can reach a ready video. It defaults to unlisted when omitted, and can be changed any time with Update a video.
| Value | Description |
|---|---|
public | Anyone with the playback or embed URL can watch. |
unlisted | Reachable only via its direct URL; not surfaced in any public index. |
private | Off the public player and embed — the player page returns 404 and embed_url/embed_code are never issued, even after ready. But the owning app’s authenticated API reads (Get/List/Watch) return a signed, expiring playback_url/poster_url once the video is ready, so the owner can play a private video through the API. Because unsigned URLs are never issued for private videos, a private video is only playable once the app has CDN token auth enabled. |
Sources
| Source | Description |
|---|---|
upload | Created via Create a video from a URL, CreateUpload, or CreateMultipartUpload and transcoded automatically. |
job | Created by a managed transcoding job — see ingesting from a storage origin. |
The rendition object
Each rendition is one encoded variant produced by the transcode. A ready video’s renditions array is its adaptive ladder.
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier. Prefixed with ren_. |
resolution | string | Resolution label (e.g. 1080p, 720p). |
codec | string | Video codec (e.g. h264, h265). |
bitrate_kbps | integer | Average bitrate in kilobits per second. |
width | integer | Frame width in pixels. |
height | integer | Frame height in pixels. |
size_bytes | integer | Encoded size of this rendition, in bytes. |
Playback, embeds, and posters
Playback and embed fields are computed per request — never stored on the video. playback_url and poster_url are returned for any ready video on authenticated reads by the owning app, including private ones (see Visibility). embed_url and embed_code are the public surface and are returned only for ready, non-private videos. All four are absent while the video is uploading/processing or in error.
On apps with CDN token auth enabled, playback_url and poster_url are signed and expiring (BunnyCDN HS256 directory tokens, valid ~6 hours by default) and re-issued on every read — always use the value from the latest Get/List/Watch response rather than caching it. On apps not yet migrated to token auth they are unsigned and access control comes from the managed-storage bucket policy; unsigned URLs are never issued for private videos.
| Field | Depends on | What it is |
|---|---|---|
playback_url | ready video + a CDN hostname configured on the app | HLS manifest (.../hls/master.m3u8) served over the CDN. Signed + expiring on token-auth apps; unsigned otherwise (and never issued unsigned for private videos). |
poster_url | ready video that has a generated poster | Poster image URL. Same signing, expiry, and per-request re-issuance as playback_url. Absent when the video has no poster. |
embed_url | ready, non-private video + a player base URL configured | The hosted player page: {player_base}/v/{video_id}. |
embed_code | same as embed_url | A responsive iframe you can paste into any page. |
The embed snippet is a responsive 16:9 wrapper — it scales to the container’s width rather than a fixed size, and the title (the video’s title, or Video player when unset) names the iframe for accessibility:
<div style="position:relative;padding-top:56.25%"><iframe
src="https://play.transcodely.com/v/vid_a1b2c3d4e5f6g7"
title="Product demo" loading="lazy"
style="position:absolute;inset:0;width:100%;height:100%;border:0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen></iframe></div>If you only have the video ID and a player base URL, you can construct the embed URL yourself as {player_base}/v/{video_id} — it is the same value the API returns in embed_url.
Create a video from a URL
POST /transcodely.v1.VideoService/CreateFromUrlThe one-call path: hand Transcodely a publicly-reachable http(s) URL and it creates a hosted video, fetches and transcodes the source, and makes it embeddable — no upload handshake. The video is created directly in processing; the worker downloads the URL at transcode time (the API never fetches it). Playback and embed URLs populate once it reaches ready, which you learn via video.ready or Watch.
Only http:// and https:// URLs are accepted. Storage-origin schemes (gs://, s3://) are rejected here — ingest those from a storage origin instead. Private/internal addresses are rejected up front and again, authoritatively, when the worker fetches (SSRF defense).
Because no bytes are uploaded, no video.uploaded event fires for URL ingest — the video.ready / video.failed events cover the outcome.
| Parameter | Type | Required | Description |
|---|---|---|---|
app_id | string | Yes | App to create the video under (app_ prefix). Must have hosting enabled. |
url | string | Yes | Public http(s) source URL (max 2048 chars). |
title | string | No | Display title (max 255 chars). |
description | string | No | Description (max 2000 chars). |
tags | array | No | Up to 20 tags. |
visibility | string | No | public, unlisted, or private. Defaults to unlisted. |
preset | string | No | Preset ID or slug to drive the transcode. Defaults to the app’s auto-profile ladder. |
Returns
Returns the created Video in processing status. Poll Get, or Watch/subscribe to video.ready, then use its embed_url.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/CreateFromUrl
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"app_id": "app_k1l2m3n4o5",
"url": "https://example.com/source/product-demo.mp4",
"title": "Product demo",
"visibility": "public"
}'{
"video": {
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "processing",
"visibility": "public",
"title": "Product demo",
"tags": [],
"output_size_bytes": 0,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:00:00Z"
}
}SDK support: typed one-call helpers for URL ingest are on the way. Until the SDK releases publish, call the
CreateFromUrlendpoint directly with any Connect/JSON client (as above). TheGetandWatchfollow-ups below are already available in the SDKs.
Wait for the video to become playable, then embed it:
// Poll until ready, then use the embed snippet.
for await (const event of client.videos.watch("vid_a1b2c3d4e5f6g7")) {
if (event.eventType === "completed") break;
}
const video = await client.videos.get("vid_a1b2c3d4e5f6g7");
if (video.status === "ready") {
document.body.insertAdjacentHTML("beforeend", video.embedCode);
}for event in client.videos.watch("vid_a1b2c3d4e5f6g7"):
if event.event_type == "completed":
break
video = client.videos.get("vid_a1b2c3d4e5f6g7")
if video.status == "ready":
print(video.embed_url)stream := client.Videos.Watch(ctx, "vid_a1b2c3d4e5f6g7")
defer stream.Close()
for stream.Next() {
if stream.Current().GetEventType() == "completed" {
break
}
}
video, err := client.Videos.Get(ctx, "vid_a1b2c3d4e5f6g7")From a storage origin
CreateFromUrl only accepts http(s) URLs. To ingest from a storage origin (gs://, s3://) into managed hosting, create a managed transcoding job with JobService.Create and set managed: true. The job writes its output to managed hosting and creates a source: "job" video; the create response carries a top-level video_id.
curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Create
-H "Authorization: Bearer ak_abc123"
-H "X-Organization-ID: org_f6g7h8i9j0"
-H "Content-Type: application/json"
-d '{
"input_url": "gs://my-bucket/source/product-demo.mp4",
"managed": true,
"outputs": [
{
"type": "hls",
"video": [
{"codec": "h264", "resolution": "1080p", "quality": "standard"},
{"codec": "h264", "resolution": "720p", "quality": "standard"}
]
}
]
}'{
"job": {
"id": "job_a1b2c3d4e5f6",
"object": "job",
"status": "pending"
},
"video_id": "vid_a1b2c3d4e5f6g7"
}Upload a video
When your source is a local file rather than a URL, upload it directly. The two-step handshake sends the bytes straight from your client to storage, never through the API:
- Call CreateUpload to reserve a
vid_record and get a presignedPUTURL. PUTthe file to that URL.- Call CompleteUpload to verify the bytes landed and kick off transcoding.
Use this single-part flow for files up to roughly 100 MB. For anything larger — or when you want resumable, parallel part uploads — use the multipart flow instead. The hard ceiling for either flow is 5 GB.
Create an upload
POST /transcodely.v1.VideoService/CreateUploadReserves a video (status uploading) and returns a time-limited presigned URL. PUT your file to upload_url before upload_expires_at (about one hour).
| Parameter | Type | Required | Description |
|---|---|---|---|
app_id | string | Yes | App to create the video under (app_ prefix). Must have hosting enabled. |
filename | string | Yes | Original filename (1–255 chars). Used for content-type detection and display. |
content_type | string | Yes | MIME type of the file (e.g. video/mp4). |
size_bytes | integer | Yes | File size in bytes (1–5368709120, i.e. up to 5 GB). |
title | string | No | Display title (max 255 chars). |
description | string | No | Description (max 2000 chars). |
tags | array | No | Up to 20 tags. |
visibility | string | No | public, unlisted, or private. Defaults to unlisted. |
preset | string | No | Preset ID or slug to drive the auto-transcode. Defaults to the app’s auto-profile ladder. |
Returns
Returns the created Video (status uploading), the presigned upload_url, and upload_expires_at.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/CreateUpload
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"app_id": "app_k1l2m3n4o5",
"filename": "product-demo.mp4",
"content_type": "video/mp4",
"size_bytes": 132710400,
"title": "Product demo",
"visibility": "public"
}'const { video, uploadUrl } = await client.videos.createUpload({
appId: "app_k1l2m3n4o5",
filename: "product-demo.mp4",
contentType: "video/mp4",
sizeBytes: 132710400,
title: "Product demo",
visibility: "public",
});
// Upload the bytes straight to storage.
await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: fileBlob,
});res = client.videos.create_upload(
app_id="app_k1l2m3n4o5",
filename="product-demo.mp4",
content_type="video/mp4",
size_bytes=132710400,
title="Product demo",
visibility="public",
)
with open("product-demo.mp4", "rb") as f:
requests.put(res.upload_url, data=f, headers={"Content-Type": "video/mp4"})res, err := client.Videos.CreateUpload(ctx, &transcodely.UploadCreateParams{
AppId: "app_k1l2m3n4o5",
Filename: "product-demo.mp4",
ContentType: "video/mp4",
SizeBytes: 132710400,
Title: proto.String("Product demo"),
Visibility: proto.String("public"),
})
// PUT the file bytes to res.GetUploadUrl().{
"video": {
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "uploading",
"visibility": "public",
"title": "Product demo",
"tags": [],
"input_content_type": "video/mp4",
"output_size_bytes": 0,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:00:00Z"
},
"upload_url": "https://storage.example.com/uploads/vid_a1b2c3d4e5f6g7?X-Amz-Signature=...",
"upload_expires_at": "2026-05-24T11:00:00Z"
}Complete the upload
POST /transcodely.v1.VideoService/CompleteUploadSignals that the PUT finished. Transcodely verifies the object exists, transitions the video to processing, and creates the auto-transcode job. This is when the video.uploaded webhook fires.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID from CreateUpload. |
Returns
Returns the Video with status transitioned to processing.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/CompleteUpload
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"id": "vid_a1b2c3d4e5f6g7"}'const video = await client.videos.completeUpload({ id: "vid_a1b2c3d4e5f6g7" });res = client.videos.complete_upload(id="vid_a1b2c3d4e5f6g7")video, err := client.Videos.CompleteUpload(ctx, &transcodely.UploadCompleteParams{
Id: "vid_a1b2c3d4e5f6g7",
}){
"video": {
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "processing",
"visibility": "public",
"title": "Product demo",
"tags": [],
"input_size_bytes": 132710400,
"input_content_type": "video/mp4",
"job_id": "job_a1b2c3d4e5f6",
"output_size_bytes": 0,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:00:30Z"
}
}To follow the video from here to ready, Watch it, poll Get, or subscribe to video.ready.
Upload large files with multipart
Multipart uploads split a file into parts (25 MB each by default, 5 MB minimum except the last part) that upload in parallel and can be retried individually. Use it for large files, flaky networks, or resumable upload UIs. The flow:
- CreateMultipartUpload — reserves the video and returns an
upload_idplus presigned URLs for the first batch of parts (up to 50). - GetUploadPartUrls — fetch URLs for later parts, or refresh expired ones (up to 100 per call).
PUTeach part and keep theETagresponse header for each.- CompleteMultipartUpload — submit every part’s
ETagto assemble the file and start processing. - AbortMultipartUpload — cancel instead; this discards the parts and deletes the video record.
Start a multipart upload
POST /transcodely.v1.VideoService/CreateMultipartUpload| Parameter | Type | Required | Description |
|---|---|---|---|
app_id | string | Yes | App to create the video under. Must have hosting enabled. |
filename | string | Yes | Original filename (1–255 chars). |
content_type | string | Yes | MIME type of the file. |
size_bytes | integer | Yes | File size in bytes (up to 5 GB). |
total_parts | integer | Yes | Number of parts, ceil(size_bytes / part_size_bytes) (1–10000). |
part_size_bytes | integer | No | Bytes per part. Defaults to 25 MB; minimum 5 MB. |
title | string | No | Display title. |
description | string | No | Description. |
tags | array | No | Up to 20 tags. |
visibility | string | No | public, unlisted, or private. Defaults to unlisted. |
preset | string | No | Preset ID or slug to drive the auto-transcode. |
Returns
Returns the Video (status uploading), the S3 upload_id, the first batch of presigned part URLs, and urls_expire_at.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/CreateMultipartUpload
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"app_id": "app_k1l2m3n4o5",
"filename": "conference-recording.mp4",
"content_type": "video/mp4",
"size_bytes": 2147483648,
"total_parts": 82
}'const res = await client.videos.createMultipartUpload({
appId: "app_k1l2m3n4o5",
filename: "conference-recording.mp4",
contentType: "video/mp4",
sizeBytes: 2147483648,
totalParts: 82,
});
// res.parts holds the first 50 presigned URLs; fetch the rest with getUploadPartUrls.res = client.videos.create_multipart_upload(
app_id="app_k1l2m3n4o5",
filename="conference-recording.mp4",
content_type="video/mp4",
size_bytes=2147483648,
total_parts=82,
)res, err := client.Videos.CreateMultipartUpload(ctx, &transcodely.MultipartCreateParams{
AppId: "app_k1l2m3n4o5",
Filename: "conference-recording.mp4",
ContentType: "video/mp4",
SizeBytes: 2147483648,
TotalParts: 82,
}){
"video": {
"id": "vid_c3d4e5f6g7h8i9",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "uploading",
"visibility": "unlisted",
"tags": [],
"input_content_type": "video/mp4",
"output_size_bytes": 0,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:00:00Z"
},
"upload_id": "a1b2c3d4e5f6-multipart-session-0d1e2f",
"parts": [
{ "part_number": 1, "upload_url": "https://storage.example.com/...part=1&sig=..." },
{ "part_number": 2, "upload_url": "https://storage.example.com/...part=2&sig=..." }
],
"urls_expire_at": "2026-05-24T11:00:00Z"
}Fetch more part URLs
POST /transcodely.v1.VideoService/GetUploadPartUrlsRequest fresh presigned URLs for specific part numbers — for parts beyond the first batch, or to refresh URLs that expired mid-upload.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID from CreateMultipartUpload. |
upload_id | string | Yes | The upload_id from CreateMultipartUpload. |
part_numbers | array | Yes | 1-based part numbers to sign (1–100 per call). |
Returns
Returns presigned URLs for the requested parts and urls_expire_at.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/GetUploadPartUrls
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"id": "vid_c3d4e5f6g7h8i9",
"upload_id": "a1b2c3d4e5f6-multipart-session-0d1e2f",
"part_numbers": [51, 52, 53]
}'{
"parts": [
{ "part_number": 51, "upload_url": "https://storage.example.com/...part=51&sig=..." },
{ "part_number": 52, "upload_url": "https://storage.example.com/...part=52&sig=..." }
],
"urls_expire_at": "2026-05-24T11:30:00Z"
}Complete a multipart upload
POST /transcodely.v1.VideoService/CompleteMultipartUploadAssembles the uploaded parts and starts processing. Submit every part with the ETag that storage returned when you PUT it.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID. |
upload_id | string | Yes | The upload_id from CreateMultipartUpload. |
parts | array | Yes | All parts as { part_number, etag }, ordered 1..N. |
Returns
Returns the Video with status transitioned to processing.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/CompleteMultipartUpload
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"id": "vid_c3d4e5f6g7h8i9",
"upload_id": "a1b2c3d4e5f6-multipart-session-0d1e2f",
"parts": [
{ "part_number": 1, "etag": ""9a0364b9e99bb480dd25e1f0284c8555"" },
{ "part_number": 2, "etag": ""c4ca4238a0b923820dcc509a6f75849b"" }
]
}'const video = await client.videos.completeMultipartUpload({
id: "vid_c3d4e5f6g7h8i9",
uploadId: "a1b2c3d4e5f6-multipart-session-0d1e2f",
parts: uploadedParts, // [{ partNumber, etag }, ...]
});video = client.videos.complete_multipart_upload(
id="vid_c3d4e5f6g7h8i9",
upload_id="a1b2c3d4e5f6-multipart-session-0d1e2f",
parts=uploaded_parts, # [{"part_number": 1, "etag": "..."}, ...]
)video, err := client.Videos.CompleteMultipartUpload(ctx, &transcodely.MultipartCompleteParams{
Id: "vid_c3d4e5f6g7h8i9",
UploadId: "a1b2c3d4e5f6-multipart-session-0d1e2f",
Parts: uploadedParts, // []*transcodely.CompletedPart
})Abort a multipart upload
POST /transcodely.v1.VideoService/AbortMultipartUploadCancels an in-progress multipart upload, cleans up the uploaded parts, and deletes the video record. Use it when the user cancels or the client gives up.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID. |
upload_id | string | Yes | The upload_id from CreateMultipartUpload. |
Returns an empty response.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/AbortMultipartUpload
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"id": "vid_c3d4e5f6g7h8i9",
"upload_id": "a1b2c3d4e5f6-multipart-session-0d1e2f"
}'await client.videos.abortMultipartUpload({
id: "vid_c3d4e5f6g7h8i9",
uploadId: "a1b2c3d4e5f6-multipart-session-0d1e2f",
});client.videos.abort_multipart_upload(
id="vid_c3d4e5f6g7h8i9",
upload_id="a1b2c3d4e5f6-multipart-session-0d1e2f",
)err := client.Videos.AbortMultipartUpload(ctx, &transcodely.MultipartAbortParams{
Id: "vid_c3d4e5f6g7h8i9",
UploadId: "a1b2c3d4e5f6-multipart-session-0d1e2f",
})Retrieve a video
POST /transcodely.v1.VideoService/Get| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID (vid_ prefix). |
Returns
Returns the Video, including its renditions and — for playable videos — playback and embed fields.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/Get
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"id": "vid_a1b2c3d4e5f6g7"}'const video = await client.videos.get("vid_a1b2c3d4e5f6g7");video = client.videos.get("vid_a1b2c3d4e5f6g7")video, err := client.Videos.Get(ctx, "vid_a1b2c3d4e5f6g7"){
"video": {
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "ready",
"visibility": "public",
"title": "Product demo",
"tags": ["marketing", "demo"],
"input_size_bytes": 132710400,
"input_content_type": "video/mp4",
"job_id": "job_a1b2c3d4e5f6",
"playback_url": "https://cdn.example.com/vid_a1b2c3d4e5f6g7/hls/master.m3u8?token=e9f8a1b2c3&expires=1716490800",
"embed_url": "https://play.transcodely.com/v/vid_a1b2c3d4e5f6g7",
"poster_url": "https://cdn.example.com/vid_a1b2c3d4e5f6g7/hls/poster.jpg?token=7c1d2e3f4a&expires=1716490800",
"duration_seconds": 128.5,
"renditions": [
{ "id": "ren_a1b2c3d4e5f6g7", "resolution": "1080p", "codec": "h264", "bitrate_kbps": 5000, "width": 1920, "height": 1080, "size_bytes": 80530636 }
],
"output_size_bytes": 80530636,
"encoding_cost": 0.03,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:03:30Z",
"ready_at": "2026-05-24T10:03:30Z"
}
}List videos
POST /transcodely.v1.VideoService/ListLists videos newest-first with page-token pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_size | integer | No | Items per page (1–100). Defaults to 20. |
page_token | string | No | Token from a previous response’s next_page_token. |
status | string | No | Filter by status (e.g. ready, processing). |
app_id | string | No | Filter to a specific app (app_ prefix). API-key callers may omit it (their key’s app is used) or pass their own app; a different app is rejected with permission_denied. Portal callers pass it to scope to one app, or omit it to list across every app in the org. |
Returns
Returns a list of Video objects, a next_page_token (absent on the last page), and total_count.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/List
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"page_size": 20, "status": "ready"}'for await (const video of client.videos.list({ pageSize: 20, status: "ready" }).autoPage()) {
console.log(video.id, video.title);
}for video in client.videos.list(page_size=20, status="ready").auto_paging_iter():
print(video.id, video.title)iter := client.Videos.List(ctx, &transcodely.VideoListParams{
PageSize: 20,
Status: proto.String("ready"),
})
defer iter.Close()
for iter.Next() {
video := iter.Current()
fmt.Println(video.GetId(), video.GetTitle())
}
if err := iter.Err(); err != nil {
log.Fatal(err)
}{
"videos": [
{
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "ready",
"visibility": "public",
"title": "Product demo",
"tags": ["marketing"],
"duration_seconds": 128.5,
"output_size_bytes": 125619404,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:03:30Z",
"ready_at": "2026-05-24T10:03:30Z"
}
],
"next_page_token": "eyJpZCI6InZpZF9hMWIyYzNkNGU1ZjZnNyJ9",
"total_count": 42
}Update a video
POST /transcodely.v1.VideoService/UpdateUpdates a video’s mutable metadata. Only the fields you send are applied.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID. |
title | string | No | New title. |
description | string | No | New description. |
tags | array | No | New tag list. Replaces the existing tags — but because proto3 repeated fields carry no presence, an empty list reads as “unchanged”, not “clear”. To remove all tags, use clear_tags. |
visibility | string | No | New visibility: public, unlisted, or private. |
clear_tags | boolean | No | Set true to remove all tags. Mutually exclusive with a non-empty tags list. |
Returns
Returns the updated Video.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/Update
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{
"id": "vid_a1b2c3d4e5f6g7",
"title": "Product demo (2026 edition)",
"visibility": "unlisted"
}'const video = await client.videos.update({
id: "vid_a1b2c3d4e5f6g7",
title: "Product demo (2026 edition)",
visibility: "unlisted",
});video = client.videos.update(
id="vid_a1b2c3d4e5f6g7",
title="Product demo (2026 edition)",
visibility="unlisted",
)video, err := client.Videos.Update(ctx, &transcodely.VideoUpdateParams{
Id: "vid_a1b2c3d4e5f6g7",
Title: proto.String("Product demo (2026 edition)"),
Visibility: proto.String("unlisted"),
}){
"video": {
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"app_id": "app_k1l2m3n4o5",
"source": "upload",
"status": "ready",
"visibility": "unlisted",
"title": "Product demo (2026 edition)",
"tags": ["marketing", "demo"],
"output_size_bytes": 125619404,
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T12:00:00Z",
"ready_at": "2026-05-24T10:03:30Z"
}
}To remove all tags in one call, send clear_tags:
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/Update
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"id": "vid_a1b2c3d4e5f6g7", "clear_tags": true}'Delete a video
POST /transcodely.v1.VideoService/DeleteSoft-deletes the video and schedules cleanup of its stored renditions. Fires the video.deleted webhook. Returns an empty response.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID. |
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/Delete
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"id": "vid_a1b2c3d4e5f6g7"}'await client.videos.delete("vid_a1b2c3d4e5f6g7");client.videos.delete("vid_a1b2c3d4e5f6g7")err := client.Videos.Delete(ctx, "vid_a1b2c3d4e5f6g7")Watch a video
POST /transcodely.v1.VideoService/WatchStreams a video’s state in real time. The server sends the current state immediately, then pushes updates as the video moves through processing. This is a server-streaming RPC; the stream closes when the video reaches a terminal state (ready or error). It is the simplest way to know when a video becomes playable.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Video ID. |
Stream events
Each message carries the current Video and an event_type:
| Event | Description |
|---|---|
snapshot | Initial state, sent on connect. |
status_change | The video’s status transitioned. |
progress | Transcoding progress advanced. |
completed | Terminal state reached. The stream closes after this. |
The server periodically re-sends the latest state as a keepalive; these arrive as regular progress (or status_change/completed) events, so there is no separate heartbeat event to handle.
curl -N -X POST https://api.transcodely.com/transcodely.v1.VideoService/Watch
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"id": "vid_a1b2c3d4e5f6g7"}'for await (const event of client.videos.watch("vid_a1b2c3d4e5f6g7")) {
console.log(event.eventType, event.video?.status);
if (event.eventType === "completed") break;
}for event in client.videos.watch("vid_a1b2c3d4e5f6g7"):
print(event.event_type, event.video.status)
if event.event_type == "completed":
breakstream := client.Videos.Watch(ctx, "vid_a1b2c3d4e5f6g7")
defer stream.Close()
for stream.Next() {
event := stream.Current()
fmt.Println(event.GetEventType(), event.GetVideo().GetStatus())
if event.GetEventType() == "completed" {
break
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}{"result": {"video": {"id": "vid_a1b2c3d4e5f6g7", "status": "processing"}, "event_type": "snapshot"}}
{"result": {"video": {"id": "vid_a1b2c3d4e5f6g7", "status": "ready"}, "event_type": "completed"}}Get usage
POST /transcodely.v1.VideoService/GetUsageReturns aggregated hosting usage and cost for a billing period, plus a per-day breakdown. App-scoped keys report their app; portal sessions report across the whole organization. All monetary fields are in the currency returned with the summary (EUR today).
| Parameter | Type | Required | Description |
|---|---|---|---|
billing_month | string | No | Month in YYYY-MM form (e.g. 2026-05). Defaults to the current billing month. |
Returns
Returns a usage summary.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/GetUsage
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"billing_month": "2026-05"}'const { usage } = await client.videos.getUsage({ billingMonth: "2026-05" });
console.log(usage?.totalCost, usage?.currency);res = client.videos.get_usage(billing_month="2026-05")
print(res.usage.total_cost, res.usage.currency)usage, err := client.Videos.GetUsage(ctx, "2026-05"){
"usage": {
"billing_month": "2026-05",
"videos_encoded": 128,
"encoding_minutes": 742.5,
"encoding_cost": 3.71,
"storage_gb_avg": 54.2,
"storage_cost": 0.81,
"videos_hosted": 96,
"egress_gb": 310.7,
"egress_cost": 3.11,
"total_requests": 148902,
"total_cost": 7.63,
"currency": "EUR",
"daily": [
{ "date": "2026-05-01", "storage_bytes": 51539607552, "egress_bytes": 9663676416, "request_count": 4820, "encoding_cost": 0.12 },
{ "date": "2026-05-02", "storage_bytes": 53687091200, "egress_bytes": 10307921510, "request_count": 5133, "encoding_cost": 0.09 }
]
}
}Usage summary
| Field | Type | Description |
|---|---|---|
billing_month | string | The billing month, YYYY-MM. |
videos_encoded | integer | Videos transcoded in the period. |
encoding_minutes | number | Total encoded minutes. |
encoding_cost | number | Encoding cost for the period. |
storage_gb_avg | number | Average stored gigabytes over the period. |
storage_cost | number | Storage cost for the period. |
videos_hosted | integer | Videos actively hosted. |
egress_gb | number | Gigabytes served to viewers. |
egress_cost | number | Egress (delivery) cost for the period. |
total_requests | integer | CDN requests served. |
total_cost | number | Sum of encoding, storage, and egress cost. |
currency | string | ISO 4217 currency for all cost fields (e.g. EUR). |
daily | array of objects | Per-day usage breakdown, oldest first. Only days with recorded activity are present (no zero-fill). See Daily usage. |
Daily usage
One entry per active day in the billing period. All monetary fields use the summary’s currency.
| Field | Type | Description |
|---|---|---|
date | string | Calendar day, YYYY-MM-DD (UTC). |
storage_bytes | integer | Stored bytes as of this day’s snapshot (point-in-time, not an increment). |
egress_bytes | integer | Bytes egressed during this day. |
request_count | integer | CDN requests served during this day. |
encoding_cost | number | Encoding cost for videos created on this day. |
Playback analytics
Every hosted video collects lightweight playback metrics — plays, watch time, and unique viewers — aggregated per UTC day. Data comes from a best-effort, anonymous beacon fired by the player and embed page: it identifies a view by a per-pageview session ID (a random UUID — no cookies, no PII). A play is counted once per pageview that starts playback; watch time is derived from periodic heartbeats (roughly 10-second granularity, so it is approximate); unique viewers counts distinct per-pageview sessions. Events roll up hourly, so the most recent activity can lag by up to an hour.
Get video stats
POST /transcodely.v1.VideoService/GetStatsReturns a single video’s daily series and range totals.
| Parameter | Type | Required | Description |
|---|---|---|---|
video_id | string | Yes | Video ID (vid_ prefix). |
start_date | string | No | Inclusive UTC range start, YYYY-MM-DD. Omitted means all retained history. |
end_date | string | No | Inclusive UTC range end, YYYY-MM-DD. Omitted means up to today. |
Returns
A daily array of daily stats (oldest first, only days with activity) plus a totals object summing them.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/GetStats
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"video_id": "vid_a1b2c3d4e5f6g7", "start_date": "2026-05-01", "end_date": "2026-05-31"}'SDK support: typed helpers (
getStats/get_stats) ship with the next SDK release; until then call the endpoint directly with any Connect/JSON client.
{
"daily": [
{ "date": "2026-05-01", "plays": 320, "watch_seconds": 184500, "unique_viewers": 271 },
{ "date": "2026-05-02", "plays": 298, "watch_seconds": 171200, "unique_viewers": 256 }
],
"totals": { "plays": 618, "watch_seconds": 355700, "unique_viewers": 527 }
}Daily stats
| Field | Type | Description |
|---|---|---|
date | string | UTC day, YYYY-MM-DD. |
plays | integer | Playback starts (one per pageview that began playing). |
watch_seconds | integer | Approximate watch time in seconds (heartbeat count × ~10s). |
unique_viewers | integer | Distinct per-pageview sessions. |
totals carries the same three metrics summed across the returned days. Unique viewers are summed per day, so a viewer active on several days counts once per day (exact only for a single-day range).
List top videos
POST /transcodely.v1.VideoService/ListTopVideosReturns an app’s videos ranked by plays (then watch time) over a date range — the leaderboard behind the dashboard’s top-videos view.
| Parameter | Type | Required | Description |
|---|---|---|---|
app_id | string | No | App to rank (app_ prefix). API-key callers may omit it (their key’s app is used); portal callers pass it to scope the leaderboard, or omit to use the org’s first app. |
start_date | string | No | Inclusive UTC range start, YYYY-MM-DD. |
end_date | string | No | Inclusive UTC range end, YYYY-MM-DD. |
limit | integer | No | Max videos to return (1–100). Defaults to 10. |
Returns
An items array of ranked videos, highest plays first.
curl -X POST https://api.transcodely.com/transcodely.v1.VideoService/ListTopVideos
-H "Authorization: Bearer ak_abc123"
-H "Content-Type: application/json"
-d '{"start_date": "2026-05-01", "end_date": "2026-05-31", "limit": 10}'SDK support: typed helpers (
listTopVideos/list_top_videos) ship with the next SDK release; until then call the endpoint directly.
{
"items": [
{
"video_id": "vid_a1b2c3d4e5f6g7",
"title": "Product demo",
"poster_url": "https://cdn.example.com/vid_a1b2c3d4e5f6g7/hls/poster.jpg",
"plays": 618,
"watch_seconds": 355700,
"unique_viewers": 527
}
]
}Ranked video fields
| Field | Type | Description |
|---|---|---|
video_id | string | Video ID (vid_ prefix). |
title | string | Video title, if set. |
poster_url | string | Poster image URL. Absent when no CDN is configured or the video has no poster. |
plays | integer | Total plays over the range. |
watch_seconds | integer | Total approximate watch seconds over the range. |
unique_viewers | integer | Unique viewers over the range (summed per day). |
Webhook events
Register a webhook endpoint to be notified about hosted videos. Four events fire:
| Event | When it fires | data payload |
|---|---|---|
video.uploaded | An upload (single-part or multipart) is completed and the video enters processing. Does not fire for URL ingest. | A Video snapshot with status: "processing". |
video.ready | Processing finished and the video is playable. | A full Video snapshot with status: "ready", its renditions, and duration_seconds. |
video.failed | Processing failed. | A Video snapshot with status: "error". |
video.deleted | A video is deleted. | A Video snapshot with status: "deleted". |
The data payload is a Video snapshot that mirrors Get, except the request-time playback/embed/poster URLs are omitted — those are computed per request and have no stable value at emit time. To get live URLs, call Get for the video, or construct the embed URL yourself as {player_base}/v/{video_id}.
video.ready is the event to drive playback readiness off of — it’s the webhook equivalent of a Watch completed:
{
"id": "evt_a1b2c3d4e5f6g7h8",
"type": "video.ready",
"data": {
"id": "vid_a1b2c3d4e5f6g7",
"object": "video",
"status": "ready",
"duration_seconds": 128.5,
"renditions": [
{ "id": "ren_a1b2c3d4e5f6g7", "resolution": "1080p", "codec": "h264" }
],
"created_at": "2026-05-24T10:00:00Z",
"updated_at": "2026-05-24T10:03:30Z"
}
}See the Webhooks API for endpoint management, signing, and delivery, and the Webhooks concept guide for the full envelope and signature-verification details.
Errors
| Connect code | Cause |
|---|---|
unauthenticated | API key missing/invalid, or the caller lacks access to the app. |
failed_precondition | Hosting is not enabled on the app; or CompleteUpload was called before the file was uploaded, or on a video that is not in uploading state. |
not_found | The vid_ doesn’t exist, was deleted, or belongs to another app. |
invalid_argument | Request validation failed (e.g. size_bytes over the 5 GB limit, an out-of-range part count, or — for CreateFromUrl — a non-http(s), private/internal, or otherwise invalid url), or the multipart parts don’t match what was uploaded. |
permission_denied | app_id names an app the caller can’t access (e.g. an API key passing a different app). |
internal | Unexpected server error — retry idempotent reads. |