Video API

Use the Video API to upload and manage video assets, captions, player settings, webhooks, and usage estimates.

View markdown

Base URL and authentication

The API base URL is:

https://rabata.io/api/v1/video

Create a Video API key from Dashboard → Video → API keys. Send it as a Bearer token with every request:

curl https://rabata.io/api/v1/video/assets \
  --header "Authorization: Bearer YOUR_VIDEO_API_KEY"

Keep the key in a secret manager and send it only over HTTPS. A missing, malformed, disabled, or unknown key returns 401 Unauthorized.

Requests are limited to 60 per minute for each authenticated account. Missing or invalid credentials consume a per-IP allowance rather than bypassing the limit. The account allowance is shared across all of its Video API keys. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (a Unix timestamp). A throttled request returns 429 Too Many Requests with Retry-After and the standard JSON error body.

Browser applications can call the API cross-origin. Preflight responses allow the Bearer Authorization, Content-Type, and Idempotency-Key headers. The API exposes the rate-limit headers to browser clients and does not use credentialed cookies.

Errors

Successful requests return JSON. Errors use this shape:

{
  "error": "A description of the problem"
}

Common statuses are:

Status Meaning
400 The request could not be parsed.
401 The Bearer token is missing or invalid.
403 The account is blocked, the Video subscription is inactive, or a metered operation has no active card or spendable promotional credit.
404 The requested resource does not exist in the authenticated account.
409 An idempotency key was reused for a different asset-create request.
415 A JSON mutation used an unsupported Content-Type.
422 A parameter, file, or resource state is invalid.
429 The request limit was exceeded.
503 A dependent storage service is temporarily unavailable.

Resource identifiers are UUIDs. A valid UUID owned by another account is treated as not found. Send Content-Type: application/json with POST, PUT, and PATCH requests, except caption uploads, which use multipart form data.

Assets

List assets

GET /api/v1/video/assets?page=1&limit=20

page starts at 1. limit defaults to 20 and is constrained to 1–100.

