Skip to main content
Developers

Build on the NULX API.

Upload video into your own storage, track it until playback is ready, and serve signed playback URLs — all over a tenant-scoped REST API.

Authentication

One header, scoped keys.

Send your key on every request. Keys are created in the dashboard and can carry only the scopes you need.

Example request

curl https://api.nulx.dev/api/v1/assets \
  -H "X-API-Key: $NULX_API_KEY"

Available scopes

assets:read List and read video assets, collections, and analytics.
assets:write Create upload sessions, register assets, and edit metadata.
playback:read Fetch playback URLs and signed playback tokens.
billing:write Read billing estimates for your tenant.
api_keys:read List existing API keys.
api_keys:write Create and revoke API keys.
webhooks:read List webhook endpoints.
webhooks:write Create, pause, and delete webhook endpoints.

API keys are available on paid plans. Free-plan accounts see the key screen but cannot issue keys.

Keep keys on your server. Never put an API key in browser JavaScript or a mobile bundle — a browser may play the short-lived URLs your backend returns, but it must not mint them.

Quickstart

Upload to playback in three calls.

  1. 1. Initiate the upload

    Ask for a presigned PUT URL. The response includes upload_url, upload_headers, and the upload id.

    curl -X POST https://api.nulx.dev/api/v1/uploads/initiate \
      -H "X-API-Key: $NULX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"filename":"launch.mp4","bytes_expected":73400320}'
  2. 2. PUT the file, then complete

    Send the raw bytes to upload_url with the returned headers, then mark the upload complete to enqueue transcoding.

    curl -X PUT "<upload_url>" \
      -H "Content-Type: video/mp4" \
      --upload-file launch.mp4
    
    curl -X POST https://api.nulx.dev/api/v1/uploads/<id>/complete \
      -H "X-API-Key: $NULX_API_KEY"
  3. 3. Fetch playback when ready

    Poll the asset until playback_status is ready, or register a webhook for video.asset.ready. Then request a playback token.

    curl https://api.nulx.dev/api/v1/playback/<asset_id> \
      -H "X-API-Key: $NULX_API_KEY"

Reference

Every public endpoint.

Generated from the same OpenAPI document served below — it cannot drift.

Tenants

GET /api/v1/tenants List tenants visible to the caller

Video Assets

GET /api/v1/assets List video assets for the caller's tenant
POST /api/v1/assets Create a video asset directly (out-of-band ingestion)
GET /api/v1/assets/export Export asset metadata (CSV or JSON)
GET /api/v1/assets/graph_export No-lock-in asset graph export (JSON)
GET /api/v1/assets/duplicates Duplicate detection lookup (desktop uploader 중복 알림)
POST /api/v1/assets/import Bulk import asset metadata from CSV
GET /api/v1/assets/{id} Fetch a single video asset
PATCH /api/v1/assets/{id} Update a video asset's title, description, or collection
PUT /api/v1/assets/{id} Update a video asset's title, description, or collection (PUT alias)
PATCH /api/v1/assets/{id}/tags Add or update schemaless tags on an asset
DELETE /api/v1/assets/{id}/tags/{key} Remove a tag from an asset
GET /api/v1/assets/{id}/download Get a short-lived signed download URL for the original source
GET /api/v1/assets/{id}/deovr_manifest.json Get a DeoVR/HereSphere deep-link manifest for a publicly playable asset
GET /api/v1/analytics Tenant-wide viewer analytics summary
GET /api/v1/assets/{id}/analytics Per-asset viewer analytics summary

Collections

GET /api/v1/collections List collections (folders) for the caller's tenant
POST /api/v1/collections Create a collection (folder) for organizing assets
GET /api/v1/collections/tree Nested collection tree for the tenant
PATCH /api/v1/collections/{id} Update a collection
PUT /api/v1/collections/{id} Update a collection (PUT alias)
DELETE /api/v1/collections/{id} Delete a collection

Subtitles

GET /api/v1/assets/{video_asset_id}/subtitles List subtitle/caption tracks for a video asset
POST /api/v1/assets/{video_asset_id}/subtitles Upload a subtitle/caption track for a video asset
PATCH /api/v1/assets/{video_asset_id}/subtitles/{id} Update a subtitle/caption track
PUT /api/v1/assets/{video_asset_id}/subtitles/{id} Update a subtitle/caption track (PUT alias)
DELETE /api/v1/assets/{video_asset_id}/subtitles/{id} Remove a subtitle/caption track
POST /api/v1/assets/{video_asset_id}/subtitles/{id}/sync Re-time a subtitle/caption track against a reference track
POST /api/v1/assets/{video_asset_id}/subtitles/{id}/translate Create a new language track by machine-translating an existing one

Tracks

