본문으로 건너뛰기
개발자

NULX API로 만드세요.

내 스토리지에 영상을 올리고, 재생 준비까지 상태를 추적하고, 서명된 재생 URL을 발급받는 테넌트 범위 REST API입니다.

공유 링크와 사이트 삽입 고르기

재생 주소를 쓰기 전에 영상별 공개 플레이어 주소, 보호된 공유 링크, 지정된 사이트 배포처를 조회하세요. 두 조회 API에는 assets:read 권한이 필요합니다.

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

사이트 배포처의 생성·수정은 계정 소유자가 대시보드에서 합니다. ?delivery=slug 삽입은 영상 범위·허용 도메인·언어·통계를 구분하지만 웹사이트를 배포하지는 않습니다. 일시정지하면 해당 삽입은 410을 반환합니다.

공개 링크는 고정 주소입니다. 보호된 공유 링크에는 PIN과 만료 기간을 둘 수 있습니다. 서명 삽입 주소에는 만료되는 서버 발급 토큰이 필요하므로 고정 삽입 코드로 저장하지 마세요. 허용 도메인은 iframe 위치만 제한합니다.

HLS 주소만 받는 외부 플레이어는 계정 소유자가 영상 → 공유 → 외부 플랫폼에 연결에서 외부 HLS 링크를 관리할 수 있습니다. 상대 플랫폼과 시청자가 재생 주소를 볼 수 있습니다.

공유 방식 비교하기

인증

헤더 하나, 범위가 정해진 키.

/api/v1 요청에는 키를 함께 보냅니다. oEmbed는 키 없이 호출합니다. 키는 대시보드에서 만들며 필요한 범위만 담을 수 있습니다.

요청 예시

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

사용 가능한 범위

assets:read 영상 자산·컬렉션·분석을 조회합니다.
assets:write 업로드 세션을 만들고 자산을 등록·수정합니다.
playback:read 재생 URL과 서명된 재생 토큰을 발급받습니다.
billing:write 테넌트의 청구 예상액을 조회합니다.
api_keys:read 기존 API 키 목록을 조회합니다.
api_keys:write API 키를 만들고 폐기합니다.
webhooks:read 웹훅 엔드포인트 목록을 조회합니다.
webhooks:write 웹훅 엔드포인트를 만들고 일시중지·삭제합니다.

API 키는 유료 플랜에서 제공됩니다. 무료 플랜은 키 화면을 볼 수 있지만 발급은 불가합니다.

키는 서버에만 두세요. 브라우저 JavaScript나 앱 번들에 API 키를 넣지 마세요 — 브라우저는 백엔드가 반환한 단기 재생 URL을 쓸 수 있지만, 직접 발급하면 안 됩니다.

빠른 시작

업로드에서 재생까지, 호출 세 번.

  1. 1. 업로드 시작

    사전 서명된 PUT URL을 요청합니다. 응답에는 upload_url, upload_headers, 업로드 id가 들어 있습니다.

    curl -X POST https://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,"playback_policy":"public"}'
  2. 2. 파일을 PUT 한 뒤 완료 처리

    받은 헤더 그대로 upload_url에 원본 바이트를 보낸 뒤, complete를 호출해 트랜스코딩을 큐에 넣습니다.

    curl -X PUT "<upload_url>" \
      -H "Content-Type: video/mp4" \
      --upload-file launch.mp4
    
    curl -X POST https://nulx.dev/api/v1/uploads/<id>/complete \
      -H "X-API-Key: $NULX_API_KEY"
  3. 3. 준비되면 재생 정보 조회

    playback_status가 ready가 될 때까지 자산을 폴링하거나 video.asset.ready 웹훅을 등록한 뒤, 재생 토큰을 요청합니다.

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

위 예시는 공개 재생용입니다. 수강생에게만 보여줄 강의 영상은 보호 재생 설정을 확인하세요. 강의 플랫폼에 연결하기

자동 자막 생성·편집·다국어 번역 API

업로드 시작 요청에 processing_options를 추가하세요. 실제 파일 전송과 업로드 완료 요청은 위와 같습니다. 기본값은 자동 자막 꺼짐이며 source_language는 auto 또는 ko, en 같은 언어 코드입니다.