{
  "data": [
    {
      "id": "ASSET_UUID",
      "title": "Product tour",
      "status": "ready",
      "original_filename": "tour.mp4",
      "file_size": 73400320,
      "duration": 83.4,
      "width": 1920,
      "height": 1080,
      "thumbnail_url": "https://...",
      "allowed_domains": ["example.com"],
      "created_at": "2026-07-30T10:00:00Z",
      "embed_url": "https://rabata.io/embed/ASSET_UUID",
      "stream_url": "https://..."
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pages": 1
  }
}

Retrieve one asset with:

GET /api/v1/video/assets/ASSET_UUID

stream_url is returned only when the asset is ready and its owner has valid, funded Video access. If metered access is paused because the account has no active card, active legacy trial, or at least $0.01 of promotional credit remaining, list and show requests still return asset metadata but omit stream_url; embeds also refuse playback. Mutating requests return 403 until funding is restored.

Assets with status: "errored" include error_code and error_message in list and detail responses. These contain the stored preparation failure code and a safe explanation, for example InvalidMedia and Video could not be processed. Upload a valid video at least 360 pixels high. Either field can be null for older failures without recorded details. Other statuses omit both fields. A failed asset still returns HTTP 200 when retrieved; the error describes media preparation, not the API request.

Player settings in asset responses

Asset responses include two objects with the same six setting names:

  • player_settings: the asset's stored overrides. A null value means inherit the account default.
  • effective_player_settings: the resolved values used by the player. Asset overrides take precedence over account settings, then system defaults. Boolean values are always true or false; accent_color is a hex color.

These objects are returned in asset list, detail and update responses. For example, an asset inheriting every setting from an account using system defaults returns:

{
  "player_settings": {
    "accent_color": null,
    "autoplay": null,
    "muted": null,
    "loop_video": null,
    "show_controls": null,
    "show_title": null
  },
  "effective_player_settings": {
    "accent_color": "#28CDD9",
    "autoplay": false,
    "muted": false,
    "loop_video": false,
    "show_controls": true,
    "show_title": false
  }
}

Read effective_player_settings to use the current values without a second request. To change an override, PATCH player_settings; send null to reset that field to inheritance. Explicit false remains an override. The effective object is read-only, and subsequent requests reflect account-default changes for inherited fields.

Upload an asset

Uploads go directly to object storage with presigned multipart URLs.

  1. Create the asset and upload session:

    curl https://rabata.io/api/v1/video/assets \ --request POST \ --header "Authorization: Bearer YOUR_VIDEO_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: YOUR_UNIQUE_UPLOAD_REQUEST_ID" \ --data '{ "title": "Product tour", "original_filename": "tour.mp4", "file_size": 73400320 }'

    file_size is in bytes. Supported extensions are .mp4, .mov, .avi, .mkv, .webm, .flv, .wmv, .m4v, .3gp, and .ogv. The maximum file size is 10 GB.

    The response contains the new asset id, an upload.upload_id, the upload.chunk_size, and one presigned URL per part.

    Use a unique Idempotency-Key of 1–255 bytes for every logical create request. Retrying the same payload with the same key returns the original asset and a fresh set of presigned part URLs without creating another asset or multipart session. Reusing the key with different create parameters returns 409 Conflict.

  2. Split the file into chunks of upload.chunk_size bytes. PUT each chunk to the corresponding presigned URL in order, without the Rabata Authorization header. Save the ETag response header from every upload.

  3. Complete the upload:

    curl https://rabata.io/api/v1/video/assets/ASSET_UUID/confirm \ --request POST \ --header "Authorization: Bearer YOUR_VIDEO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "upload_id": "UPLOAD_ID", "parts": [ { "part_number": 1, "etag": "PART_1_ETAG" }, { "part_number": 2, "etag": "PART_2_ETAG" } ] }'

The asset moves through waiting, uploaded, processing, and ready. Poll the asset or listen for an asset.ready webhook before using its stream URL. A response with transcoding_started: false includes a warning and can be retried.

If the upload cannot be completed, cancel its multipart session and discard the waiting asset:

POST /api/v1/video/assets/ASSET_UUID/abort
Content-Type: application/json

{
  "upload_id": "UPLOAD_ID"
}

Cancellation remains available when the Video subscription has expired. A completed cancellation returns HTTP 200 with success: true, discarded: true, and cleanup_pending: false. If billing suspension prevents storage access, the asset is removed from the library immediately and the API returns HTTP 202 with cleanup_pending: true. Storage cleanup is retried in the background when access returns; this does not remove account restrictions. Repeating the abort request is safe.

Update or delete an asset

Update the domain allowlist or asset-specific player settings:

curl https://rabata.io/api/v1/video/assets/ASSET_UUID \
  --request PATCH \
  --header "Authorization: Bearer YOUR_VIDEO_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "allowed_domains": ["example.com", "www.example.com"],
    "player_settings": {
      "accent_color": "#28CDD9",
      "autoplay": false,
      "muted": false,
      "loop_video": false,
      "show_controls": true,
      "show_title": false
    }
  }'

An empty allowed_domains array allows embedding from any domain. At most 20 HTTPS hostnames can be supplied; enter hostnames without paths. A hostname without a port permits HTTPS port 443 only. Specify an explicit port, such as intranet.example.com:8443, to allow another HTTPS port (1–65535); wildcard ports are not supported.

*.example.com permits the main site and all of its subdomains. Restrictions are enforced by the browser's Content Security Policy (frame-ancestors), and the recommended embed_html checks the parent origin before loading the iframe so it can show an actionable message on an unlisted site. Legacy iframe-only embeds remain blank when blocked and do not count as a video view.

Delete an asset with:

DELETE /api/v1/video/assets/ASSET_UUID

Deleting an asset removes it from the API and schedules its stored media for cleanup. Deleting a waiting upload follows the cancellation behavior above, including HTTP 202 when storage cleanup is pending.

Get embed code

When the asset is ready:

GET /api/v1/video/assets/ASSET_UUID/embed

The response contains embed_url, the original iframe string, and embed_html. Use embed_html for the recommended iframe plus viewer-identity script. The dashboard's Copy embed code action copies this enhanced snippet too. The original iframe and embed_url remain available for integrations that cannot include scripts. See Viewer identity for their limits.

Manage a custom thumbnail

Request a presigned image upload:

POST /api/v1/video/assets/ASSET_UUID/thumbnail
Content-Type: application/json

{
  "filename": "poster.jpg",
  "content_type": "image/jpeg",
  "file_size": 245760
}

Upload the image to the returned url, then confirm its returned storage key:

PUT /api/v1/video/assets/ASSET_UUID/thumbnail
Content-Type: application/json

{
  "key": "RETURNED_STORAGE_KEY"
}

Remove the custom thumbnail with:

DELETE /api/v1/video/assets/ASSET_UUID/thumbnail

