Pega tu API Key o token JWT para probar endpoints directamente.
Inicia sesión con tu cuenta para obtener un JWT automáticamente.
Hosting de video programático. Sube, transcodifica, transmite y gestiona tu infraestructura de video a través de una API RESTful con entrega HLS adaptativa y funciones potenciadas por IA.
La API de StreamVault te permite subir, gestionar y transmitir video a escala. Toda la comunicación es sobre HTTPS con cuerpos JSON. Los timestamps son enteros UNIX epoch (segundos).
Content-Type: application/json. Los errores siempre incluyen { "error": "mensaje" }.
Todos los endpoints autenticados requieren Authorization y X-Workspace-Id.
Obtenlo vía POST /auth/login. Expira en 15 min. Renueva con POST /auth/refresh.
Las keys inician con sv_live_. Se muestran una sola vez al crearlas.
curl BASE_URL/api/videos \ -H "Authorization: Bearer sv_live_TU_KEY" \ -H "X-Workspace-Id: TU_WORKSPACE_ID"
Códigos HTTP estándar. Todos los errores incluyen un campo error legible.
| Estado | Significado |
|---|---|
| 200 | Éxito |
| 201 | Recurso creado |
| 202 | Async en cola |
| 204 | Eliminado |
| 400 | Petición inválida |
| 401 | No autenticado |
| 403 | Sin permisos / scope |
| 404 | No encontrado |
| 429 | Rate limit — ver Retry-After |
| 500 | Error interno |
// HTTP 404 { "error": "Video no encontrado", "status": 404 }
Sube un archivo de video. Se transcoda automáticamente a HLS multicalidad.
public | private | unlistedcurl -X POST BASE_URL/api/upload \ -H "Authorization: Bearer sv_live_KEY" \ -H "X-Workspace-Id: WS_ID" \ -F "file=@video.mp4" \ -F "title=Demo" \ -F "visibility=public"
const form = new FormData(); form.append('file', fileInput.files[0]); form.append('title', 'Demo'); const res = await fetch('BASE_URL/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer sv_live_KEY', 'X-Workspace-Id': 'WS_ID' }, body: form }); const video = await res.json();
{
"id": "abc123",
"title": "Demo",
"status": "processing",
"m3u8Url": "https://..."
}
Importa un video desde una URL externa.
curl -X POST BASE_URL/api/import \ -H "Authorization: Bearer sv_live_KEY" \ -H "X-Workspace-Id: WS_ID" \ -H "Content-Type: application/json" \ -d '{"url":"https://ejemplo.com/v.mp4"}'
Lista videos del workspace con paginación cursor.
ready | processing | errornewest | oldest | viewscurl "BASE_URL/api/videos?limit=10&sort=newest" \ -H "Authorization: Bearer sv_live_KEY" \ -H "X-Workspace-Id: WS_ID"
Obtiene el detalle completo de un video incluyendo URLs, embed config y ads.
Actualiza metadata del video.
public|private|unlisted|passwordElimina el video y todos sus archivos S3 permanentemente. Retorna 204.
Operaciones masivas sobre hasta 200 videos.
delete | move | visibilitymovevisibilityObtiene un token firmado para reproducir el HLS cifrado AES-128.
Genera URL firmada de descarga. TTL 10 min. Solo plan Pro+.
Reintenta la transcodificación de un video con estado error.
Incrementa el contador de vistas. Sin auth. Throttle: 1 por IP/video cada 10s.
Rastrea la posición por usuario para experiencias "continuar viendo".
Obtiene la posición guardada. Respuesta: { "position": 65.4 }
Estructura jerárquica dentro del workspace. Los videos en carpetas eliminadas quedan sin carpeta.
Lista todas las carpetas del workspace.
Renombra o mueve la carpeta.
Elimina la carpeta.
Colecciones ordenadas de videos. Las playlists públicas pueden embeberse.
public | privateAgrega un video. Body: {"videoId":"..."}. También: DELETE .../videos/:videoId y POST .../videos/reorder
Marcadores en la línea de tiempo del reproductor. Exportables como WebVTT.
También: GET .../chapters/export.vtt
Sube subtítulos VTT/SRT o pistas de audio alternas. Los SRT se convierten automáticamente a VTT.
Sube una pista (multipart/form-data).
subtitles|captions|audioSpeech-to-text con Whisper. Procesamiento asíncrono con notificación vía webhook al completar.
Inicia una transcripción. Responde 202 con { "status": "queued" }.
transcription.complete o haz polling al endpoint de listado.
Busca texto en los subtítulos. Responde con segmentos y timestamps.
Descarga el VTT. Query opcional: ?offset=2.5
Reportes por video: vistas, retención, dispositivos, geografía y viewers en tiempo real.
Reporte completo. Query: ?days=7|30|90
Viewers activos en ventana de 5 min. Respuesta: { "liveViewers": 42 }
Exporta CSV. Rate limit: 5 req/min.
Callbacks HTTP firmados HMAC-SHA256. Eventos: video.ready, video.failed, video.deleted, transcription.complete, *
secret (una vez). Verifica: X-Webhook-Signature: sha256=HMAC(secret,rawBody)
Últimos 50 intentos con códigos de estado y latencia.
Gestión multi-tenant. Invita miembros y gestiona roles.
Actualiza nombre, settings, watermark, dominio embed custom.
También: POST .../invite, PATCH .../members/:userId, DELETE .../members/:userId
Crea y revoca API keys con scopes. Requiere plan Pro+. Solo con JWT.
Lista claves (sin secrets).
Revoca inmediatamente. Todas las peticiones con esa key devuelven 401.
curl -X POST BASE_URL/api/apikeys \ -H "Authorization: Bearer TU_JWT" \ -H "X-Workspace-Id: WS_ID" \ -H "Content-Type: application/json" \ -d '{"name":"CI/CD","scopes":["uploads:write","videos:read"]}'
Health check sin autenticación. Retorna estado de DB, storage y cola.
{
"status": "ok",
"uptime": 86400,
"database": "connected",
"storage": "connected",
"queue": "connected"
}
La API usa paginación basada en cursor (no por página). Esto garantiza resultados consistentes incluso cuando se agregan o eliminan elementos.
nextCursor devuelto es opaco — no lo modifiques, úsalo tal cual en la siguiente petición.
1. Primera petición — sin cursor:
GET /api/videos?limit=20
2. Respuesta incluye nextCursor si hay más resultados:
{ "videos": [...], "nextCursor": "eyJpZCI6...", "hasMore": true }
3. Siguiente página — pasa el cursor:
GET /api/videos?limit=20&cursor=eyJpZCI6...
// Obtener todos los videos con paginación automática async function getAllVideos() { let cursor = null; const allVideos = []; do { const url = `BASE_URL/api/videos?limit=50` + (cursor ? `&cursor=${cursor}` : ''); const res = await fetch(url, { headers: { 'Authorization': 'Bearer sv_live_KEY', 'X-Workspace-Id': 'WS_ID' } }); const data = await res.json(); allVideos.push(...data.videos); cursor = data.nextCursor; } while (cursor); return allVideos; }
{
"videos": [ ... ],
"nextCursor": "eyJpZCI6MTIzfQ==",
"hasMore": true,
"total": 847
}
Todos los endpoints están sujetos a límites de tasa. Los headers de respuesta indican el estado actual.
| Endpoint | Límite | Ventana |
|---|---|---|
| General | 100 req | 1 minuto |
/api/upload | 10 req | 1 minuto |
/auth/* | 5 req | 1 minuto |
/api/videos/:id/views | 1 req / IP / video | 10 segundos |
analytics/export.csv | 5 req | 1 minuto |
Retry-After: N indicando cuántos segundos esperar.
HTTP/1.1 200 OK X-RateLimit-Limit: 100 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1716000060 # Cuando se excede: HTTP/1.1 429 Too Many Requests Retry-After: 43 X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0
async function fetchWithRetry(url, opts, retries = 3) { const res = await fetch(url, opts); if (res.status === 429 && retries > 0) { const wait = (res.headers.get('Retry-After') || 5) * 1000; await new Promise(r => setTimeout(r, wait)); return fetchWithRetry(url, opts, retries - 1); } return res; }
Todos los webhooks se envían como POST con Content-Type: application/json y header de firma X-Webhook-Signature: sha256=HMAC(secret,rawBody).
{
"event": "video.ready",
"timestamp": 1716000000,
"workspaceId": "ws_abc123",
"data": {
"id": "vid_xyz",
"title": "Mi Video",
"duration": 125.4,
"qualities": ["360p", "720p", "1080p"],
"m3u8Url": "https://cdn.../master.m3u8",
"thumbnailUrl": "https://cdn.../thumb.jpg"
}
}
{
"event": "transcription.complete",
"timestamp": 1716000060,
"workspaceId": "ws_abc123",
"data": {
"videoId": "vid_xyz",
"language": "es",
"duration": 125.4,
"segments": 42,
"vttUrl": "https://.../subtitles.vtt"
}
}
{
"event": "video.failed",
"timestamp": 1716000030,
"workspaceId": "ws_abc123",
"data": {
"id": "vid_xyz",
"title": "Mi Video",
"error": "Codec no soportado"
}
}
const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); }
No existe aún un SDK oficial, pero puedes usar este wrapper reutilizable directamente en tu proyecto.
lib/streamvault.js e impórtalo en toda tu app.
// lib/streamvault.js class StreamVault { constructor(apiKey, workspaceId, base = 'BASE_URL') { this.headers = { 'Authorization': `Bearer ${apiKey}`, 'X-Workspace-Id': workspaceId, 'Content-Type': 'application/json' }; this.base = base; } async req(method, path, body) { const res = await fetch(this.base + path, { method, headers: this.headers, body: body ? JSON.stringify(body) : undefined }); if (!res.ok) throw await res.json(); return res.status === 204 ? null : res.json(); } listVideos(params = {}) { const q = new URLSearchParams(params).toString(); return this.req('GET', `/api/videos?${q}`); } getVideo(id) { return this.req('GET', `/api/videos/${id}`); } updateVideo(id, data) { return this.req('PATCH', `/api/videos/${id}`, data); } deleteVideo(id) { return this.req('DELETE', `/api/videos/${id}`); } importVideo(url, title) { return this.req('POST', '/api/import', {url, title}); } listFolders() { return this.req('GET', '/api/folders'); } createWebhook(url, events) { return this.req('POST', '/api/webhooks', {url, events}); } } module.exports = StreamVault; // Uso: // const sv = new StreamVault('sv_live_KEY', 'ws_ID'); // const { videos } = await sv.listVideos({ limit: 10 });
# lib/streamvault.py import requests class StreamVault: def __init__(self, api_key, workspace_id, base="BASE_URL"): self.base = base self.headers = { "Authorization": f"Bearer {api_key}", "X-Workspace-Id": workspace_id, "Content-Type": "application/json" } def req(self, method, path, body=None): r = requests.request( method, self.base + path, headers=self.headers, json=body ) r.raise_for_status() return r.json() if r.text else None def list_videos(self, **kw): return self.req("GET", "/api/videos") def get_video(self, id): return self.req("GET", f"/api/videos/{id}") def delete_video(self, id): return self.req("DELETE", f"/api/videos/{id}") def import_video(self, url, title=None): return self.req("POST", "/api/import", {"url": url, "title": title}) # sv = StreamVault("sv_live_KEY", "ws_ID") # data = sv.list_videos()
Historial de cambios de la API. Los cambios incompatibles siempre se anuncian con antelación.