{"filename":"lecture.mp4","bytes_expected":73400320,"content_type":"video/mp4","processing_options":{"auto_captions":true,"source_language":"auto","duration_seconds":600}}

AI 크레딧이 포함된 요금제에서는 작업 시작 전에 크레딧을 예약할 수 있도록 auto_captions와 함께 duration_seconds(영상 길이, 초)가 필요합니다. 대시보드는 자동으로 보냅니다.

영상 처리가 완료되면 자막 목록에서 트랙 ID를 얻습니다. GET으로 WebVTT 원문을 읽고 PATCH로 문장·시간을 저장한 뒤, 해당 트랙을 원하는 언어로 번역합니다. 읽기는 assets:read, 쓰기는 assets:write 권한이 필요합니다.

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

curl -X PATCH https://nulx.dev/api/v1/assets/<asset_id>/subtitles/<track_id> \
  -H "X-API-Key: $NULX_API_KEY" -H "Content-Type: application/json" \
  -d '{"content":"WEBVTT\n\n00:00:00.000 --> 00:00:03.000\nHello, everyone.\n"}'

curl -X POST https://nulx.dev/api/v1/assets/<asset_id>/subtitles/<track_id>/translate \
  -H "X-API-Key: $NULX_API_KEY" -H "Content-Type: application/json" \
  -d '{"target_language":"ko","label":"한국어"}'

번역 요청은 202와 대상 트랙을 반환합니다. 목록의 translation_state(completed/failed)와 진행률을 확인하세요. 실행 중인 같은 언어의 재요청·수정·삭제는 409, 잘못된 자막은 422, 다른 계정의 자막은 404입니다. 번역은 원문 자막을 사용하므로 음성을 다시 분석하지 않습니다.

자막·번역 사용량 요금 확인

강의 플랫폼에 연결하기

강사는 강의 사이트에서 영상을 올리고, 수강생은 그 사이트에서 시청합니다. NULX API 키는 강의 플랫폼 서버에만 보관하세요.

  1. 강사가 올릴 수 있는지 확인

    플랫폼 서버가 강사 권한을 확인하고 assets:write 키로 업로드를 시작합니다. 한 번에 보낼 파일은 initiate, 큰 파일이나 이어 올리기는 initiate_multipart를 사용하세요. 고객 R2 연결을 쓰려면 storage_connection_id를 지정합니다. 유료 강의는 playback_policy를 signed로 지정하세요.

  2. R2로 파일 보내기

    기본 제공 R2에는 플랫폼 서버가 발급받은 단기 주소로 파일을 전송하세요. 고객 R2의 CORS에서 강의 사이트 출처의 PUT과 업로드 요청 헤더를 허용하고 ETag 응답 헤더를 노출하면 강사 브라우저에서 직접 보낼 수 있습니다. 전송 후 NULX 완료 API를 호출해야 화질별 변환이 시작됩니다.

  3. 영상이 준비되면 강의에 연결

    video.asset.ready 웹훅이나 자산 조회로 준비 완료를 확인하고, NULX 영상 ID를 해당 강사의 강의에 연결합니다. 강사별 영상 소유권은 강의 플랫폼에서 검사하세요.

  4. 수강생에게만 재생 주소 발급

    재생 요청마다 플랫폼 서버가 수강 권한을 확인한 뒤 별도 playback:read 키로 재생 정보를 받아 허용된 수강생에게만 embed_url을 전달하세요. 기본 15분 토큰이 만료되면 서버에서 새 주소를 발급받아 플레이어를 다시 열어야 합니다. 새 embed_url의 t 쿼리에 이전 시청 초를 넣으면 그 위치에서 시작합니다. API 키나 원본 다운로드 주소를 브라우저에 보내지 마세요.

{"filename":"lecture.mp4","bytes_expected":73400320,"playback_policy":"signed"}

업로드·변환 상태 표시