Captions

List or retrieve captions:

GET /api/v1/video/assets/ASSET_UUID/captions
GET /api/v1/video/assets/ASSET_UUID/captions/CAPTION_UUID

Upload a WebVTT or SubRip file as multipart form data:

curl https://rabata.io/api/v1/video/assets/ASSET_UUID/captions \
  --request POST \
  --header "Authorization: Bearer YOUR_VIDEO_API_KEY" \
  --form "file=@captions.vtt" \
  --form "label=English" \
  --form "language=en"

Caption files can be up to 2 MB. Language values use a lowercase BCP 47 tag such as en, en-us, or pt-br. Each asset can have one active caption per language and up to 30 captions.

Delete a caption with:

DELETE /api/v1/video/assets/ASSET_UUID/captions/CAPTION_UUID

Player defaults

Get the account-wide player defaults:

GET /api/v1/video/player_config

Update any subset of the settings:

PATCH /api/v1/video/player_config
Content-Type: application/json

{
  "accent_color": "#28CDD9",
  "autoplay": false,
  "muted": false,
  "loop_video": false,
  "show_controls": true,
  "show_title": false
}

Asset-specific player settings override these account defaults.

Analytics

Get views and watch-time totals plus a time series:

GET /api/v1/video/assets/ASSET_UUID/analytics?from=2026-07-01&to=2026-07-30&granularity=day

granularity can be day or hour; it defaults to day. from defaults to 30 days ago and to defaults to now.

The summary and each time-series point contain separate unique_viewers and playback_sessions values. Unique viewers are deduplicated across the selected time range, not calculated by adding daily unique counts. The library dashboard also deduplicates a viewer who watches multiple videos belonging to one customer.

Viewer identity

The player uses a new session ID on each page load. Pause/resume does not start another view on that page. A separate persistent viewer ID lets a returning browser count once across multiple sessions. Anonymous browser IDs estimate viewers: shared browsers, clearing storage, expiry, and browser restrictions affect that estimate. They do not identify a person across devices.

The recommended embed_html includes /video-viewer.js. This script stores a random ID in the embedding site's first-party local storage, scoped to the Rabata origin and customer. IDs expire after one year. Existing iframe-only embeds use storage inside the iframe, additionally scoped to the embedding origin; browsers may partition or block that storage. If storage or the expected bridge is unavailable, playback and session analytics continue without a viewer ID. Those plays are not counted as unique viewers.

For signed-in viewers, set data-viewer-id on the generated iframe before loading it. Use a stable, opaque ID from your application, never an email address, name, username, or other personal detail. Accepted IDs contain 1–128 letters, digits, underscores, or hyphens. A customer-supplied ID can identify the same viewer across devices and sites within that customer's library.

Keep the generated data-viewer-scope unchanged; the placeholder below means the customer UUID already included in embed_html.

<iframe data-rabata-video data-viewer-id="u_7bd123e9"
  data-viewer-scope="CUSTOMER_UUID_FROM_EMBED_HTML"
  data-rabata-src="https://rabata.io/embed/ASSET_UUID?viewer_bridge=1"
  width="640" height="360" allow="autoplay; fullscreen" allowfullscreen></iframe>
<script defer src="https://rabata.io/video-viewer.js"></script>

The script verifies the embedding origin, loads the iframe when it is allowed, and passes the ID directly to the iframe with origin-checked messages; do not put viewer IDs in URLs. Rabata stores a customer-scoped keyed hash, not the supplied ID. Anonymous and customer-supplied IDs use different namespaces. These are pseudonymous analytics identifiers, not authentication credentials.

To disable anonymous persistence, add disable_cookies=true to the iframe URL and data-disable-cookies="true" to the enhanced iframe. Despite the option's name, it disables the player's local-storage persistence too. Apply it before loading the iframe, and connect this setting to your site's consent controls. An explicit data-viewer-id still identifies a signed-in viewer; omit it as well when you do not want persistent viewer identification. To change identity or these options, reload the iframe so the current session never mixes viewers.

Historical events without a viewer ID remain playback-session data and are not backfilled as unique users. The unique-viewer metric covers identified plays recorded after viewer tracking was enabled; it can therefore be lower than playback sessions. It counts viewer IDs from view starts, not every heartbeat.

Get the audience-retention curve for the same period:

GET /api/v1/video/assets/ASSET_UUID/retention?from=2026-07-01&to=2026-07-30

