Developer API
Integrate video hosting and streaming capabilities into your applications with our RESTful API. Upload videos, manage content, and deliver media at scale.
Projects API with subprojects, Auto-Delete Plans, Branding (per-project logo with auto-extracted color palette), and the full Player Skin API with 4-level inheritance (account / project / channel / video). See the corresponding sections below for the full reference.
Quick Start
Get your API key and make your first request in 5 minutes.
Get your API keys
Your API requests are authenticated using API keys. Any request that doesn't include an API key will return an error. You can generate an API key from your Dashboard under Videos → Global Settings → API Keys.
API Authentication
Use Bearer authentication. Send an Authorization header in all requests:
Authorization: Bearer YOUR_API_KEY
Base URL
StreamVault has a single canonical base URL. All endpoints — including video uploads — live under it:
| Purpose | URL |
|---|---|
| API base (everything: videos, uploads, channels, streams, projects, branding) | https://api.streamvault.one/v1 |
| Health check (returns Worker version + uptime) | https://api.streamvault.one/v1/health |
Make your first request
Send an authenticated request to the videos endpoint to list your videos:
// List your videos
const response = await fetch('https://api.streamvault.one/v1/videos', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
});
const data = await response.json();
console.log(data.items);Example Response
{
"items": [
{
"videoId": "fFL56RU6PjvJPeE",
"videoKey": "fFL56RU6PjvJPeE",
"name": "My Video",
"projectId": "40e78621-c780-4224-94fc-20a831110d91",
"duration": 150,
"status": { "state": "ready" },
"created": "2024-01-15T10:30:00+00:00"
}
],
"page": 1,
"size": 20,
"total": 10
}A page holds up to 100 videos (size, 20 by default); total is how many videos match, so keep asking for the next page until you have them all. Each video has more fields than this example shows: see List Videos.
Rate Limits
API requests are limited to 120 requests per minute per API key. When you exceed the limit, requests return 429 Too Many Requests until the window resets. Batch endpoints let you act on many videos in a single request — prefer them over per-video loops.
{
"error": "Rate limit exceeded. Please slow down and try again shortly."
}Errors
Errors return a JSON body with an error message and a conventional HTTP status code:
| Status | Meaning |
|---|---|
| 400 | Invalid request — missing or malformed parameters |
| 401 | Missing, invalid, or revoked API key |
| 404 | Resource not found, or not owned by your account |
| 429 | Rate limit exceeded — retry after a short delay |
| 500 | Something went wrong on our side — safe to retry |
{
"error": "Invalid or revoked API key"
}Videos API
The Videos API allows you to upload, manage, and retrieve video content programmatically.
Upload Videos
| Method | Endpoint | Description |
|---|---|---|
| POST | /videos | Upload video via URL |
| POST | /videos/browser | Get TUS upload credentials for browser-based upload |
Manage Videos
| Method | Endpoint | Description |
|---|---|---|
| GET | /videos | Get list of videos |
| GET | /videos/{id} | Get a single video |
| GET | /videos/{id}/status | Get video transcoding status |
| PUT | /videos/{id} | Update video details |
| DELETE | /videos/{id} | Delete a video |
| POST | /videos/{id}/cta/copy | Copy CTA from one video to others |
| POST | /videos/change-project | Move videos between projects |
Additional Operations
| Method | Endpoint | Description |
|---|---|---|
| GET | /videos/{id}/original-file | Get original video URL |
| GET | /videos/collected-emails | Get collected emails (last 365 days) |
| POST | /videos/{id}/expirable-link | Create expirable playback link |
| PUT | /videos/image/{id} | Update video thumbnail |
| POST | /videos/captions/{id} | Add captions/subtitles |
| DELETE | /videos/captions?videoId=&captionsId= | Delete captions/subtitles |
Video Status Codes
| Status | Value | Description |
|---|---|---|
| Na | 0 | Not available |
| Uploaded | 1 | Uploaded |
| Processing | 2 | Transcoding in progress |
| Published | 3 | Ready for playback |
| Error | -1 | Transcoding failed |
| Deleted | 50 | Deleted |
Quick Example
// Upload a video via URL
const response = await fetch('https://api.streamvault.one/v1/videos', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
channelKey: 'abc123',
url: 'https://example.com/video.mp4'
})
});
const video = await response.json();
console.log(video.iframeLink);Projects API
Projects are folders that group your videos. They support arbitrary nesting via parent_id so you can model brands → shows → seasons (or any hierarchy that fits your workflow).
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /projects | List your projects, paged (page, size, parentId, search, sort) |
| POST | /projects | Create a project (or subproject via parent_id) |
| GET | /projects/{id} | Get one project, with its sub-project and video counts |
| PATCH | /projects/{id} | Update name / description / parent_id |
| DELETE | /projects/{id} | Delete (cascade-deletes videos in the project) |
Create a project
curl -X POST https://api.streamvault.one/v1/projects \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Kinana VOD AR",
"description": "Arabic news bulletins for the Kinana brand",
"parent_id": null
}'Response
{
"id": "448ba8b6-eaa6-4ff8-80be-9a075e47c642",
"user_id": "...",
"name": "Kinana VOD AR",
"description": "...",
"parent_id": null,
"created_at": "2026-05-07T...",
"updated_at": "2026-05-07T..."
}Subprojects (parent_id)
Pass parent_id (a UUID) to create a subproject under an existing parent. Set parent_id to null for a top-level project.
Update / move a project
PATCH /v1/projects/{id} accepts a partial body. Setting parent_id to another project's UUID re-parents it; setting it to null makes it top-level.
{ "name": "Renamed", "description": "New description", "parent_id": "uuid-of-new-parent-or-null" }Listing sub-projects
GET /v1/projects is paged and lists projects at every level of nesting. Add parentId to list only the sub-projects directly below one project. A video belongs to exactly one project, so a parent that only holds sub-projects has no videos of its own: to get everything under it, use includeSubprojects=true on GET /v1/videos (below).
Query parameters: GET /v1/projects
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | 1-based page number. A page past the last one returns an empty items array and the real total.Default: 1 |
| size | integer | Optional | Projects per page, 1 to 200. limit is accepted as an alias.Default: 50 |
| parentId | string | Optional | Only the projects directly below this project (its id or short key). Use root for top-level projects only. Left out: projects at every level. |
| search | string | Optional | Name contains this text, case-insensitive. Up to 100 characters. |
| sort | string | Optional | created_at, updated_at or name. Prefix with - for descending.Default: -created_at |
The sub-projects of one project
curl "https://api.streamvault.one/v1/projects?parentId=PROJECT_ID&size=100" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"items": [
{ "id": "...", "name": "1. Basic Forex Education", "parent_id": "PROJECT_ID", "created_at": "...", "updated_at": "..." }
],
"page": 1,
"size": 100,
"total": 13,
"hasMore": false
}total is the number of matching projects across all pages; hasMore is true while there is another page. To read every project:
const all = [];
for (let page = 1; ; page++) {
const res = await fetch('https://api.streamvault.one/v1/projects?size=200&page=' + page, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
});
const data = await res.json();
all.push(...data.items);
if (!data.hasMore) break;
}Videos of a project and everything below it
GET /v1/videos?projectId=ID lists the videos of exactly that project, so a project that only holds sub-projects returns none. Add includeSubprojects=true to cover the project and every sub-project below it, at any depth. Every video says which project it sits in (projectId), so you can tell the sub-projects apart.
curl "https://api.streamvault.one/v1/videos?projectId=PROJECT_ID&includeSubprojects=true&size=100" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"items": [
{ "videoId": "...", "name": "...", "projectId": "ID-OF-THE-SUB-PROJECT-IT-SITS-IN", "...": "..." }
],
"page": 1,
"size": 100,
"total": 132,
"includeSubprojects": true,
"projectCount": 14
}projectCount is how many projects the listing covered: the project itself plus the sub-projects below it. One call covers a project and up to 149 sub-projects under it. A bigger tree is refused with 400 and "code": "SUBTREE_TOO_LARGE": list its sub-projects with parentId and request their videos one at a time.
Counts on one project
GET /v1/projects/{id} also says how big the tree under the project is. A count is null when it could not be worked out (a tree deeper than 10 levels or larger than 2,000 projects, or a temporary error); it is never a number that is too small.
{
"id": "PROJECT_ID",
"name": "FXEM VOD AR",
"parent_id": null,
"ancestor_chain": [],
"child_count": 13,
"video_count": 0,
"descendant_count": 13,
"subtree_video_count": 132
}Count fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| child_count | integer | Optional | Sub-projects directly below this project. |
| video_count | integer | Optional | Videos directly in this project (what GET /v1/videos?projectId= lists). |
| descendant_count | integer | Optional | Sub-projects at every level below this project. |
| subtree_video_count | integer | Optional | Videos in this project and in every sub-project below it (what includeSubprojects=true lists). |
Errors
| Status | Value | Description |
|---|---|---|
| Bad request | 400 | parentId, search, sort or includeSubprojects cannot be read (the message names it), includeSubprojects was sent without a projectId, or the tree is too large (SUBTREE_TOO_LARGE) |
| Not found | 404 | The project named by parentId, or by projectId together with includeSubprojects, is not one of yours |
An empty value counts as leaving the parameter out. These parameters are checked strictly: a value that cannot be read is an error that names it, never silently ignored, because an ignored filter would hand back every project and look like a result.
Auto-Delete Plans
Delete old videos automatically. A plan names the projects it covers, an age in days and an optional title filter; while a plan is active, an hourly job permanently deletes the videos it matches. Manage plans in the dashboard under Settings → Auto Delete, or with the endpoints below. Mirrors DynTube's /auto-delete-plans URL shape so existing DynTube integrations work with just a host swap.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /auto-delete-plans/search | List with body-based pagination (DynTube-compatible) |
| GET | /auto-delete-plans | List with query params (?page=1&size=50) |
| GET | /auto-delete-plans/{id} | Get one plan |
| GET | /auto-delete-plans/{id}/preview | The videos the plan matches right now, oldest first — deletes nothing |
| POST | /auto-delete-plans | Create a plan |
| PUT | /auto-delete-plans/{id} | Update a plan — send only the fields you want to change; the others stay as they are |
| PATCH | /auto-delete-plans/{id} | Same as PUT |
| DELETE | /auto-delete-plans/{id} | Delete a plan |
Plan shape
{
"id": "uuid",
"name": "Daily News Cleanup",
"project_ids": ["uuid1", "uuid2"],
"retention_days": 15,
"title_filter": { "enabled": false, "value": "" },
"is_active": true,
"last_run_at": "2026-05-09T12:00:00Z",
"last_run_deleted_count": 7,
"last_run_error_count": 0,
"created_at": "2026-05-01T00:00:00Z",
"updated_at": "2026-05-09T12:00:00Z"
}project_ids— list of project UUIDs the rule applies to; each project's sub-projects are covered too. Every entry must be the id of one of your own projects, otherwise the request is refused. Empty array (or noproject_idsat all) = applies to every project you own.retention_days— videos older than this many days are deleted. A whole number from 1 to 3650.title_filter.enabled+value— substring match (case-insensitive); the text is taken literally, so%and_are ordinary characters. Videos must match the filter to be deleted. Avaluewithout"enabled": truedoes nothing — the plan then covers every video of its projects.is_active—falsepauses the plan without deleting it. A plan created through the API is active unless you sendis_active: false; the dashboard always creates plans paused.
How a run works
- Once an hour, every active plan is evaluated. A video matches when it is in a covered project (or one of its sub-projects), is older than
retention_daysand passes the title filter, if one is on. - Matches are deleted oldest first, at most 40 videos per hourly run, shared by all plans. A bigger backlog is worked off over several hours.
- A video that is still being encoded is skipped and tried again the next hour.
- A deleted video disappears from your account at once. Its files are removed from storage about 24 hours later.
- If a step fails (for example a storage error) the video is kept and tried again the next hour.
last_run_at,last_run_deleted_countandlast_run_error_countdescribe the latest run. - Deleting is permanent. Pause a plan (
is_active: false) or delete it to stop.
Safe way to start: create the plan paused, read its preview, then switch it on.
- Only the fields listed above are accepted. A key the API does not know (
project_idinstead ofproject_ids, say) answers400, names the key and changes nothing. The fields aGETreturns (id,created_at,last_run_*) may be sent back; they are ignored. A flag must betrueorfalse—"false"in quotes is refused. A plan with no project list covers all your projects, so always sendis_active: falsewith a new plan and check its preview before you switch it on. - Edits made through the API apply at once. The next hourly run uses the new projects, age or title filter of an active plan. The dashboard is more careful: changing what an active plan selects pauses it, so the new selection can be previewed first. If you change an active plan through the API, send
is_active: falsewith the change, preview, then switch it back on. - A plan id that does not exist (or is not yours) answers
404on every endpoint, the preview included. Creating or changing a plan with a project id that is not yours, a retention that is not a whole number of days, or a list that holds anything but project ids answers400with the reason, and nothing is created or changed.
Create a plan
curl -X POST https://api.streamvault.one/v1/auto-delete-plans \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily News 15-day cleanup",
"project_ids": ["uuid-of-project-1"],
"retention_days": 15,
"title_filter": { "enabled": true, "value": "daily news" },
"is_active": false
}'Preview, then start
# What would this plan delete? (nothing is deleted)
curl https://api.streamvault.one/v1/auto-delete-plans/PLAN_ID/preview \
-H "Authorization: Bearer YOUR_API_KEY"
# Looks right? Switch it on — it runs from the next hour
curl -X PUT https://api.streamvault.one/v1/auto-delete-plans/PLAN_ID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": true }'{
"plan_id": "uuid",
"evaluated_at": "2026-10-12T09:00:00Z",
"candidate_count": 485,
"candidates": [
{ "video_id": "uuid", "name": "Daily news 29 Sep", "created_at": "2026-09-29T13:18:51Z", "age_days": 13, "storage_location": "b2" }
]
}Search / list
curl -X POST https://api.streamvault.one/v1/auto-delete-plans/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "page": 1, "pageSize": 50 }'Response
{
"plans": [ /* ... */ ],
"pager": { "page": 1, "pageSize": 50, "totalResults": 3, "totalPages": 1 }
}Branding (Project Logo)
Upload a brand logo per project, automatically extract its color palette, and apply it to that project's player skin (controls color, big play disc, frame, watermark). The same logo also doubles as a corner overlay on the player.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| PUT | /projects/{id}/logo | Upload a logo (multipart) and auto-extract its palette |
| GET | /projects/{id}/logo | Get the logo URL + extracted palette |
| DELETE | /projects/{id}/logo | Remove the logo (does not clear applied colors) |
| POST | /projects/{id}/logo/apply | Re-apply the saved palette to player_settings |
Upload a file
curl -X PUT https://api.streamvault.one/v1/projects/PROJECT_ID/logo \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@./logo.png" \
-F "auto_apply=true" \
-F 'palette={"vibrant":"#FFC800","dark_muted":"#121218","muted":"#FFFFFF","text_on_vibrant":"#000000"}'Or pass a remote URL
curl -X PUT https://api.streamvault.one/v1/projects/PROJECT_ID/logo \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "url=https://example.com/logo.png" \ -F "auto_apply=true"
- Max 2 MB
- Accepted: PNG, JPEG, WebP, SVG (SVG is rasterized at 256×256 server-side before extraction)
auto_apply(defaulttrue) — write the extracted palette toproject_player_settings(controls_color,big_play_color,big_play_icon_color,player_color,watermark_color)palette(optional, JSON) — pre-extracted palette to use directly. When provided, the server skips its own extraction and uses your values verbatim. Recommended from a browser context where@vibrant/coreis already running client-side; server-side extraction is best-effort and may return only a partial palette. Shape:{ vibrant?, dark_vibrant?, light_vibrant?, muted?, dark_muted?, light_muted?, text_on_vibrant? }— all fields hex strings, all optional/nullable.
Response (palette extraction)
{
"logo_url": "https://api.streamvault.one/b2/project-logos/PROJECT_ID/logo.png",
"logo_palette": {
"vibrant": "#FF6B35",
"dark_vibrant": "#7A2E0E",
"light_vibrant": "#FFB89A",
"muted": "#806257",
"dark_muted": "#3B2A20",
"light_muted": "#D4C4BA",
"text_on_vibrant": "#FFFFFF",
"extracted_at": "2026-05-09T22:00:00Z"
},
"applied": true,
"applied_fields": [
"controls_color", "big_play_color", "big_play_icon_color",
"player_color", "watermark_color"
]
}Re-apply palette
If you previously declined auto_apply, or you've manually overridden colors and want to revert to the logo-derived palette:
curl -X POST https://api.streamvault.one/v1/projects/PROJECT_ID/logo/apply \ -H "Authorization: Bearer YOUR_API_KEY"
Logo overlay & deletion
When the project's logo_overlay_enabled player setting is true, the uploaded logo renders as a corner watermark on every player in the project. Position, size, and opacity are configured under Player Skin.
Delete logo
curl -X DELETE https://api.streamvault.one/v1/projects/PROJECT_ID/logo \ -H "Authorization: Bearer YOUR_API_KEY"
Note: deleting the logo does not clear the applied colors from project_player_settings. Use the Player Skin API or the dashboard's "Reset to inherit" links to clear those.
Player Skin API
Configure how the video player looks and behaves. Settings can be set at account, project (including sub-project), channel and video level; the most specific level wins, field by field.
When controls_color is set (either directly or via the Branding palette auto-apply), the entire control chrome paints with that color: progress fill, seek handle, volume slider, time display, playback speed pill, and every icon (play/pause, next, volume, settings, picture-in-picture, fullscreen). The big-play disc uses big_play_color, which falls back to controls_color when unset. Net effect: one brand color drives every accent on the player.
Inheritance hierarchy
Per-video override (highest priority, video.player_settings JSONB) ↓ Channel default (channel_player_settings — look and captions only) ↓ Sub-project default (project_player_settings of the sub-project) ↓ Project default (project_player_settings — also where logo palette writes) ↓ Account default (account_player_settings — set in Global Settings → Player Skins)
A field set to null means "inherit from parent". The runtime resolution merges left-to-right; per-video wins, then channel, sub-project, project, account, then a built-in default.
A sub-project is a project with a parent, so it has no endpoints of its own: use the project endpoints with the sub-project's id. Its settings apply on top of its parent's, which apply on top of the account's (up to ten levels deep), and a sub-project that sets nothing shows its parent's look. A channel only changes how the player looks (skin, colors, big-play button) and whether captions start on; it does not change behavior such as autoplay or loop.
Account / project / sub-project / channel / video endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /account/player-settings | Get your account-level skin defaults |
| PATCH | /account/player-settings | Update account-level defaults |
| GET | /projects/{id}/player-settings | Get a project's (or sub-project's) own settings, plus the effective settings its videos get and the settings it inherits |
| PATCH | /projects/{id}/player-settings | Override fields at project or sub-project level |
| GET | /channels/{id}/player-settings | Get this channel's own settings |
| PATCH | /channels/{id}/player-settings | Override look and captions at channel level |
| GET | /videos/{id}/player-settings | Get this video's own overrides and the merged effective settings |
| PATCH | /videos/{id}/player-settings | Override at the video level |
Sending changes
PATCH (or PUT) changes only the fields you send. Send them flat, as in the example below, or nested under player_settings; if a field appears both ways, the flat one wins. skin and player_skin are the same field. Fields you do not send keep their value; send "merge": false to replace everything stored on that layer with what you send.
- Clearing.
nullremoves that override at project, sub-project, channel and video level, so the next layer shows through. The account is the bottom layer, so therenullputs the built-in value back. - Validation.
skinmust be one of the skins below, and opacities are whole numbers from 0 to 100 (out-of-range numbers are clamped). A wrong type answers 400. - Unknown fields. A key this route does not recognize is not applied. The response lists it under
ignored, so a typo is visible instead of silent. - Video ids. The video endpoint takes the video's
idas the API returns it, or its dashboard id.
Settings shape
{
"skin": "modern",
"player_color": "#0E1014",
"controls_color": "#4CC2E0",
"watermark_color": "#FFFFFF",
"controls_opacity": 90,
"watermark_opacity": 70,
"big_play_button": true,
"big_play_color": null,
"big_play_icon_color": null,
"big_play_opacity": 100,
"big_play_size": "md",
"big_play_shape": "circle",
"big_play_show_on_hover": false,
"autoplay": false,
"start_muted": false,
"loop": false,
"captions_on_by_default": false,
"seek_buttons": true,
"fullscreen_control": true,
"ip_watermark": false,
"playback_speed_control": true,
"allow_fast_forward": true,
"click_for_sound": false,
"click_to_resume": false,
"auto_resume_playback": false,
"default_quality": "auto",
"default_volume": 100,
"show_player_controls": true,
"persistent_control_bar": false,
"volume_control": true,
"quality_selector": true,
"sticky_player": false,
"pause_on_scroll": false
}Skin options
skin accepts one of: modern (default), pill, sharp, halo, minimal, bold, heritage, floating.
Big play button options
big_play_color(hex,null= inheritcontrols_color)big_play_icon_color(hex,null= inheritplayer_color)big_play_opacity(int 0–100)big_play_size(sm/md/lg)big_play_shape(circle/rounded/square)big_play_show_on_hover(boolean — keeps the disc visible during playback while the user hovers; not in DynTube)
Inheritance behavior
Setting any color/opacity/skin field to null clears that override. The next-highest-priority layer takes effect. Example: a project sets controls_color: '#FF6B35', a video sets controls_color: null — that video plays with the project's orange. If the project later clears its override too, the video falls back to the account default cyan.
The dashboard shows an "Inherited from {project|channel|account}" badge next to fields that are inheriting, with a "Reset to inherit" link on overridden fields.
Update example
curl -X PATCH https://api.streamvault.one/v1/projects/PROJECT_ID/player-settings \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"skin": "pill",
"controls_color": "#FF6B35",
"big_play_size": "lg",
"big_play_show_on_hover": true,
"ip_watermark": true
}'Read what the player will use
A project's GET returns its own row plus effective (what the player gets for videos in this project: the account, then each parent project, then this one) and inherited (the same without this project's own settings). A video's GET returns its own overrides in player_settings and the full merge in effective; add ?channel_id=CHANNEL_ID to include a channel's look. Fields that no layer sets use the player's built-in defaults.
curl https://api.streamvault.one/v1/videos/VIDEO_ID/player-settings \ -H "Authorization: Bearer YOUR_API_KEY"
{
"id": "VIDEO_ID",
"project_id": "SUB_PROJECT_ID",
"player_settings": { "autoplay": true },
"effective": {
"skin": "pill",
"player_color": "#0E1014",
"controls_color": "#FF6B35",
"controls_opacity": 90,
"autoplay": true
}
}Saved skins (legacy)
Older videos can point at a saved skin, through skinId on PATCH /videos/{id}. That is a legacy mechanism, separate from the layers above: it only touches a few cosmetic details (loading color, background, corner radius, a corner logo), saved skins cannot be created or listed through the API, and a project's default skin is not applied to its videos. To style the player, use the endpoints above.
CTAs
Overlay call-to-action buttons and email-capture prompts on the player — per video, or per project with inheritance. In the CTA and subtitle routes, {id} is the video's id as GET /videos lists it and its embed link carries it; the video's asset id (cf_video_id) is accepted too.
Per-video CTAs
| Method | Endpoint | Description |
|---|---|---|
| GET | /videos/{id}/ctas | List a video's CTAs |
| POST | /videos/{id}/ctas | Create a CTA on a video |
| PATCH | /videos/{id}/ctas/{ctaId} | Update a CTA |
| DELETE | /videos/{id}/ctas/{ctaId} | Delete a CTA |
| POST | /videos/{id}/cta/copy | Copy this video's CTAs to other videos |
| POST | /videos/batch/cta | Bulk-assign a CTA to many videos |
Per-project CTAs
| Method | Endpoint | Description |
|---|---|---|
| GET | /projects/{id}/ctas | List a project's CTAs |
| POST | /projects/{id}/ctas | Create a project-level CTA |
| PATCH | /projects/{id}/ctas/{ctaId} | Update a project CTA |
| DELETE | /projects/{id}/ctas/{ctaId} | Delete a project CTA |
Inheritance & override
Videos inherit their project's CTAs by default. Toggle per level withPATCH /videos/{id}or PATCH /projects/{id}with { "cta_inherit": false }. A video's own CTAs render on top of inherited ones.
CTA shape
{
"title": "Subscribe",
"url": "https://example.com", // https://, http://, mailto: or tel: only
"type": "link", // "link" or "email"
"show_at": 0, // seconds; when to appear
"hide_at": null, // seconds, later than show_at; null = until end
"position": "bottom-right", // top/bottom x left/center/right, or "center"
"background": "#2764EB", // null = brand palette fallback
"text_color": "#ffffff"
}A CTA's link must be a full link starting with https://,http://,mailto: ortel:; anything else (a javascript: link, a bare domain, a relative path) is refused with 400, and the player never draws one. The API creates link and email CTAs; image, image-on-pause and custom-HTML CTAs are made in the dashboard.
Full parameter reference (palette keys, email-capture fields) is in theAPI Referenceunder CTAs.
Webhooks
StreamVault allows you to set up webhooks to receive notifications for specific events such asvideoPublished.
Setting Up Webhooks
- Log in to your StreamVault account.
- Navigate to Dashboard → Settings → Webhooks.
- Set the event type (e.g.
videoPublished). - Provide the webhook URL where you want to receive notifications.
- Webhook configuration will be automatically saved.
Video Published Event
{
"event": "videoPublished",
"video": {
"Id": "YOUR_VIDEO_ID",
"Duration": "00:00:02.2200000",
"ProjectId": "YOUR_PROJECT_ID",
"AccountKey": "YOUR_ACCOUNT_KEY",
"Region": "use",
"Image": {
"xsUrl": "",
"smUrl": "",
"mdUrl": "",
"lgUrl": ""
},
"Captions": [],
"Key": "YOUR_VIDEO_KEY",
"ChannelKey": "YOUR_CHANNEL_KEY",
"PrivateLink": "YOUR_PRIVATE_LINK",
"IframeLink": "YOUR_IFRAME_LINK",
"HLSLink": "YOUR_HLS_LINK",
"PlanType": 1,
"Mp4Url": "",
"Mp4Urls": [],
"Formats": {
"Hls": true,
"Mp4": false,
"Options": { "SecureMp4": true }
},
"HLSUrl": "YOUR_HLS_URL",
"HLSUrlWeb": "YOUR_HLS_URL_WEB",
"Title": "YOUR_VIDEO_TITLE",
"Description": "YOUR_VIDEO_DESCRIPTION",
"Options": {},
"Tags": [],
"Version": 1,
"Status": 3,
"Created": "2024-01-15T10:30:00Z"
}
}SDKs & Libraries
Official client libraries are on the roadmap but are not published yet. Until they ship, the REST API works from any language with an HTTP client; the examples below use nothing beyond what your runtime already provides.
JavaScript
Node.js & browser SDK
Coming soonReact
React component library
Coming soonPython
Python SDK
Coming soonJavaScript Quick Example (REST)
const BASE = 'https://api.streamvault.one/v1';
const headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
};
// List videos
const { videos } = await fetch(`${BASE}/videos`, { headers }).then(r => r.json());
// Upload via URL
const video = await fetch(`${BASE}/videos`, {
method: 'POST',
headers,
body: JSON.stringify({ url: 'https://example.com/video.mp4', title: 'My Video' }),
}).then(r => r.json());
console.log(video.iframeLink);React Embed Component
function StreamVaultEmbed({ videoId }) {
return (
<div style={{ position: 'relative', paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}>
<iframe
src={`https://streamvault.one/embed/${videoId}`}
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', border: 'none' }}
allow="encrypted-media; fullscreen; picture-in-picture"
allowFullScreen
/>
</div>
);
}Embed via iframe
<!-- Embed StreamVault Player --> <iframe src="https://streamvault.one/embed/VIDEO_ID" width="640" height="360" frameborder="0" allow="encrypted-media" allowfullscreen> </iframe>
Need Help?
Can't find what you're looking for? Reach out to our support team or check the user documentation.