파일 전송률은 강사 화면에서 직접 측정하세요. NULX는 video.upload.completed·video.upload.failed, video.job.queued·video.job.started·video.job.failed, video.asset.ready·video.asset.errored 웹훅으로 상태 변화를 알립니다. 작업 실패는 재시도될 수 있고 영상 오류는 이번 처리에서 재생할 수 없다는 뜻입니다. 변환 중에는 플랫폼 서버가 assets:read 키로 업로드 응답의 숫자 asset_id를 조회하면 작업 상태와 진행률을 받을 수 있습니다. 진행률은 작업자가 보고한 값이며 재시도 시 다시 낮아질 수 있습니다.

GET /api/v1/assets/<asset_id>
{"playback_status":"processing","processing_stage":"transcoding","transcoding_job":{"status":"running","progress_percent":42}}

JavaScript SDK로 R2에 업로드

아래 /lessons/42/... 주소는 강의 사이트 서버에 직접 만들 예시입니다. SDK는 그 서버에서 받은 짧은 업로드 주소로만 파일을 보내며, 브라우저에 NULX API 키를 두지 않습니다.

<script src="https://nulx.dev/player/nulx-player-dd7597fd0f3742ce.js?locale=ko"
  integrity="sha384-9sVWZ35V6gt1rYswpaB09YVGjGfA3BOvHgdZ7CLbDFy3fMEKJB3VNmRhw2319SCU" crossorigin="anonymous" async></script>
<input id="lesson-file" type="file" accept="video/*">
<p id="upload-status" role="status"></p>
<script type="module">
await customElements.whenDefined("nulx-player")
const post = async (path, body) => {
  const headers = { "Content-Type": "application/json" }
  const csrf = document.querySelector('meta[name="csrf-token"]')?.content
  if (csrf) headers["X-CSRF-Token"] = csrf
  const response = await fetch(path, {
    method: "POST", credentials: "same-origin", headers, body: JSON.stringify(body)
  })
  if (!response.ok) {
    const error = new Error("Request failed: " + response.status)
    error.status = response.status
    throw error
  }
  return response.json()
}
document.querySelector("#lesson-file").addEventListener("change", async (event) => {
  const file = event.target.files[0]
  if (!file) return
  const status = document.querySelector("#upload-status")
  try {
    await NulxSDK.uploadMultipart(file, {
      create: (file) => post("/lessons/42/uploads", { filename: file.name, bytes_expected: file.size, content_type: file.type || "application/octet-stream" }),
      signPart: ({ uploadId, partNumber, contentLength }) => post(`/lessons/42/uploads/${uploadId}/parts`, { part_number: partNumber, content_length: contentLength }),
      complete: ({ uploadId, bytesReceived, parts }) => post(`/lessons/42/uploads/${uploadId}/complete`, { bytes_received: bytesReceived, parts }),
      onProgress: ({ percent }) => { status.textContent = percent + "%" }
    })
    status.textContent = "업로드를 마쳤어요. 영상 변환이 시작됩니다."
  } catch (_error) {
    status.textContent = "영상을 올리지 못했어요. 다시 시도해 주세요."
  }
})
</script>

강의 사이트 서버는 로그인·강사 소유권·CSRF를 확인하고, NULX의 initiate_multipart에 signed 정책과 Idempotency-Key를 직접 지정하세요. 이어 sign_part와 complete_multipart를 대신 호출해 필요한 값만 브라우저에 돌려줍니다. SDK는 R2의 s3_multipart 업로드를 지원합니다. R2 CORS에 강의 사이트의 PUT과 ETag 응답 헤더가 필요합니다. 단기 업로드 주소는 브라우저에 보이므로 기록·공유하지 마세요. 업로드가 실패하면 서버에서 같은 세션으로 재개하거나 abort_multipart로 정리하세요.

수강생 플레이어 상태 받기

SDK는 위의 버전 고정·무결성 검증 스크립트에서 함께 제공됩니다. 이전에 복사한 플레이어 코드에는 SDK가 없을 수 있습니다. 강의 사이트의 로그인 세션으로 재생 정보를 요청하고, 갱신할 때는 lesson.renew()를 호출하면 이전 시청 위치에서 이어집니다.

