@eldritchlogic/heygen-sdk
v0.1.0
Published
Fully-typed Node.js/TypeScript SDK for the HeyGen API — videos, video agent, avatars, voices, translations, lipsync, streaming, webhooks, and every other documented endpoint.
Maintainers
Readme
@eldritchlogic/heygen-sdk
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-Aftersupport, idempotency keys, SSE streaming, and webhook signature verification.
Install
npm install @eldritchlogic/heygen-sdkQuick start
import { HeyGen } from "@eldritchlogic/heygen-sdk";
const heygen = new HeyGen({ apiKey: process.env.HEYGEN_API_KEY });
// Create a video and wait for it to render.
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);Authentication & configuration
const heygen = new HeyGen({
apiKey: "...", // default: process.env.HEYGEN_API_KEY (sent as x-api-key)
accessToken: "...", // alternative: OAuth2 bearer token
baseUrl: "https://api.heygen.com", // override for testing/proxies
timeoutMs: 60_000, // per-request timeout
maxRetries: 2, // automatic retries on 429/5xx/network errors
defaultHeaders: {}, // merged into every request
fetch: customFetch, // bring your own fetch
});Every method also accepts per-request options as its last argument:
await heygen.videos.create(body, {
idempotencyKey: crypto.randomUUID(), // safe retries for mutations
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.
Pagination
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);Polling helpers
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.
Webhooks
import { verifyWebhookSignature, WebhookVerificationError } from "@eldritchlogic/heygen-sdk";
// Register an endpoint (store the returned secret — it's only shown once):
const endpoint = await heygen.webhooks.endpoints.create({
url: "https://yourapp.com/webhooks/heygen",
events: ["avatar_video.success", "avatar_video.fail"],
});
// In your webhook handler (keep the raw body — parsed JSON won't verify):
app.post("/webhooks/heygen", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhookSignature({
payload: req.body, // raw Buffer/string
signature: req.header("Heygen-Signature")!, // hex HMAC-SHA256
timestamp: req.header("Heygen-Timestamp"), // replay protection (±300 s)
secret: process.env.HEYGEN_WEBHOOK_SECRET!,
});
// handle event ...
res.sendStatus(200);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.sendStatus(401);
throw err;
}
});Error handling
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;
}Endpoint reference
Every operation in HeyGen's OpenAPI specs, and the SDK method that calls it.
Videos — heygen.videos
| 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} |
Video Agent — heygen.videoAgents
| 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 |
Avatars — heygen.avatars
| 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} |
Avatar Realtime — heygen.avatarRealtime
| 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 |
Voices & audio — heygen.voices, heygen.audio
| 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 |
Templates — heygen.templates
| Method | Endpoint |
| --- | --- |
| list(params?) | GET /v3/templates |
| get(templateId) | GET /v3/templates/{template_id} |
| generate(templateId, body) | POST /v3/templates/{template_id} |
Video translation — heygen.videoTranslations
| 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 |
Lipsync — heygen.lipsyncs
| 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} |
HyperFrames — heygen.hyperframes
| 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} |
AI Clipping — heygen.aiClipping
| 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} |
Assets — heygen.assets
| 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} |
Webhooks — heygen.webhooks
| 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 |
Brand, users, workflows, background removal
| 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} |
Legacy API — heygen.legacy
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 |
Types
Every schema in HeyGen's OpenAPI spec is exported by name:
import type { CreateVideoV3RequestBody, VideoDetail, AvatarLookItem } from "@eldritchlogic/heygen-sdk";Regenerating types
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 # regenerates src/generated/*, then `npm test` verifies coverageIf HeyGen adds an endpoint, the coverage test fails until the SDK implements it.
License
MIT
