@puppetry.com/sdk
v0.1.1
Published
Official TypeScript SDK for the Puppetry API — create talking head videos from photos using AI
Maintainers
Readme
@puppetry.com/sdk
Official TypeScript SDK for the Puppetry API — create talking head videos from photos using AI.
Installation
npm install @puppetry.com/sdkQuick Start
import { Puppetry } from "@puppetry.com/sdk";
const client = new Puppetry({ apiKey: "pk_..." });
// Create a video from text
const job = await client.videos.createFromText({
text: "Welcome to our product demo!",
image_url: "https://example.com/photo.jpg",
voice: "puppetry-af_heart",
idempotencyKey: "demo-video-1",
});
// Wait for completion (polls automatically, honoring the create Retry-After hint)
const video = await job.waitForCompletion();
console.log(video.url);Features
- Text-to-Video: Provide text → get a talking head video with AI voice
- Audio-to-Video: Provide audio → lip-sync any portrait
- 500+ Voices: Choose from built-in voices or clone your own
- 29 Languages: Arabic, Chinese, English, French, German, Hindi, and more
- Progress Events: Stream job events through status polling that follows API retry hints
- Voice Cloning: Upload 6 seconds of audio to create a custom voice
- TypeScript-first: Full type safety, JSDoc, IntelliSense support
API Reference
Create a Client
const client = new Puppetry({
apiKey: "pk_...", // Required — get yours at puppetry.com/dashboard/settings/api-keys
baseUrl: "https://www.puppetry.com/api/v1", // Optional
timeout: 30000, // Optional, ms
maxRetries: 2, // Optional, retries GET 429/5xx and idempotent POST 429/5xx
retryDelayMs: 500, // Optional, exponential backoff base delay
onRetry: (event) => console.warn(event.code, event.retryAfter), // Optional
});The SDK only retries side-effecting POST calls when you pass an
idempotencyKey. Use one for video creates and signed upload URL requests so a
transient 429/5xx cannot create duplicate jobs or charges.
onRetry runs before the SDK sleeps for a safe/idempotent retry and includes
Developer API job, operation, request, retry-after, and status URL metadata when
the server returns it.
Videos
// From text (text-to-speech + lip sync)
const job = await client.videos.createFromText({
text: "Hello, world!",
image_url: "https://example.com/photo.jpg",
voice: "puppetry-af_heart",
language: "en",
expressiveness: 1.0,
idempotencyKey: "text-video-demo-1",
});
console.log(job.source); // "text"
console.log(`First poll at ${job.nextPollAt ?? "the Retry-After time"}`);Agents and launch checks can preflight video credits and active slots before reserving uploads or queueing a job:
const readiness = await client.videos.getReadiness();
if (!readiness.can_create) {
console.log(readiness.reason, readiness.retryAfter);
for (const blocker of readiness.blockers ?? []) {
console.log(blocker.code, blocker.retryable);
}
}
// Throws PuppetryRateLimitError with retryAfter for active-slot blockers, or
// PuppetryError(402) when the API key is out of video credits.
await client.videos.ensureReady();readiness.blockers preserves the API's machine-readable recovery list. Agents
can treat insufficient_video_credits as a buy-credits path and
concurrent_job_limit_reached as a backoff path using retryAfter or
retry_after_seconds.
For hosted audio uploads that will immediately feed videos.createFromAudio(),
pass requireVideoReadiness: true so Puppetry fails before reserving upload
quota if video credits or active slots are currently blocked.
If an idempotent retry replays an already completed job, the SDK normalizes the
terminal URL onto url, videoUrl, resultUrl, downloadUrl, and
outputUrl while
preserving raw API fields such as download_url and idempotent_replay.
Video job responses also preserve and normalize the create source across
source, request_source, and requestSource, so agents can distinguish text
jobs from audio/lip-sync jobs during status polling or idempotent replay. The
seven-day lookup expiry is normalized across expires_at and expiresAt.
Retryable video job responses expose both relative retry seconds and absolute
poll timestamps as next_poll_at / nextPollAt.
SDK waitForCompletion() and videos.stream() polling honors both forms, so
agents can follow the server-paced poll schedule without custom sleep logic.
Video create, replay, and status responses also normalize the public operation
handle across operation_id / operationId, using the response operation-id
headers when the body omits those aliases.
waitForCompletion() also treats retryable status-poll errors as part of the
same job lifecycle: it uses the error's preserved job identity, status URL, and
retry timing to keep polling the original job until the caller timeout expires.
The polling URL is normalized across status_url / statusUrl; when a queued
create or replay response only exposes the URL in Location /
Content-Location, the SDK fills the same aliases from those headers. Job
helpers also derive their polling target from that canonical status URL when it
is present, so idempotent replays and sparse header-only responses keep polling
the accepted video job.
// From audio (lip sync only)
const job2 = await client.videos.createFromAudio({
audio_url: "https://example.com/speech.mp3",
image_url: "https://example.com/photo.jpg",
idempotencyKey: "audio-video-demo-1",
});
// Get job status
const status = await client.jobs.get(job.id);
// Wait for completion (with polling)
const completed = await job.waitForCompletion({
intervalMs: 2000, // Poll every 2s (default)
timeoutMs: 300000, // 5 min timeout (default)
// initialDelayMs defaults to the job's Retry-After hint when present
onPoll: (latest) => {
console.log(latest.status, latest.progress ?? 0);
},
});
// The explicit resource helper is still available when you only have an ID.
const completedAgain = await client.jobs.waitForCompletion(job.id, {
initialDelayMs: (job.retryAfter ?? 0) * 1000,
});
// Stream progress events through polling
for await (const event of client.videos.stream(job.id)) {
console.log(`${event.type}: ${event.progress}%`);
if (event.type === "completed") {
console.log(`Video ready: ${event.video_url}`);
}
}Voices
// List available voices
const { data: voices } = await client.voices.list({
language: "en",
gender: "female",
limit: 20,
});
// List built-in Puppetry voices for /tts/puppetry
const puppetryVoices = await client.voices.listPuppetry();
console.log(puppetryVoices.default_voice_id);
console.log(puppetryVoices.data[0]?.preview_text);Text to Speech
const voices = await client.voices.listPuppetry();
const audio = await client.tts.createPuppetry({
voice_id: voices.default_voice_id,
text: "Create a voiceover before turning it into video.",
speed: 1.0,
});
console.log(audio.audio_url);Uploads
const upload = await client.uploads.createAudioUrl({
contentType: "audio/mpeg",
fileSize: 12345,
idempotencyKey: "upload-demo-1",
});
await fetch(upload.uploadUrl, {
method: upload.method,
headers: upload.headers,
body: audioBytes,
});
console.log(upload.readUrl);
// Or let the SDK request the signed URL and upload bytes in one call.
const uploaded = await client.uploads.uploadAudio({
contentType: "audio/mpeg",
sizeBytes: audioBytes.byteLength,
body: audioBytes,
idempotencyKey: "upload-demo-1",
});
const job = await client.videos.createFromAudio({
audio_url: uploaded.readUrl,
image_url: "https://example.com/photo.jpg",
});The raw API fields (upload_url, read_url, expiry fields, and limits) stay
available too; the SDK also exposes camelCase aliases for TypeScript and agent
callers, including idempotentReplay when a retried upload reservation is
replayed.
Puppets
// Create a reusable puppet (portrait)
const puppet = await client.puppets.create({
imageUrl: "https://example.com/portrait.jpg",
name: "Marketing Avatar",
});
// List puppets
const { data: puppets } = await client.puppets.list({ limit: 10 });
// Delete a puppet when it should no longer appear in your reusable library
await client.puppets.delete(puppet.id);Usage
const usage = await client.usage.get();
console.log(
`${usage.videoCredits?.balance ?? usage.creditsRemaining} video credits remaining`,
);
console.log(usage.videoGeneration?.blockers ?? []);
console.log(
`${usage.videoCredits?.netUsed ?? usage.creditsUsed} video credits used this month`,
);
// Agent integrations can use the quota alias for a clearer tool name.
const quota = await client.quota.get();
console.log(`${quota.creditsRemaining} video credits remaining`);Agent Tool Manifest
import {
Puppetry,
PUPPETRY_AGENT_TOOLS,
callPuppetryAgentTool,
} from "@puppetry.com/sdk";
console.log(PUPPETRY_AGENT_TOOLS.map((tool) => tool.name));
// puppetry_create_video_from_text, puppetry_create_video_from_audio,
// puppetry_create_video_from_text_and_wait,
// puppetry_create_video_from_audio_and_wait, puppetry_create_audio_upload_url,
// puppetry_lipsync, puppetry_list_voices, puppetry_get_job_status,
// puppetry_wait_for_video, puppetry_get_video_readiness, puppetry_get_quota
const client = new Puppetry({ apiKey: process.env.PUPPETRY_API_KEY! });
const result = await callPuppetryAgentTool(
client,
"puppetry_create_video_from_text",
{
prompt: "Agent-generated launch update.",
photo_url: "https://example.com/avatar.jpg",
voice: "puppetry-af_heart",
preflightReadiness: true,
},
);
console.log(result.id);
console.log(result.readiness_checked, result.readiness?.credits_remaining);
await callPuppetryAgentTool(client, "puppetry_get_job_status", {
taskId: result.taskId ?? result.id,
});Direct video agent tools accept preflightReadiness / preflight_readiness
when the caller wants to check video credits and active slots before queueing a
credit-bearing job. Successful direct-create results include
readiness_checked / readinessChecked and, when preflight ran, the normalized
readiness object that was checked before the create POST. The create-and-wait
agent tools perform that preflight by default and expose
preflightReadiness: false only for callers that already checked readiness.
Both result shapes include snake_case and camelCase readiness aliases so agents
can read the same evidence regardless of their naming convention.
MCP stdio server
{
"mcpServers": {
"puppetry": {
"command": "npx",
"args": ["-y", "@puppetry.com/sdk"],
"env": { "PUPPETRY_API_KEY": "pk_live_xxx" }
}
}
}The puppetry-mcp bin serves the same tool manifest over MCP stdio and
dispatches calls through the SDK. By default it uses
https://www.puppetry.com/api/v1, the live Developer API route family. Set
PUPPETRY_BASE_URL only when pointing agents at a preview or staging API host.
When the live API returns a Puppetry error, MCP JSON-RPC errors include
structured error.data with the API status/statusCode, Puppetry code,
message, optional details, and optional retry_after_seconds/
retryAfterSeconds/retryAfter seconds so agents can back off instead of
retrying blindly. Retry hints are normalized to seconds whether the API sends a
numeric or HTTP-date Retry-After header, and fall back to top-level
retry_after_seconds / retryAfterSeconds / retryAfter body aliases or
matching details.* aliases if an intermediary strips the header. Video job
responses also include next_poll_at / nextPollAt when the API can provide an
absolute retry time, plus pollable video identity fields such as object,
id, job_id / jobId, source, request_source / requestSource,
status_url / statusUrl, and expires_at / expiresAt when the error
belongs to a video job.
Error Handling
import { PuppetryError, PuppetryAuthError, PuppetryRateLimitError, PuppetryNetworkError } from '@puppetry.com/sdk';
try {
await client.videos.createFromText({ ... });
} catch (err) {
if (err instanceof PuppetryAuthError) {
console.error('Invalid API key');
} else if (err instanceof PuppetryRateLimitError) {
console.error(`Rate limited. Retry after ${err.retryAfter}s`);
} else if (err instanceof PuppetryNetworkError) {
console.error(`Network error: ${err.message}`);
} else if (err instanceof PuppetryError) {
if (err.retryAfter) {
console.error(`Try again after ${err.retryAfter}s`);
}
console.error(`API error ${err.status}: ${err.message}`);
}
}Requirements
- Node.js 18+ (uses native
fetch) - For Node.js 16, pass a
fetchimplementation:new Puppetry({ apiKey: '...', fetch: nodeFetch })
License
MIT
