Fully-typed Node.js/TypeScript SDK for the HeyGen API — every documented endpoint is covered: videos, Video Agent, avatars, realtime avatars, voices & TTS, video translation, lipsync, AI clipping, HyperFrames, assets, webhooks, brand, workflows, background removal, plus the complete legacy (pre-v3) API surface.
- 100% endpoint coverage, mechanically verified. A test suite walks HeyGen's published OpenAPI specs (vendored in
specs/) and fails if any operation lacks an SDK method or hits the wrong method/path.
- Fully typed. Request and response types are generated from HeyGen's official OpenAPI spec — every schema is exported by name.
- Zero runtime dependencies. Built on the global
fetch (Node.js ≥ 18.17, Bun, Deno, edge runtimes, modern browsers).
- Batteries included. Cursor auto-pagination, job polling helpers, automatic retries with
Retry-After support, idempotency keys, SSE streaming, and webhook signature verification.
npm install @eldritchlogic/heygen-sdk
import { HeyGen } from "@eldritchlogic/heygen-sdk";
const heygen = new HeyGen({ apiKey: process.env.HEYGEN_API_KEY });
const { video_id } = await heygen.videos.create({
type: "avatar",
avatar_id: "your_avatar_look_id",
script: "Welcome to our product tour!",
aspect_ratio: "auto",
resolution: "1080p",
});
const video = await heygen.videos.waitForCompletion(video_id);
console.log(video.video_url);
Or let the Video Agent do everything from a prompt:
const session = await heygen.videoAgents.create({
prompt: "A 30-second product intro for an AI note-taking app, upbeat tone",
});
const done = await heygen.videoAgents.waitForCompletion(session.session_id);
const heygen = new HeyGen({
apiKey: "...",
accessToken: "...",
baseUrl: "https://api.heygen.com",
timeoutMs: 60_000,
maxRetries: 2,
defaultHeaders: {},
fetch: customFetch,
});
Every method also accepts per-request options as its last argument:
await heygen.videos.create(body, {
idempotencyKey: crypto.randomUUID(),
timeoutMs: 120_000,
signal: abortController.signal,
headers: { "x-trace-id": "..." },
});
Retries: GET/PUT/DELETE requests are retried automatically on 429, 5xx, and network errors (respecting Retry-After). POST/PATCH requests are only retried when you pass an idempotencyKey, which HeyGen deduplicates server-side for 24 h.
List endpoints return a Page<T> with data, hasMore, and nextToken. Iterate the page to walk all pages lazily, or use nextPage() / toArray(limit):
for await (const voice of await heygen.voices.list({ language: "English" })) {
console.log(voice.name);
}
const firstHundred = await (await heygen.videos.list()).toArray(100);
Async jobs expose waitForCompletion (and friends) that poll until a terminal status:
await heygen.videos.waitForCompletion(videoId, { intervalMs: 5000, timeoutMs: 15 * 60_000 });
await heygen.videoTranslations.waitForCompletion(translationId);
await heygen.lipsyncs.waitForCompletion(lipsyncId);
await heygen.aiClipping.waitForCompletion(jobId);
await heygen.hyperframes.renders.waitForCompletion(renderId);
await heygen.backgroundRemovals.waitForCompletion(jobId);
await heygen.voices.waitForClone(voiceId);
await heygen.avatars.waitForTraining(groupId);
await heygen.videoTranslations.proofreads.waitForCompletion(proofreadId);
Prefer webhooks over polling in production — see below.
import { verifyWebhookSignature, WebhookVerificationError } from "@eldritchlogic/heygen-sdk";
const endpoint = await heygen.webhooks.endpoints.create({
url: "https://yourapp.com/webhooks/heygen",
events: ["avatar_video.success", "avatar_video.fail"],
});
app.post("/webhooks/heygen", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhookSignature({
payload: req.body,
signature: req.header("Heygen-Signature")!,
timestamp: req.header("Heygen-Timestamp"),
secret: process.env.HEYGEN_WEBHOOK_SECRET!,
});
res.sendStatus(200);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.sendStatus(401);
throw err;
}
});
All API failures throw typed subclasses of APIError with status, code, param, docUrl, requestId, headers, and the raw body:
| Status |
Error class |
| 400 |
BadRequestError |
| 401 |
AuthenticationError |
| 403 |
PermissionDeniedError |
| 404 |
NotFoundError |
| 409 |
ConflictError (e.g. idempotent retry still in flight) |
| 422 |
UnprocessableEntityError |
| 429 |
RateLimitError (with .retryAfter seconds) |
| 5xx |
InternalServerError |
| network |
APIConnectionError / APITimeoutError |
import { RateLimitError } from "@eldritchlogic/heygen-sdk";
try {
await heygen.videos.create(body);
} catch (err) {
if (err instanceof RateLimitError) console.log(`retry in ${err.retryAfter}s`);
throw err;
}
Every operation in HeyGen's OpenAPI specs, and the SDK method that calls it.
| Method |
Endpoint |
create(body) |
POST /v3/videos |
get(videoId) |
GET /v3/videos/{video_id} |
list(params?) |
GET /v3/videos |
delete(videoId) |
DELETE /v3/videos/{video_id} |
statuses(params?) |
GET /v3/videos/statuses |
batches.create(body) |
POST /v3/videos/batches |
batches.get(batchId, params?) |
GET /v3/videos/batches/{batch_id} |
createV2(body) (deprecated) |
POST /v2/videos |
getV2(videoId) (deprecated) |
GET /v2/videos/{video_id} |
listV2(params?) (deprecated) |
GET /v2/videos |
deleteV2(videoId) (deprecated) |
DELETE /v2/videos/{video_id} |
| Method |
Endpoint |
create(body) |
POST /v3/video-agents |
get(sessionId) |
GET /v3/video-agents/{session_id} |
list(params?) |
GET /v3/video-agents |
sendMessage(sessionId, body) |
POST /v3/video-agents/{session_id} |
stop(sessionId, body?) |
POST /v3/video-agents/{session_id}/stop |
listVideos(sessionId) |
GET /v3/video-agents/{session_id}/videos |
getResource(sessionId, resourceId) |
GET /v3/video-agents/{session_id}/resources/{resource_id} |
listStyles(params?) |
GET /v3/video-agents/styles |
generateV1(body) (deprecated) |
POST /v1/video_agent/generate |
| Method |
Endpoint |
create(body) |
POST /v3/avatars |
listGroups(params?) |
GET /v3/avatars |
getGroup(groupId) |
GET /v3/avatars/{group_id} |
deleteGroup(groupId) |
DELETE /v3/avatars/{group_id} |
createConsent(groupId, body) |
POST /v3/avatars/{group_id}/consent |
looks.list(params?) |
GET /v3/avatars/looks |
looks.get(lookId) |
GET /v3/avatars/looks/{look_id} |
looks.update(lookId, body) |
PATCH /v3/avatars/looks/{look_id} |
looks.delete(lookId) |
DELETE /v3/avatars/looks/{look_id} |
| Method |
Endpoint |
createSession(body) |
POST /v3/avatar-realtime |
getSession(streamId) |
GET /v3/avatar-realtime/{stream_id} |
appendText(streamId, body) |
POST /v3/avatar-realtime/{stream_id}/text |
cancel(streamId) |
POST /v3/avatar-realtime/{stream_id}/cancel |
streamWords(streamId) — async iterator over SSE |
GET /v3/avatar-realtime/{stream_id}/words |
| Method |
Endpoint |
voices.list(params?) |
GET /v3/voices |
voices.get(voiceId) |
GET /v3/voices/{voice_id} |
voices.delete(voiceId) |
DELETE /v3/voices/{voice_id} |
voices.clone(body) |
POST /v3/voices/clone |
voices.design(body) |
POST /v3/voices |
voices.speech(body) — TTS |
POST /v3/voices/speech |
voices.textToSpeechV1(body) (deprecated) |
POST /v1/audio/text_to_speech |
voices.listV1(params?) (deprecated) |
GET /v1/audio/voices |
audio.searchSounds(params) |
GET /v3/audio/sounds |
| Method |
Endpoint |
list(params?) |
GET /v3/templates |
get(templateId) |
GET /v3/templates/{template_id} |
generate(templateId, body) |
POST /v3/templates/{template_id} |
| Method |
Endpoint |
create(body) |
POST /v3/video-translations |
get(id) |
GET /v3/video-translations/{video_translation_id} |
list(params?) |
GET /v3/video-translations |
update(id, body) |
PATCH /v3/video-translations/{video_translation_id} |
delete(id) |
DELETE /v3/video-translations/{video_translation_id} |
listLanguages() |
GET /v3/video-translations/languages |
statuses(params?) |
GET /v3/video-translations/statuses |
proofreads.create(body) |
POST /v3/video-translations/proofreads |
proofreads.get(id) |
GET /v3/video-translations/proofreads/{proofread_id} |
proofreads.downloadSrt(id) |
GET /v3/video-translations/proofreads/{proofread_id}/srt |
proofreads.uploadSrt(id, body) |
PUT /v3/video-translations/proofreads/{proofread_id}/srt |
proofreads.generateVideo(id, body?) |
POST /v3/video-translations/proofreads/{proofread_id}/generate |
batches.create(body) |
POST /v3/video-translations/batches |
batches.get(batchId, params?) |
GET /v3/video-translations/batches/{batch_id} |
createV2(body) (deprecated) |
POST /v2/video_translate |
listTargetLanguagesV2() (deprecated) |
GET /v2/video_translate/target_languages |
getCaptionV2(params) (deprecated) |
GET /v2/video_translate/caption |
| Method |
Endpoint |
create(body) |
POST /v3/lipsyncs |
get(lipsyncId) |
GET /v3/lipsyncs/{lipsync_id} |
list(params?) |
GET /v3/lipsyncs |
update(lipsyncId, body) |
PATCH /v3/lipsyncs/{lipsync_id} |
delete(lipsyncId) |
DELETE /v3/lipsyncs/{lipsync_id} |
statuses(params?) |
GET /v3/lipsyncs/statuses |
batches.create(body) |
POST /v3/lipsyncs/batches |
batches.get(batchId, params?) |
GET /v3/lipsyncs/batches/{batch_id} |
| Method |
Endpoint |
renders.create(body) |
POST /v3/hyperframes/renders |
renders.get(renderId) |
GET /v3/hyperframes/renders/{render_id} |
renders.list(params?) |
GET /v3/hyperframes/renders |
renders.delete(renderId) |
DELETE /v3/hyperframes/renders/{render_id} |
| Method |
Endpoint |
create(body) |
POST /v3/ai-clipping |
get(jobId) |
GET /v3/ai-clipping/{job_id} |
list(params?) |
GET /v3/ai-clipping |
delete(jobId) |
DELETE /v3/ai-clipping/{job_id} |
| Method |
Endpoint |
upload(file, params?) — multipart |
POST /v3/assets |
get(assetId) |
GET /v3/assets/{asset_id} |
list(params) |
GET /v3/assets |
delete(assetId) |
DELETE /v3/assets/{asset_id} |
search(params) |
GET /v3/assets/search |
createDirectUpload(body) |
POST /v3/assets/direct-uploads |
completeUpload(assetId, body?) |
POST /v3/assets/{asset_id}/complete |
statuses(params?) |
GET /v3/assets/statuses |
batches.createDirectUploads(body) |
POST /v3/assets/direct-uploads/batches |
batches.complete(body) |
POST /v3/assets/complete/batches |
batches.get(batchId, params?) |
GET /v3/assets/batches/{batch_id} |
| Method |
Endpoint |
endpoints.create(body) |
POST /v3/webhooks/endpoints |
endpoints.list(params?) |
GET /v3/webhooks/endpoints |
endpoints.update(endpointId, body) |
PATCH /v3/webhooks/endpoints/{endpoint_id} |
endpoints.delete(endpointId) |
DELETE /v3/webhooks/endpoints/{endpoint_id} |
endpoints.rotateSecret(endpointId) |
POST /v3/webhooks/endpoints/{endpoint_id}/rotate-secret |
listEventTypes() |
GET /v3/webhooks/event-types |
listEvents(params?) |
GET /v3/webhooks/events |
verify(params) |
— HMAC verification helper |
| Method |
Endpoint |
brand.listKits(params?) |
GET /v3/brand-kits |
brand.listGlossaries(params?) |
GET /v3/brand-glossaries |
users.me() |
GET /v3/users/me |
users.meV1() (deprecated) |
GET /v1/user/me |
workflows.list() |
GET /v1/workflows |
workflows.createExecution(body) |
POST /v1/workflows/executions |
workflows.createGraphExecution(body) |
POST /v1/workflows/graph-executions |
workflows.getExecution(executionId) |
GET /v1/workflows/executions/{execution_id} |
backgroundRemovals.create(body) |
POST /v3/background-removals |
backgroundRemovals.get(jobId) |
GET /v3/background-removals/{job_id} |
backgroundRemovals.list(params?) |
GET /v3/background-removals |
backgroundRemovals.delete(jobId) |
DELETE /v3/background-removals/{job_id} |
The complete pre-v3 surface (supported by HeyGen until October 31, 2026). Prefer the v3 resources above for new work; these methods return loosely-typed objects where HeyGen does not document response schemas.
| Namespace |
Endpoints |
legacy.streaming |
newSession, startSession, listSessions, sendTask, interruptTask, stopSession, createSessionToken, listAvatars — /v1/streaming.* |
legacy.photoAvatars |
generatePhotos, generateLooks, getGeneration, createGroup, addLooksToGroup, train, getTrainingStatus, addMotion, addSoundEffect, upscale, get, listGroups, listGroupAvatars — /v2/photo_avatar/*, /v2/avatar_group* |
legacy.videoAvatars |
create, getStatus, delete — /v2/video_avatar |
legacy.videos |
generate, getStatus, list, delete, createWebm — /v2/video/generate, /v1/video_status.get, /v1/video.list, /v1/video.delete, /v1/video.webm |
legacy.templates |
list, get, generate, getVariableSchema — /v2/templates, /v2/template/{id}, /v2/template/{id}/generate, /v3/template/{id} |
legacy.webhooks |
listEndpoints, addEndpoint, updateEndpoint, deleteEndpoint, listAvailableEvents — /v1/webhook/* |
legacy.folders |
create, list, update, trash, restore — /v1/folders* |
legacy.brandVoices |
list, update — /v1/brand_voice/* |
legacy.avatars |
list — GET /v2/avatars |
legacy.voices |
list — GET /v2/voices |
legacy.user |
getRemainingQuota — GET /v2/user/remaining_quota |
legacy.videoTranslate |
getStatus — GET /v2/video_translate/{id} |
legacy.assets |
upload(file, contentType) — POST upload.heygen.com/v1/asset |
Every schema in HeyGen's OpenAPI spec is exported by name:
import type { CreateVideoV3RequestBody, VideoDetail, AvatarLookItem } from "@eldritchlogic/heygen-sdk";
The specs are vendored in specs/. To refresh from HeyGen's published spec:
curl -s https://developers.heygen.com/openapi/external-api.json -o specs/external-api.json
npm run gen:types
If HeyGen adds an endpoint, the coverage test fails until the SDK implements it.
MIT