VideohatiDocs
SDKs

Node.js SDK

Official @videohati/node SDK — create a client, upload video, create playback sessions, manage webhooks, and handle errors from a Node.js server.

@videohati/node is the official server SDK. It is ESM, ships zero runtime dependencies, and is typed against the public OpenAPI 3.1 spec at https://api.videohati.com/openapi.json. Version 1.0.0-rc.0.

Install

Node.js 18 or newer. The package is ESM only — use import, not require.

npm install @videohati/node

Create a client

Create one client and reuse it. The mode (live or test) always follows the key prefix — a vh_test_ key runs against test data.

import { Videohati } from "@videohati/node";

const videohati = new Videohati({
  apiKey: process.env.VIDEOHATI_API_KEY, // vh_live_... or vh_test_...
});

VideohatiOptions:

OptionDefaultWhat it does
apiKeyProject API key (vh_live_... or vh_test_...).
env"live" | "test" — asserts the key prefix matches.
baseUrlhttps://api.videohati.comAPI origin.
maxRetries2Retries for idempotent requests, 429s, and 5xxs.
timeoutMs30000Per-attempt timeout in milliseconds.
fetchglobal fetchCustom fetch implementation.

Never ship an API key to a browser. Anyone can read it from page source and use your account. For playback, create the session server-side and hand only the session token to the page.

Authentication

The client authenticates with a project API key sent as a bearer token. Use a vh_live_ key in production and a vh_test_ key against test data — the SDK derives the mode from the prefix. You create and manage keys in the dashboard.

Videos

The videos resource covers the whole lifecycle: create, upload, read, and manage thumbnails.

MethodSignatureWhat it does
createvideos.create({ originalFilename, projectId? })Registers a video. Returns { videoId, state: "uploading" }. Step 1.
listvideos.list({ limit?, cursor?, state?, projectId? })One page of videos: { data, nextCursor? }.
getvideos.get(videoId, { projectId? })Full detail, including upload and encoding projections.
deletevideos.delete(videoId, { projectId? })Soft-deletes the video; aborts any active upload.
setThumbnailvideos.setThumbnail(videoId, { data, contentType, projectId? })Sets a custom thumbnail (JPEG/PNG/WebP, at most 5 MB).
resetThumbnailvideos.resetThumbnail(videoId, { projectId? })Reverts to the automatic poster frame.
startUploadvideos.startUpload(videoId, { sizeBytes, contentType, checksumSha256?, idempotencyKey?, projectId? })Opens a multipart upload; returns part size, part count, and the part URL template. Step 2.
completeUploadvideos.completeUpload(videoId, { checksumSha256?, projectId? })Finalizes the upload. Step 3.
abortUploadvideos.abortUpload(videoId, { projectId? })Cancels an open upload.
uploadvideos.upload(file, options?)Runs create → start → part uploads → complete in one call.
waitForStatevideos.waitForState(videoId, { targetStates?, intervalMs?, timeoutMs?, projectId? })Polls until the video reaches a target state.

One-call upload

videos.upload(file, options) runs the full multipart flow in a single call. file is a path, a Buffer/Uint8Array, or a Blob/File.

const uploaded = await videohati.videos.upload("./lecture-01.mp4", {
  onProgress: (p) => console.log(`${p.uploadedBytes}/${p.totalBytes} bytes`),
});

// Poll until the video is playable end to end:
const video = await videohati.videos.waitForState(uploaded.videoId);
console.log(video.state); // "ready"

UploadOptions:

OptionDefaultWhat it does
projectIdIgnored under API-key auth (the key is already project-scoped).
filenamefile basename, else upload.binStored original filename.
contentTypeinferred from extensionMIME type sent to the server.
checksumSha256Lowercase hex SHA-256 of the whole file; the server verifies it.
idempotencyKeyMakes the upload resume-safe across restarts.
onProgress(progress) => void with uploadedBytes, totalBytes, partsCompleted, totalParts.
partConcurrency4Parts uploaded in parallel.
maxPartAttempts3Attempts per part.
videoIdReuse an already-created video instead of creating one.
signalAbortSignal to cancel the upload.
fetchglobal fetchCustom fetch for the part uploads.

waitForState defaults to targetStates: ["ready", "failed"], intervalMs: 3000, and timeoutMs: 900000 (15 minutes); it rejects with a VideohatiError on timeout.

Read and delete

const { data } = await videohati.videos.list({ state: "ready" });

const detail = await videohati.videos.get(videoId);
console.log(detail.state); // uploading | uploaded | encoding | ready | failed

await videohati.videos.delete(videoId); // soft delete; aborts any active upload

Playback