<div id="lesson-player"></div>
<p id="lesson-status" role="status"></p>
<script type="module">
await customElements.whenDefined("nulx-player")
const host = document.querySelector("#lesson-player")
const status = document.querySelector("#lesson-status")
host.addEventListener("nulx:state", ({ detail }) => {
  // detail.state: idle, loading, ready, buffering, playing, paused, ended, error
  status.textContent = {
    loading: "영상을 불러오는 중…",
    buffering: "재생을 준비하는 중…",
    error: "영상을 재생하지 못했어요. 다시 시도해 주세요."
  }[detail.state] || ""
})
host.addEventListener("nulx:progress", ({ detail }) => {
  // detail.currentTime and detail.duration are seconds
})
const lesson = NulxSDK.player(host, async () => {
  const response = await fetch("/lessons/42/playback", { credentials: "same-origin" })
  if (!response.ok) throw new Error("Playback denied: " + response.status)
  return response.json() // { asset_id, playback_policy: "signed", embed_url }
})
try { await lesson.load() }
catch (_error) { status.textContent = "영상을 재생하지 못했어요. 다시 시도해 주세요." }
// Refresh from your backend before the token expires; watch position is kept.
// await lesson.renew()
</script>

강의 사이트 서버는 매 요청마다 수강 권한을 확인하고 별도 playback:read 키로 NULX 재생 정보를 발급받으세요. 브라우저에는 asset_id, signed playback_policy, 단기 embed_url만 보내세요. SDK는 다른 출처·공개 재생 주소를 거부하지만, 허가된 시청자가 받은 단기 주소의 복사나 화면 녹화까지 막지는 못합니다.

Cloudflare R2 CORS 설정 방법

서명 재생은 지원 플랜과 저장소의 보호 재생 설정이 필요합니다. 원본 버킷을 공개하면 보호 재생을 우회할 수 있습니다. 임베드 도메인에는 강의 사이트 주소를 등록하세요.

도메인 설정은 다른 사이트의 iframe 삽입을 막습니다. 허가된 브라우저에 전달된 단기 재생 URL은 만료 전 복사할 수 있습니다.

NULX 플레이어에는 다운로드 버튼이 없지만, 재생 가능한 영상은 화면 녹화나 네트워크 저장을 완전히 막을 수 없습니다. assets:write 키는 원본 다운로드 API에도 접근하므로 서버 밖에 노출하지 마세요.

외부 플레이어에 시청자별 재생 주소 주기

다른 회사의 웹 플레이어(hls.js, Video.js 등)나 자체 앱이 NULX 영상을 직접 재생할 때 씁니다. 로그인한 시청자가 재생을 누를 때마다 플랫폼 서버가 playback:read 키로 주소를 새로 받아 그 시청자의 플레이어에만 넘기세요. 비공개·보호 재생 영상만 됩니다.

  • 처음 재생한 곳(같은 인터넷 연결)에서만 재생돼요. 다른 곳에서 열면 막혀요.
  • 영상 길이 + 30분 동안만 쓸 수 있어요 (최소 2시간, 최대 24시간).
  • 다른 회사 웹사이트의 플레이어와 자체 앱 모두에서 재생돼요.
  • 와이파이에서 데이터로 바뀌는 등 연결이 바뀌면 주소를 다시 받아야 해요.
POST /api/v1/playback/<asset_id>/grants
X-API-Key: <playback:read key>
{"viewer":"student-1024"}

201 {"grant_id":"0123456789abcdef","hls_url":"https://cdn.nulx.dev/_nulx_edge/grant-…/master.m3u8",
     "expires_at":"…","ttl_seconds":7200,"binding":"first_network","viewer":"student-1024"}

DELETE /api/v1/playback/<asset_id>/grants/<grant_id>

공개 영상은 422 public_video, 직접 연결한 저장소의 영상은 422 storage_unsupported로 거절됩니다. 다른 곳에서 재생하면 영상 요청이 403이 되며, 그때는 새 주소를 받으면 됩니다. 영상을 휴지통에 넣거나 재생할 수 없게 바꾸거나 키를 지우면 그 주소도 바로 멈춥니다.

oEmbed API

공개 영상 URL을 제목·썸네일·iframe 코드가 담긴 JSON으로 변환합니다. API 키 없이 호출할 수 있습니다.

oEmbed는 처리 완료된 공개 영상에서만 사용할 수 있어요. 비공개·보호 재생·공개 예정 영상과 보호 공유 링크는 지원하지 않아요.

요청 예시

