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. 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. 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. 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