Create the session on your server and hand only sessionToken to the page (embed it with @videohati/player or @videohati/react).

MethodSignatureWhat it does
createSessionplayback.createSession({ videoId, ttlSeconds?, referrerAllowlist?, viewerIdFromRequest?, viewerDisplayText?, projectId? })Creates a session server-side; returns the sessionToken for the player.

Your app calls only createSession; the player performs the other playback calls itself with the session token.

createSession parameters:

ParameterDefaultWhat it does
videoIdrequiredThe video to play.
ttlSecondsserver defaultSession lifetime.
referrerAllowlistRestrict playback to these referrers.
viewerIdFromRequestOpaque viewer identifier for analytics and forensics.
viewerDisplayTextText drawn into the on-video watermark.
projectIdIgnored under API-key auth (the key is already project-scoped).
const session = await videohati.playback.createSession({
  videoId,
  viewerDisplayText: "Ahmed K.",
});
console.log(session.sessionToken);

Webhooks

Register endpoints, inspect deliveries, and verify signatures. Full flow in the webhooks guide.

MethodSignatureWhat it does
registerwebhooks.register(projectId, { url, description? })Registers an endpoint. secret is returned once.
listwebhooks.list(projectId)Lists endpoints.
getwebhooks.get(projectId, id)One endpoint.
updatewebhooks.update(projectId, id, { url?, description?, enabled? })Updates an endpoint.
deletewebhooks.delete(projectId, id)Soft-deletes an endpoint.
rotateSecretwebhooks.rotateSecret(projectId, id)Mints a fresh secret (returned once).
reopenCircuitwebhooks.reopenCircuit(projectId, id)Returns a tripped endpoint to service.
sendTestwebhooks.sendTest(projectId, id)Queues a synthetic video.encoding.ready delivery.
listDeliverieswebhooks.listDeliveries(projectId, id, { limit?, cursor?, state?, eventType? })One page of delivery attempts.
verifywebhooks.verify({ payload, header, secret, toleranceSeconds? })Verifies a signature and returns the parsed envelope.

Verify a signature

verifyWebhookSignature is also exported standalone. Pass the raw request body — not re-serialized JSON.

import { verifyWebhookSignature, SIGNATURE_HEADER } from "@videohati/node";

app.post("/webhooks/videohati", (req, res) => {
  const { valid, event } = verifyWebhookSignature({
    payload: req.rawBody, // string or Uint8Array, exactly as received
    header: req.headers[SIGNATURE_HEADER], // "videohati-signature"
    secret: process.env.VIDEOHATI_WEBHOOK_SECRET,
  });
  if (!valid) return res.status(400).end();
  if (event?.type === "video.encoding.ready") {
    // The video is ready — create a playback session, notify a user, etc.
  }
  res.status(200).end();
});

The header is Videohati-Signature: t=<unix_ts>,v1=<hex_sha256> (HMAC-SHA256 over <unix_ts>.<raw_body>); verification is constant-time and rejects timestamps more than 300 seconds old (tune with toleranceSeconds).

Projects and API keys

You create and manage projects and API keys in the dashboard, not through the SDK. See Concepts for how accounts, projects, and keys relate.

Errors

Every non-2xx response throws a typed subclass of VideohatiError, which carries status and code (the API's machine code, for example video_not_found). Errors never contain your API key.

ClassStatusWhen
VideohatiConnectionErrorThe request never reached the API (DNS, TLS, socket).
VideohatiTimeoutErrorThe request was aborted after timeoutMs. Subclass of the connection error.
VideohatiValidationError400The body or query failed validation.
VideohatiAuthenticationError401Missing or invalid credential.
VideohatiPermissionError403Valid credential, insufficient permission.
VideohatiNotFoundError404Resource missing or not visible to this caller.
VideohatiConflictError409Conflicts with the resource's current state.
VideohatiRateLimitError429Rate limit exceeded; read retryAfterSeconds.
VideohatiServerError5xxThe API failed to complete the request.
import {
  VideohatiError,
  VideohatiNotFoundError,
  VideohatiRateLimitError,
} from "@videohati/node";

try {
  await videohati.videos.get(videoId);
} catch (error) {
  if (error instanceof VideohatiNotFoundError) {
    // 404 — error.code === "video_not_found"
  } else if (error instanceof VideohatiRateLimitError) {
    await sleep((error.retryAfterSeconds ?? 1) * 1000);
  } else if (error instanceof VideohatiError) {
    console.error(error.status, error.code, error.message);
  }
}

GET and DELETE requests, 429s, and 5xxs retry with exponential backoff capped at 8 seconds — 2 retries by default. A 429 honors the Retry-After header. Tune the behavior with maxRetries and timeoutMs.