curl --get https://nulx.dev/oembed \
  --data-urlencode "url=https://nulx.dev/watch/<external_id>" \
  --data-urlencode "format=json" \
  --data-urlencode "maxwidth=640"

<external_id>를 영상의 외부 ID로 바꾸세요. 영상 상세 → 공유 → 웹사이트에 영상 넣기 → 개발자용 연동 보기에서 해당 영상의 요청 주소를 복사할 수 있습니다.

요청 파라미터

url
필수. 같은 호스트의 /watch/<external_id> 또는 /embed/<external_id> URL. 쿼리 값은 URL 인코딩하세요. /s/ 보호 공유 링크와 HLS 주소는 지원하지 않습니다.
format
선택. json만 지원하며 생략해도 JSON을 반환합니다.
maxwidth
선택. 기본 너비 640px. 양의 정수는 80~1920px로 보정하며 16:9 비율로 높이를 계산합니다.
maxheight
선택. 기본 높이 360px. 양의 정수는 80~1080px로 보정하며 현재 높이보다 작으면 너비도 함께 줄입니다.

성공 응답 예시

{
  "version": "1.0",
  "type": "video",
  "provider_name": "NULX",
  "provider_url": "https://nulx.dev/",
  "title": "...",
  "author_name": "...",
  "width": 640,
  "height": 360,
  "html": "<iframe src=\"https://nulx.dev/embed/<external_id>\" width=\"640\" height=\"360\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture; encrypted-media\" allowfullscreen title=\"...\"></iframe>",
  "thumbnail_url": "https://nulx.dev/watch/<external_id>/poster"
}

포스터가 있는 영상만 thumbnail_url을 반환합니다. 공개 시청·임베드 페이지의 application/json+oembed 링크로 API를 자동 발견할 수 있으며, 실제 자동 임베드 지원 여부는 사용하는 서비스에 따라 다릅니다.

오류 응답은 본문이 비어 있습니다. 404: URL 누락·잘못된 호스트/경로·없는 영상·비공개·보호 재생·처리 중·공개 예정 영상. 501: JSON 이외의 형식. 429: IP당 분당 60회 제한 초과. 임베드 도메인 제한은 그대로 적용됩니다.

레퍼런스

공개 엔드포인트 전체.

아래 OpenAPI 문서와 같은 원본에서 생성한 표라 어긋날 수 없습니다.

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/trash List videos in the trash
POST /api/v1/assets/{id}/trash Move a video to the trash
POST /api/v1/assets/{id}/restore Restore a video from the trash
POST /api/v1/assets/{id}/retry Retry encoding after a failure
PATCH /api/v1/assets/{id}/playback_policy Change who can play a video
GET /api/v1/assets/{id}/distribution Choose a sharing or embed method for one video
GET /api/v1/assets/{video_asset_id}/share_links List live share links for a video
POST /api/v1/assets/{video_asset_id}/share_links Create a share link
DELETE /api/v1/assets/{video_asset_id}/share_links/{id} Stop a share link
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
GET /api/v1/deliveries List the tenant's site embed destinations

Collections

PATCH /api/v1/collections/{id}/playback_policy Protect a folder and every video in it
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

POST /api/v1/assets/{video_asset_id}/subtitle_requests Queue caption generation or translation with a durable idempotent receipt
GET /api/v1/assets/{video_asset_id}/subtitle_requests Recover a request by client idempotency key
GET /api/v1/assets/{video_asset_id}/subtitle_requests/{id} Poll a subtitle processing attempt without reading caption storage
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
GET /api/v1/assets/{video_asset_id}/subtitles/{id} Read a subtitle track and its editable WebVTT content
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/mcp_upload_requests Save a recoverable upload intent and attempt multipart admission
GET /api/v1/mcp_upload_requests/{id} Read a saved upload request and actual source-upload state
POST /api/v1/mcp_upload_requests/{id}/resume Retry admission for the original saved upload intent
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

Storage Connections

