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/nodeCreate 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:
| Option | Default | What it does |
|---|---|---|
apiKey | — | Project API key (vh_live_... or vh_test_...). |
env | — | "live" | "test" — asserts the key prefix matches. |
baseUrl | https://api.videohati.com | API origin. |
maxRetries | 2 | Retries for idempotent requests, 429s, and 5xxs. |
timeoutMs | 30000 | Per-attempt timeout in milliseconds. |
fetch | global fetch | Custom 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.
| Method | Signature | What it does |
|---|---|---|
create | videos.create({ originalFilename, projectId? }) | Registers a video. Returns { videoId, state: "uploading" }. Step 1. |
list | videos.list({ limit?, cursor?, state?, projectId? }) | One page of videos: { data, nextCursor? }. |
get | videos.get(videoId, { projectId? }) | Full detail, including upload and encoding projections. |
delete | videos.delete(videoId, { projectId? }) | Soft-deletes the video; aborts any active upload. |
setThumbnail | videos.setThumbnail(videoId, { data, contentType, projectId? }) | Sets a custom thumbnail (JPEG/PNG/WebP, at most 5 MB). |
resetThumbnail | videos.resetThumbnail(videoId, { projectId? }) | Reverts to the automatic poster frame. |
startUpload | videos.startUpload(videoId, { sizeBytes, contentType, checksumSha256?, idempotencyKey?, projectId? }) | Opens a multipart upload; returns part size, part count, and the part URL template. Step 2. |
completeUpload | videos.completeUpload(videoId, { checksumSha256?, projectId? }) | Finalizes the upload. Step 3. |
abortUpload | videos.abortUpload(videoId, { projectId? }) | Cancels an open upload. |
upload | videos.upload(file, options?) | Runs create → start → part uploads → complete in one call. |
waitForState | videos.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:
| Option | Default | What it does |
|---|---|---|
projectId | — | Ignored under API-key auth (the key is already project-scoped). |
filename | file basename, else upload.bin | Stored original filename. |
contentType | inferred from extension | MIME type sent to the server. |
checksumSha256 | — | Lowercase hex SHA-256 of the whole file; the server verifies it. |
idempotencyKey | — | Makes the upload resume-safe across restarts. |
onProgress | — | (progress) => void with uploadedBytes, totalBytes, partsCompleted, totalParts. |
partConcurrency | 4 | Parts uploaded in parallel. |
maxPartAttempts | 3 | Attempts per part. |
videoId | — | Reuse an already-created video instead of creating one. |
signal | — | AbortSignal to cancel the upload. |
fetch | global fetch | Custom 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 uploadPlayback
Create the session on your server and hand only sessionToken to the page
(embed it with @videohati/player or @videohati/react).
| Method | Signature | What it does |
|---|---|---|
createSession | playback.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:
| Parameter | Default | What it does |
|---|---|---|
videoId | required | The video to play. |
ttlSeconds | server default | Session lifetime. |
referrerAllowlist | — | Restrict playback to these referrers. |
viewerIdFromRequest | — | Opaque viewer identifier for analytics and forensics. |
viewerDisplayText | — | Text drawn into the on-video watermark. |
projectId | — | Ignored 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.
| Method | Signature | What it does |
|---|---|---|
register | webhooks.register(projectId, { url, description? }) | Registers an endpoint. secret is returned once. |
list | webhooks.list(projectId) | Lists endpoints. |
get | webhooks.get(projectId, id) | One endpoint. |
update | webhooks.update(projectId, id, { url?, description?, enabled? }) | Updates an endpoint. |
delete | webhooks.delete(projectId, id) | Soft-deletes an endpoint. |
rotateSecret | webhooks.rotateSecret(projectId, id) | Mints a fresh secret (returned once). |
reopenCircuit | webhooks.reopenCircuit(projectId, id) | Returns a tripped endpoint to service. |
sendTest | webhooks.sendTest(projectId, id) | Queues a synthetic video.encoding.ready delivery. |
listDeliveries | webhooks.listDeliveries(projectId, id, { limit?, cursor?, state?, eventType? }) | One page of delivery attempts. |
verify | webhooks.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.
| Class | Status | When |
|---|---|---|
VideohatiConnectionError | — | The request never reached the API (DNS, TLS, socket). |
VideohatiTimeoutError | — | The request was aborted after timeoutMs. Subclass of the connection error. |
VideohatiValidationError | 400 | The body or query failed validation. |
VideohatiAuthenticationError | 401 | Missing or invalid credential. |
VideohatiPermissionError | 403 | Valid credential, insufficient permission. |
VideohatiNotFoundError | 404 | Resource missing or not visible to this caller. |
VideohatiConflictError | 409 | Conflicts with the resource's current state. |
VideohatiRateLimitError | 429 | Rate limit exceeded; read retryAfterSeconds. |
VideohatiServerError | 5xx | The 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.