Usage and pricing

Retrieve the current calendar month's completed usage and estimated charges:

GET /api/v1/video/usage

To retrieve a previous month, pass month in YYYY-MM format:

GET /api/v1/video/usage?month=2026-08

Omitting month selects the current month. Invalid or future months return HTTP 400. Both data and summary refer to the selected month.

summary.status is estimated for the current month, pending for a past month without a finalized billing snapshot, and finalized when a snapshot exists. Finalized responses use the usage, rates, and costs saved when the invoice was created, even if today's prices or subscription have changed. invoice_id identifies the associated invoice, or is null when none is attached.

For compatibility, estimated_cost and estimated_total retain their names in finalized responses. They then contain the saved Video charges before promotional credit, tax, and invoice-level rounding; other services on the same invoice are not included. source_storage.current_bytes is live storage for the current month and null for historical months; average_bytes describes the selected period.

The data array retains its existing record format. The summary object contains the usage-pricing contract:

{
  "data": [
    {
      "period_start": "2026-07-31T00:00:00Z",
      "period_end": "2026-07-31T23:59:59Z",
      "storage_bytes": 1200000000,
      "egress_bytes": 410000000
    }
  ],
  "summary": {
    "period_start": "2026-07-01",
    "period_end": "2026-07-31",
    "data_through": "2026-07-30",
    "status": "estimated",
    "invoice_id": null,
    "currency": "usd",
    "source_storage": {
      "current_bytes": 1200000000,
      "average_bytes": "1000000000.000000",
      "rate_per_tb_month": "6.000000",
      "rate_per_gb_month": "0.006000",
      "estimated_cost": "0.0060"
    },
    "delivery": {
      "720p": {
        "seconds": "6000.000",
        "minutes": "100.0",
        "rate_per_minute": "0.000600",
        "estimated_cost": "0.0600"
      },
      "1080p": {
        "seconds": "0.000",
        "minutes": "0.0",
        "rate_per_minute": "0.000800",
        "estimated_cost": "0.0000"
      },
      "2k": {
        "seconds": "0.000",
        "minutes": "0.0",
        "rate_per_minute": "0.001300",
        "estimated_cost": "0.0000"
      },
      "4k": {
        "seconds": "0.000",
        "minutes": "0.0",
        "rate_per_minute": "0.002500",
        "estimated_cost": "0.0000"
      }
    },
    "estimated_total": "0.0660",
    "transcoding": {
      "standard_on_demand_included": true
    }
  }
}

rate_per_tb_month is the canonical source-storage price. The deprecated rate_per_gb_month field remains temporarily for existing clients and expresses the equivalent rate in the API's legacy decimal-GB unit.

delivery always contains the 720p, 1080p, 2k, and 4k rate tiers. data_through is the last contiguous completed UTC day, or null when no day is ready. For a finalized month, it equals period_end. Current-month values are estimates; invoice charges are finalized only after every billable day in the month is complete.

Webhooks

Create a public HTTP or HTTPS callback for any combination of asset.created, asset.ready, asset.errored, and asset.deleted. Use HTTPS in production:

curl https://rabata.io/api/v1/video/webhooks \
  --request POST \
  --header "Authorization: Bearer YOUR_VIDEO_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "callback_url": "https://example.com/rabata-webhooks",
    "events": ["asset.ready", "asset.errored"]
  }'

The callback hostname must resolve only to public IP addresses. An empty events array subscribes to all supported events. Each account may register a normalized callback URL only once. Host casing, default HTTP/HTTPS ports, and a trailing slash do not make separate webhook registrations. The create response returns the signing secret once; store it in your secret manager because later responses do not expose it.

List, retrieve, update, or delete webhook configurations with:

GET    /api/v1/video/webhooks
GET    /api/v1/video/webhooks/WEBHOOK_UUID
PATCH  /api/v1/video/webhooks/WEBHOOK_UUID
DELETE /api/v1/video/webhooks/WEBHOOK_UUID

Delivery requests use Content-Type: application/json, identify the event in X-Rabata-Event, and include an HMAC-SHA256 digest in X-Rabata-Signature. Respond with a 2xx status after accepting the event. Failed deliveries are retried with backoff.

The asset.errored payload includes error_code and error_message alongside event, asset_id, status, and timestamp. These use the same stored preparation details as asset responses and can be null when no details were recorded. The error is captured when the event is queued, so delivery retries retain the original explanation. Other webhook events omit these fields.