GET /api/v1/storage_connections List the caller's storage connections without credentials
POST /api/v1/storage_connections/{id}/check Run a scoped read-only provider check
GET /api/v1/storage_connections/cloudflare_options Discover the owner's approved Cloudflare accounts and zones
POST /api/v1/storage_connections/setup_r2 Provision dedicated protected R2 storage after explicit owner consent

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/credits Read the account's NULX AI credit wallet and fixed purchase pack
POST /api/v1/billing/credits/checkout Create or replay a hosted checkout for 1000 NULX AI credits
GET /api/v1/billing/credits/purchases Recover an AI credit purchase by idempotency key
GET /api/v1/billing/credits/purchases/{id} Read a purchase belonging to the authenticated account
GET /api/v1/billing/estimate Estimate the current billing period charges

Usage

GET /api/v1/setup/status Nonsecret current account setup and protection counts
GET /api/v1/usage/plan This month's encoding use and when anything is charged beyond the monthly fee
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 /oembed Get embed information for a public video
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/{id}/grants Issue a per-viewer HLS address for an external player (web or app)
DELETE /api/v1/playback/{id}/grants/{grant_id} Stop a per-viewer address
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

AI workflows

POST /api/v1/source_imports Import a public HTTPS video
GET /api/v1/source_imports/{id} Read URL import state
POST /api/v1/source_imports/{id}/resume Resume a retryable source import
POST /api/v1/assets/{video_asset_id}/caption_workflows Generate captions then translations automatically
GET /api/v1/assets/{video_asset_id}/caption_workflows/{id} getCaptionWorkflow
POST /api/v1/assets/{video_asset_id}/caption_workflows/{id}/resume resumeCaptionWorkflow

오류

REST API 오류 응답

/api/v1 실패는 error와 code 쌍을 반환합니다. 메시지가 아니라 code로 판별하세요.

형태

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

주요 코드

unauthorized API 키가 없거나, 잘못됐거나, 만료됐습니다.
forbidden_scope 키는 유효하지만 필요한 범위가 없습니다.
ip_not_allowed 키는 유효하지만 허용 IP 목록 밖의 주소에서 호출됐습니다. 응답의 observed_ip가 NULX가 본 주소입니다.
parameter_missing 필수 파라미터가 없거나 잘못됐습니다.
playback_token_required 서명 재생 자산입니다 — 먼저 토큰을 요청하세요.
rate_limited 요청이 너무 많습니다. 잠시 후 다시 시도하세요.
not_found 내 테넌트에 해당 리소스가 없습니다.
internal_server_error 서버 측 문제입니다 — 재시도하거나 문의하세요.

웹훅

폴링 대신 구독하세요.

대시보드나 API로 HTTPS 엔드포인트를 등록하면 수명주기 이벤트를 서명된 JSON으로 보내드립니다. 5xx·429·전송 오류는 백오프로 재시도하고, 그 외 4xx·3xx 응답은 재시도하지 않습니다.

전달 형식

POST https://your-server.example/hooks/nulx
NULX-Event: video.asset.ready
NULX-Event-Id: <event uuid>
NULX-Delivery-Id: <delivery uuid>
NULX-Timestamp: 1758432000
NULX-Signature: t=1758432000,v1=<hex hmac-sha256>

{
  "id": "<event uuid>",
  "event": "video.asset.ready",
  "created_at": "2026-09-21T04:00:00Z",
  "data": { "asset_id": "…", "title": "…", "playback_status": "ready", "playback_url": "…" }
}

서명 검증

엔드포인트 생성 시 한 번만 표시되는 secret으로 "<timestamp>.<raw body>"의 HMAC-SHA256(hex)을 계산해 v1과 비교하세요. 재전송 공격을 막으려면 5분 이상 오래된 timestamp는 거부하세요.

expected = "t=" + timestamp + ",v1=" + hex_hmac_sha256(secret, timestamp + "." + raw_body)

구독 가능한 이벤트

video.asset.ready
video.asset.errored
tenant.traffic.anomaly
video.upload.completed
video.upload.failed
video.job.queued
video.job.started
video.job.failed
video.subtitle.ready
video.subtitle.failed
video.asset.deleted
video.track.ready

OpenAPI

계약 전체를 가져가세요.

공개 표면 전체의 OpenAPI 3.0 문서입니다 — Swagger UI, Postman, 클라이언트 생성기에 바로 넣을 수 있습니다.

API에 대해 궁금한 점이 있나요? 문의하기