Referencia
URL base: https://api.xonaplay.com. La lista de endpoints de abajo se genera
a partir de nuestro archivo OpenAPI, /openapi.json.
Autenticación
| Credencial | Cómo | Dónde |
|---|---|---|
| Secret key | Authorization: Bearer xp_sk_… |
Todos los endpoints de esta página |
| Ninguna | — | /playback de contenido público en Open o Protected |
Una key actúa sobre todo su workspace. Creá, listá y revocá keys en el
dashboard, en Configuración → API. Una key revocada responde
401 INVALID_KEY.
Convenciones
- JSON de entrada y salida, siempre en snake_case.
- Éxito:
{ "status": "ok", "data": … }. Las listas suman"meta": { "total": 120, "page": 1, "per_page": 20, "last_page": 6 }. - Error:
{ "status": "error", "code": "NOT_FOUND", "message": "…" }. Los códigos van en mayúsculas y son estables; los mensajes son para humanos y pueden cambiar. - Los errores de validación son
422conerrors: [{ "field": "title", "message": "…" }]. - Ids: todo
{id}acepta elidUUID o elshort_id. - Timestamps: ISO 8601 en UTC.
- Paginación:
?page=1&per_page=20(máximo 100).
Objetos de video y de transmisiones en vivo
GET /v1/videos/{id} y GET /v1/live/{id} devuelven el mismo objeto que usa
nuestro propio dashboard — no se le saca nada a la API pública.
links.playeres la página pública de reproducción (la que embebés en un iframe). No es un archivo de video.links.hlssolo funciona en Open; en cualquier otro nivel está protegido y responde403 TOKEN_REQUIREDsin token. Para reproducir en tu propio player, llamá siempre a/playback.custody,engine,compute_engineystorage_profiledescriben dónde guarda y renderiza XonaPlay el video (storage propio o tuyo, qué engine lo procesó). Los podés ignorar para la mayoría de las integraciones.playback_securityes el override por-video del default del workspace:inherit,open,protected,strictomaximum.deliveryeshls(default) uoriginal: si XonaPlay renderiza HLS desde el original sin tocar. Ver Original o HLS.
Límites de uso
| Quién llama | Por minuto |
|---|---|
| Secret key | 600 requests |
| Cookie de sesión | 300 requests |
/playback sin key |
60 por IP |
Si pasás el límite recibís 429 RATE_LIMITED con un header Retry-After (en
segundos) que te dice cuánto esperar antes del próximo pedido. El bucket de
600/min solo aplica a una secret key válida; una inválida o revocada cae al
límite más bajo de IP anónima.
Webhooks
Registrá un endpoint HTTPS en Configuración → API → Webhooks (o con
POST /v1/webhooks). Nosotros hacemos POST de:
{
"id": "evt_5c1f0e7a9b2d4c3e",
"type": "video.ready",
"created_at": "2026-10-09T01:30:00.000Z",
"data": {
"video": {
"id": "0f8e2c7a-…",
"short_id": "9KfR2mQx",
"title": "Lesson 1",
"status": "ready",
"duration_sec": 3600
}
}
}| Evento | Cuándo |
|---|---|
video.ready |
Terminó la codificación; el video ya se puede reproducir |
video.failed |
La codificación falló definitivamente |
live.started |
Una transmisión en vivo salió al aire |
live.ended |
Una transmisión en vivo terminó |
live.started y live.ended viajan bajo otra clave: data.live (un objeto
de transmisión en vivo — id, short_id, name, status, …), no
data.video:
{
"id": "evt_8a2d4c3e9b2d4c3e",
"type": "live.started",
"created_at": "2026-10-09T01:32:00.000Z",
"data": {
"live": {
"id": "3b7f0c1a-…",
"short_id": "7qLp3Zxa",
"name": "Weekly class",
"status": "live"
}
}
}- Firmado con
X-Xona-Signature: t=<epoch en segundos>,v1=<hex>dondev1es HMAC-SHA256 de"<t>.<body crudo>"con el secreto de tu endpoint (whsec_…, se muestra una sola vez). Ver la receta de verificación. - Respondé cualquier 2xx dentro de 10 segundos. Cualquier otra cosa se reintenta después de 1 min, 5 min, 30 min, 2 h y 6 h; después el envío queda marcado como fallido.
- Los envíos pueden llegar más de una vez o desordenados: usá
idpara deduplicar, y volvé a leer el video si el orden importa.
Videos
List, read, update and delete videos, and get a signed playback link.
GET /v1/videos
List videos
Videos of the authenticated workspace, merged from every engine (platform custody and BYO), newest first. Paginated.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
page |
query | string | No | |
per_page |
query | string | No | |
q |
query | string | No | |
status |
query | string | No |
Responses
200— Paginated list of videos.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).
{
"status": "",
"data": [
{
"id": "",
"short_id": "",
"title": "",
"slug": "",
"description": "",
"status": "",
"visibility": "",
"playback_security": "",
"delivery": "",
"source_type": "",
"duration_sec": "",
"width": "",
"height": "",
"size_original_bytes": "",
"size_processed_bytes": "",
"qualities": [
""
],
"custody": "",
"engine": "",
"compute_engine": "",
"storage_profile": "",
"links": {
"player": "",
"hls": "",
"poster": ""
},
"metadata": "",
"created_at": "",
"updated_at": ""
}
],
"meta": {
"total": "",
"page": "",
"per_page": "",
"last_page": ""
}
}GET /v1/videos/{video_id}
Get a video
Accepts either the id (UUID) or the short_id.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes |
Responses
200— The video.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND).
{
"status": "",
"data": {
"id": "",
"short_id": "",
"title": "",
"slug": "",
"description": "",
"status": "",
"visibility": "",
"playback_security": "",
"delivery": "",
"source_type": "",
"duration_sec": "",
"width": "",
"height": "",
"size_original_bytes": "",
"size_processed_bytes": "",
"qualities": [
""
],
"custody": "",
"engine": "",
"compute_engine": "",
"storage_profile": "",
"links": {
"player": "",
"hls": "",
"poster": ""
},
"metadata": "",
"created_at": "",
"updated_at": ""
}
}PATCH /v1/videos/{video_id}
Update a video
Partial update. playback_security overrides the workspace default for this video only; it is rejected on BYO-engine videos (NOT_SUPPORTED_FOR_BYO) and maximum requires the staff opt-in for the workspace (MAXIMUM_NOT_ALLOWED).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes |
Request body
title: stringslug: stringvisibility: objectplayback_security: objectdelivery: stringmetadata: object
Responses
200— The updated video.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND).422— Validation failed,playback_security=maximumwithout the workspace opt-in (MAXIMUM_NOT_ALLOWED), or an override on a BYO video (NOT_SUPPORTED_FOR_BYO).
{
"status": "",
"data": {
"id": "",
"short_id": "",
"title": "",
"slug": "",
"description": "",
"status": "",
"visibility": "",
"playback_security": "",
"delivery": "",
"source_type": "",
"duration_sec": "",
"width": "",
"height": "",
"size_original_bytes": "",
"size_processed_bytes": "",
"qualities": [
""
],
"custody": "",
"engine": "",
"compute_engine": "",
"storage_profile": "",
"links": {
"player": "",
"hls": "",
"poster": ""
},
"metadata": "",
"created_at": "",
"updated_at": ""
}
}DELETE /v1/videos/{video_id}
Delete a video
only_original=true keeps the processed renditions and deletes only the source file.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes | |
only_original |
query | string | No |
Responses
200— Deleted.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND).
{
"status": "",
"data": {
"deleted": true,
"only_original": true
}
}GET /v1/videos/{video_id}/download
Download the original file
Short-lived signed link (1 hour) to the original exactly as it was uploaded or imported — never transcoded. Requires a secret key or a session.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes |
Responses
200— Download link.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND), or its original was deleted (ORIGINAL_NOT_AVAILABLE).409— The original is still uploading (VIDEO_NOT_READY).422— The video lives on your own server (NOT_SUPPORTED_FOR_BYO).
{
"status": "",
"data": {
"url": "",
"expires_at": "",
"file_name": "",
"size_bytes": ""
}
}GET /v1/videos/{video_id}/tracks
List audio and subtitle tracks
Tracks that came with the original file (origin: source) plus the ones added through the API (origin: manual). supported is false when tracks cannot be added to this video, with the reason in unsupported_reason.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes |
Responses
200— Tracks.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND).
{
"status": "",
"data": {
"supported": true,
"unsupported_reason": "",
"tracks": [
{
"id": "",
"kind": "",
"origin": "",
"lang": "",
"label": "",
"status": "",
"error": "",
"created_at": ""
}
]
}
}POST /v1/videos/{video_id}/tracks/audio
Add an audio track
Step 1 of 2. Creates the track (uploading) and returns a pre-signed PUT for the audio file (mp3, m4a, aac, wav, flac, ogg or opus; up to 1 GiB). Upload the file, then call POST /videos/{video_id}/tracks/{track_id}/complete.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes |
Request body
lang: string (required)label: string (required)file_name: string (required)size_bytes: object (required)content_type: string (required)
Responses
200— Track and upload URL.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND).409— The video does not accept tracks yet (TRACKS_NOT_SUPPORTED: not HLS, still processing, or on your own server), or the per-video limit was reached (TRACK_LIMIT_REACHED).422— Invalidlang/label, unsupported file type or size (UNSUPPORTED_AUDIO_FORMAT,AUDIO_TOO_LARGE).
{
"status": "",
"data": {
"track": {
"id": "",
"kind": "",
"origin": "",
"lang": "",
"label": "",
"status": "",
"error": "",
"created_at": ""
},
"upload": {
"url": "",
"method": "",
"headers": "",
"expires_at": ""
}
}
}POST /v1/videos/{video_id}/tracks/subtitles
Add a subtitle track
Send the text of an .srt or .vtt file (max 2 MB) in content. SRT is converted to WebVTT. The track is ready immediately.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes |
Request body
lang: string (required)label: string (required)content: string (required)
Responses
200— The new track.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video with that id or short_id (NOT_FOUND).409— The video does not accept tracks yet (TRACKS_NOT_SUPPORTED: not HLS, still processing, or on your own server), or the per-video limit was reached (TRACK_LIMIT_REACHED).413— The subtitle file is larger than 2 MB (SUBTITLE_TOO_LARGE).422— Invalidlang,labelor subtitle content (INVALID_LANG,INVALID_SUBTITLE).
{
"status": "",
"data": {
"track": {
"id": "",
"kind": "",
"origin": "",
"lang": "",
"label": "",
"status": "",
"error": "",
"created_at": ""
}
}
}DELETE /v1/videos/{video_id}/tracks/{track_id}
Delete a track you added
Only origin: manual tracks can be deleted.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes | |
track_id |
path | string | Yes |
Responses
200— Deleted.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video or track with that id (NOT_FOUND).409— The video does not accept tracks yet (TRACKS_NOT_SUPPORTED: not HLS, still processing, or on your own server), or the per-video limit was reached (TRACK_LIMIT_REACHED).
{
"status": "",
"data": {
"deleted": true
}
}POST /v1/videos/{video_id}/tracks/{track_id}/complete
Complete an audio track upload
Step 2 of 2. Verifies the uploaded file and starts processing (processing → ready or failed). Safe to call twice.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes | |
track_id |
path | string | Yes |
Responses
200— The track.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No video or track with that id (NOT_FOUND).409— The file is missing or its size does not match (UPLOAD_MISSING,UPLOAD_SIZE_MISMATCH), or the video does not accept tracks.503— No processing server is available right now (NO_ENGINE_AVAILABLE).
{
"status": "",
"data": {
"track": {
"id": "",
"kind": "",
"origin": "",
"lang": "",
"label": "",
"status": "",
"error": "",
"created_at": ""
}
}
}Uploads
Open a direct upload session, PUT the parts to storage, then confirm.
POST /v1/videos/imports
Import a video from a URL
XonaPlay downloads the file from source_url server-to-server (redirects allowed, public HTTPS only) and stores it untouched as the original — your server never handles the bytes. With delivery: "original" the video is ready as soon as the download finishes; with "hls" (default) rendering starts then. You get video.ready or video.failed.
Request body
title: string (required)source_url: string (required)file_name: stringdelivery: objectstorage_profile_id: stringcompute_engine_id: string
Responses
200— Import queued.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).402— Plan limit reached (PLAN_LIMIT).422— Invalid or not allowedsource_url(VALIDATION_FAILED).
{
"status": "",
"data": {
"video_id": "",
"import_id": "",
"status": ""
}
}POST /v1/videos/uploads
Create an upload session
Starts a multipart direct upload straight to storage: creates the video (status uploading) and returns one pre-signed PUT URL per part.
Request body
title: string (required)file_name: string (required)size_bytes: number (required)content_type: stringstorage_profile_id: stringcompute_engine_id: stringdelivery: object
Responses
200— Upload session created.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).422— Validation failed (e.g. missingsize_bytes).
{
"status": "",
"data": {
"video_id": "",
"session_id": "",
"part_size": "",
"urls": [
""
],
"max_height": ""
}
}DELETE /v1/videos/uploads/{session_id}
Abort an upload session
Cancels the multipart upload and deletes the pending video.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
session_id |
path | string | Yes |
Responses
200— Aborted.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No upload session with that id (NOT_FOUND).
{
"status": "",
"data": {
"aborted": true
}
}POST /v1/videos/uploads/{session_id}/complete
Complete an upload session
Closes the multipart upload (completes it in storage), verifies the total size against what was declared and the plan limits, and queues the video for processing.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
session_id |
path | string | Yes |
Request body
parts: arraypart_number: number (required)etag: string (required)
Responses
200— The upload was completed and the video queued.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No upload session with that id (NOT_FOUND).409— The session is already closed, or the uploaded size exceeds what was declared or the plan storage limit.
{
"status": "",
"data": {
"video_id": "",
"status": ""
}
}Live
List live streams and get a signed playback link for one that is on air.
GET /v1/live
List live streams
Live streams of the authenticated workspace, merged from every engine (platform custody and BYO), newest first. Paginated.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
page |
query | string | No | |
per_page |
query | string | No | |
q |
query | string | No |
Responses
200— Paginated list of live streams.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).
{
"status": "",
"data": [
{
"id": "",
"short_id": "",
"name": "",
"slug": "",
"status": "",
"visibility": "",
"playback_security": "",
"stream_key": "",
"ingest": {
"primary": "",
"backup": ""
},
"dvr_enabled": true,
"dvr_window_minutes": "",
"replay_enabled": true,
"replay_unlisted": true,
"viewers": "",
"total_views": "",
"custody": "",
"engine": "",
"storage_profile": "",
"links": {
"player": "",
"hls": "",
"poster": ""
},
"last_live_at": "",
"created_at": "",
"updated_at": ""
}
],
"meta": {
"total": "",
"page": "",
"per_page": "",
"last_page": ""
}
}GET /v1/live/{stream_id}
Get a live stream
Accepts either the id (UUID) or the short_id.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
stream_id |
path | string | Yes |
Responses
200— The live stream.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No live stream with that id or short_id (NOT_FOUND).
{
"status": "",
"data": {
"id": "",
"short_id": "",
"name": "",
"slug": "",
"status": "",
"visibility": "",
"playback_security": "",
"stream_key": "",
"ingest": {
"primary": "",
"backup": ""
},
"dvr_enabled": true,
"dvr_window_minutes": "",
"replay_enabled": true,
"replay_unlisted": true,
"viewers": "",
"total_views": "",
"custody": "",
"engine": "",
"storage_profile": "",
"links": {
"player": "",
"hls": "",
"poster": ""
},
"last_live_at": "",
"created_at": "",
"updated_at": ""
}
}Playback
Signed HLS/embed links for a video or live stream — the only way to play content.
GET /v1/live/{stream_id}/playback
Get signed playback links for a live stream
The ONLY way to play a live stream: returns a signed hls_url/embed_url pair whose shape depends on the workspace playback security level. No credential required for public open/protected content; otherwise a secret key or session is required. On a fatal player error, call this again (see the "Reload" rule in the guide) instead of reusing the old URL.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
stream_id |
path | string | Yes | |
viewer_id |
query | string | No |
Responses
200— Signed playback links.401— A secret key or session is required for this content (KEY_REQUIRED).403— Domain not allowed (DOMAIN_NOT_ALLOWED), workspace unavailable (WORKSPACE_UNAVAILABLE), or viewer banned (ACCESS_DENIED).404— No live stream with that id or short_id (NOT_FOUND).409— The stream is not live right now (LIVE_NOT_ACTIVE); retry later.429— Too many requests; retry afterRetry-After(RATE_LIMITED).
{
"status": "",
"data": {
"stream_id": "",
"short_id": "",
"hls_url": "",
"poster_url": "",
"embed_url": "",
"expires_at": "",
"security": "",
"status": ""
}
}GET /v1/videos/{video_id}/playback
Get signed playback links for a video
The ONLY way to play a video: returns a signed hls_url/embed_url pair whose shape depends on the workspace/video playback security level (open/protected/strict/maximum). No credential required for public open/protected content; otherwise a secret key or session is required. On a fatal player error, call this again (see the "Reload" rule in the guide) instead of reusing the old URL.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
video_id |
path | string | Yes | |
viewer_id |
query | string | No |
Responses
200— Signed playback links.401— A secret key or session is required for this content (KEY_REQUIRED).403— Domain not allowed (DOMAIN_NOT_ALLOWED), workspace unavailable (WORKSPACE_UNAVAILABLE), or viewer banned (ACCESS_DENIED).404— No video with that id or short_id (NOT_FOUND).409— The video is still processing (VIDEO_NOT_READY); retry later.429— Too many requests; retry afterRetry-After(RATE_LIMITED).
{
"status": "",
"data": {
"video_id": "",
"short_id": "",
"hls_url": "",
"original_url": "",
"poster_url": "",
"thumbnails_url": "",
"embed_url": "",
"expires_at": "",
"security": "",
"delivery": "",
"duration_sec": ""
}
}Webhooks
Register endpoints for video and live events, and inspect deliveries.
GET /v1/webhooks
List webhook endpoints
Webhook endpoints registered for the workspace (secrets never come back in full).
Responses
200— Webhook endpoints.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).
{
"status": "",
"data": [
{
"id": "",
"url": "",
"description": "",
"secret_last4": "",
"events": [
""
],
"enabled": true,
"disabled_by_staff": true,
"last_success_at": "",
"last_failure_at": "",
"created_at": "",
"updated_at": ""
}
]
}POST /v1/webhooks
Create a webhook endpoint
Registers a new endpoint. The signing secret (whsec_…) is returned ONCE, in this response — store it, it cannot be retrieved again.
Request body
url: string (required)description: stringevents: array (required)
Responses
200— Endpoint created, with its signing secret.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).409— Workspace already has the maximum number of endpoints (ENDPOINT_LIMIT_REACHED).422— Validation failed (badurlorevents).
{
"status": "",
"data": {
"endpoint": {
"id": "",
"url": "",
"description": "",
"secret_last4": "",
"events": [
""
],
"enabled": true,
"disabled_by_staff": true,
"last_success_at": "",
"last_failure_at": "",
"created_at": "",
"updated_at": ""
},
"secret": ""
}
}PATCH /v1/webhooks/{id}
Update a webhook endpoint
Partial update. An endpoint disabled by staff cannot be re-enabled by the workspace.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | Yes |
Request body
url: stringdescription: stringevents: arrayenabled: boolean
Responses
200— The updated endpoint.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).403— This endpoint was disabled by staff and cannot be re-enabled (DISABLED_BY_STAFF).404— No webhook endpoint with that id (NOT_FOUND).422— Validation failed (badurlorevents).
{
"status": "",
"data": {
"id": "",
"url": "",
"description": "",
"secret_last4": "",
"events": [
""
],
"enabled": true,
"disabled_by_staff": true,
"last_success_at": "",
"last_failure_at": "",
"created_at": "",
"updated_at": ""
}
}DELETE /v1/webhooks/{id}
Delete a webhook endpoint
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | Yes |
Responses
200— Deleted.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No webhook endpoint with that id (NOT_FOUND).
{
"status": "",
"data": {
"deleted": true
}
}GET /v1/webhooks/{id}/deliveries
List delivery attempts
Delivery history for one endpoint, newest first. Paginated.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | Yes | |
page |
query | string | No | |
per_page |
query | string | No |
Responses
200— Paginated deliveries.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No webhook endpoint with that id (NOT_FOUND).
{
"status": "",
"data": [
{
"id": "",
"event": "",
"status": "",
"attempts": "",
"last_status_code": "",
"last_error": "",
"created_at": "",
"delivered_at": ""
}
],
"meta": {
"total": "",
"page": "",
"per_page": "",
"last_page": ""
}
}POST /v1/webhooks/{id}/test
Send a test event
Delivers a webhook.test event to the endpoint right away, so you can check your receiver.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | Yes |
Responses
200— The test delivery.401— Missing or invalid credential (KEY_REQUIRED,INVALID_KEY).404— No webhook endpoint with that id (NOT_FOUND).
{
"status": "",
"data": {
"id": "",
"event": "",
"status": "",
"attempts": "",
"last_status_code": "",
"last_error": "",
"created_at": "",
"delivered_at": ""
}
}