GET /api/v1/assets/{video_asset_id}/tracks List timecode-anchored tracks for a video asset
POST /api/v1/assets/{video_asset_id}/tracks Create or replace an asset track (idempotent upsert)
PUT /api/v1/assets/{video_asset_id}/tracks Create or replace an asset track (idempotent upsert, PUT alias)
GET /api/v1/assets/{video_asset_id}/tracks/{id} Show a single asset track
PATCH /api/v1/assets/{video_asset_id}/tracks/{id} Update an asset track by id
PUT /api/v1/assets/{video_asset_id}/tracks/{id} Update an asset track by id (PUT alias)
DELETE /api/v1/assets/{video_asset_id}/tracks/{id} Remove an asset track

Uploads

POST /api/v1/uploads/initiate Create a tenant-scoped direct upload session
POST /api/v1/uploads/initiate_multipart Initiate a resumable multipart source upload
POST /api/v1/uploads/{id}/sign_part Presign a single S3 multipart part PUT
POST /api/v1/uploads/{id}/complete_multipart Finalize a multipart/resumable upload
POST /api/v1/uploads/{id}/abort_multipart Abort a multipart/resumable upload
GET /api/v1/uploads/{id} Fetch an upload session's status
POST /api/v1/uploads/{id}/complete Mark an upload complete and enqueue transcoding

API Keys

GET /api/v1/api_keys List API keys for the caller's tenant
POST /api/v1/api_keys Issue a new API key for a tenant
DELETE /api/v1/api_keys/{id} Revoke an API key

Device Tokens

POST /api/v1/device_tokens Redeem a desktop pairing code for a device token
DELETE /api/v1/device_tokens/current Revoke the API key used for this request

Billing

GET /api/v1/billing/estimate Estimate the current billing period charges

Usage

GET /api/v1/usage/summary Current-month usage and billing estimate per tenant

Encoding

GET /api/v1/encoding_spec Canonical transcoding encoding specification

Playback

GET /api/v1/assets/{id}/deovr_manifest.json Get a DeoVR/HereSphere deep-link manifest for a publicly playable asset
GET /api/v1/playback/{id} Get playback URLs and a signed playback token for an asset
POST /api/v1/playback_events Ingest a viewer playback beacon (public, unauthenticated)

Ingest Batches

GET /api/v1/ingest_batches/desktop_storage Resolve the server-managed desktop upload storage
POST /api/v1/ingest_batches Create a storage ingest batch lease
GET /api/v1/ingest_batches/{id} Fetch ingest batch status
POST /api/v1/ingest_batches/{id}/manifest/preview Validate an ingest manifest without mutation
POST /api/v1/ingest_batches/{id}/manifest/commit Commit a client encoder manifest to assets
POST /api/v1/ingest_batches/{id}/cancel Cancel an uncommitted ingest batch
POST /api/v1/ingest_batches/{id}/uploads Create a server-managed desktop object upload
GET /api/v1/ingest_batches/{id}/uploads List upload intents recorded for a desktop batch (resume/이어올리기)
PUT /api/v1/ingest_batches/{id}/uploads/{upload_id}/proxy Stream a small object through the control plane to Bunny
PUT /api/v1/ingest_batches/{id}/uploads/{upload_id}/proxy_parts/{part_number} Stage one part of a chunked Bunny server-proxy upload
POST /api/v1/ingest_batches/{id}/uploads/{upload_id}/proxy_complete Assemble and commit a chunked Bunny server-proxy upload
POST /api/v1/ingest_batches/{id}/uploads/{upload_id}/parts Sign one exact R2 multipart part
POST /api/v1/ingest_batches/{id}/uploads/{upload_id}/parts/{part_number}/complete Durably record one confirmed multipart part etag (resume/이어올리기)
POST /api/v1/ingest_batches/{id}/uploads/{upload_id}/complete Complete an R2 multipart upload
DELETE /api/v1/ingest_batches/{id}/uploads/{upload_id} Abort an active R2 multipart upload

Webhooks

GET /api/v1/webhooks List webhook endpoints for the caller's tenant(s)
POST /api/v1/webhooks Register a new webhook endpoint for a tenant
DELETE /api/v1/webhooks/{id} Delete a webhook endpoint
PATCH /api/v1/webhooks/{id}/active Activate or deactivate a webhook endpoint

Errors

Errors are JSON, always.

Failures return a stable error and code pair — match on code, not on the message.

Shape

{
  "error": "insufficient_scope",
  "code": "forbidden_scope",
  "required_scopes": ["assets:write"]
}

Common codes

unauthorized Missing, invalid, or expired API key.
forbidden_scope The key is valid but lacks the required scope.
parameter_missing A required parameter is absent or invalid.
playback_token_required The asset uses signed playback — request a token first.
rate_limited Too many requests; back off and retry.
not_found No such resource in your tenant.
internal_server_error Something broke on our side — retry or contact support.

Webhooks

Subscribe instead of polling.

Register an HTTPS endpoint and NULX posts signed JSON events — video.asset.ready, video.upload.failed, and the rest of the lifecycle. Each delivery is HMAC-signed; the spec documents the verification recipe.

OpenAPI

Take the whole contract.

The full OpenAPI 3.0 document for the public surface — import it into Swagger UI, Postman, or a client generator.

Questions about the API? Contact us