KeyoAPI

← Blog ·

AI Avatar API Node.js Example: Upload, Job Status, and Retry Patterns

Learn how to design a production-ready Node.js workflow for AI avatar APIs, including media upload, asynchronous job polling, retries, authentication, error handling, security, cost controls, and provider capability checks.

Building a talking avatar feature usually involves more than sending text to a model. A production workflow may need to:

  1. Upload an image, video, or audio asset.
  2. Create an asynchronous avatar-generation job.
  3. Track job status.
  4. Download or store the completed video.
  5. Retry temporary failures without duplicating work.

The difficult part is that avatar APIs do not share a universal upload format, job schema, or status endpoint. Before writing integration code, confirm the provider's current documentation and API catalog.

KeyoAPI hosts talking-avatar / lip-sync models in the live catalog — including Duix-Avatar and InfiniteTalk. Start from /ai-avatar-video-generator, then confirm interactive rates on /model/Duix-Avatar, /model/InfiniteTalk, and the full /pricing-list.

Start With a Capability Check

An OpenAI-compatible chat API is not automatically an avatar-video API. These are separate capabilities:

The KeyoAPI base URL is:

https://www.keyoapi.xyz/v1

Authentication uses Bearer tokens, and you can check available model IDs with:

GET /v1/models

Use an Adapter Boundary

Keep provider-specific behavior behind a small interface. Your application should understand the workflow, while the adapter owns the provider's actual upload and job API.

A useful interface looks like this:

class AvatarProvider { async uploadAsset(file) { throw new Error("Implement with the provider's documented upload API"); } async createJob(input) { throw new Error("Implement with the provider's documented job API"); } async getJob(jobId) { throw new Error("Implement with the provider's documented status API"); } async downloadResult(job) { throw new Error("Implement with the provider's documented result API"); }
}

This design has several advantages:

Do not replace these methods with guessed paths such as /uploads, /avatar/jobs, or /videos/{id} unless the provider's current documentation explicitly defines them.

Node.js Workflow With Dependency Injection

The following example implements the application-side workflow. The provider adapter remains intentionally abstract because live docs must confirm avatar-specific endpoints.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function isRetryableError(error) { return ( error?.retryable === true || error?.status === 408 || error?.status === 429 || error?.status >= 500 );
} async function withExponentialBackoff( operation, { attempts = 4, baseDelayMs = 500, maxDelayMs = 10_000, shouldRetry = isRetryableError, } = {},
) { let lastError; for (let attempt = 0; attempt < attempts; attempt += 1) { try { return await operation(attempt); } catch (error) { lastError = error; if (attempt === attempts - 1 || !shouldRetry(error)) { throw error; } const exponentialDelay = Math.min( maxDelayMs, baseDelayMs * 2 ** attempt, ); const jitter = Math.floor(Math.random() * 250); await sleep(exponentialDelay + jitter); } } throw lastError;
} async function renderAvatar({ provider, sourceImage, speechAudio, script, idempotencyKey, pollIntervalMs = 2_000, maxPolls = 90,
}) { const image = await withExponentialBackoff(() => provider.uploadAsset(sourceImage), ); const audio = await withExponentialBackoff(() => provider.uploadAsset(speechAudio), ); const job = await withExponentialBackoff(() => provider.createJob({ imageAssetId: image.id, audioAssetId: audio.id, script, idempotencyKey, }), ); for (let poll = 0; poll < maxPolls; poll += 1) { const currentJob = await withExponentialBackoff(() => provider.getJob(job.id), ); if (currentJob.status === "completed") { return provider.downloadResult(currentJob); } if (currentJob.status === "failed" || currentJob.status === "cancelled") { const error = new Error( currentJob.error?.message || `Avatar job ${currentJob.status}`, ); error.retryable = false; throw error; } await sleep(pollIntervalMs); } const timeout = new Error("Avatar job did not finish before the polling limit"); timeout.retryable = false; throw timeout;
}

The orchestration code assumes only a small normalized result shape:

{ id: "provider-job-id", status: "queued" | "processing" | "completed" | "failed" | "cancelled", error: { message: "optional provider error" }
}

Those status names are application-level conventions. Map the provider's actual values to them inside the adapter.

Implement the Provider Adapter From Documentation

A real adapter should be written only after confirming the provider contract. Its responsibilities typically include:

Uploading assets

The adapter should verify:

Do not log raw image, audio, or video payloads. Store only the provider's asset identifier and the metadata needed to resume the workflow.

Creating the job

The job request may require some combination of:

Only send fields documented by the provider. Unsupported fields can cause validation errors or, worse, silently change behavior.

Reading job status

The adapter should translate provider responses into a stable internal form:

function normalizeJob(providerJob) { return { id: providerJob.id, status: mapProviderStatus(providerJob.status), progress: providerJob.progress ?? null, error: providerJob.error ? { code: providerJob.error.code, message: providerJob.error.message, }: null, };
}

Keep the original provider response available in structured logs when it does not contain sensitive content. This makes troubleshooting easier when the provider changes response fields.

Downloading the result

A completed job may return:

The adapter should download the result to application-controlled storage rather than relying indefinitely on a temporary provider URL. Validate the response content type and size before storing it.

Authentication and Key Management

For documented KeyoAPI requests, authentication uses a Bearer token:

Authorization: Bearer YOUR_API_KEY

A Node.js client configuration uses an environment variable:

const apiKey = process.env.KEYO_API_KEY; if (!apiKey) { throw new Error("KEYO_API_KEY is required");
}

Do not commit API keys to Git, include them in browser code, or expose them in client-side logs. For a web application:

  1. The browser sends a request to your backend.
  2. Your backend authenticates the user.
  3. Your backend calls the external API.
  4. The browser receives only the job identifier or application-specific result.

Use separate credentials for development, staging, and production when the provider supports that arrangement. Restrict access to secret-management systems, rotate credentials periodically, and redact authorization headers from request logs.

The official KeyoAPI documentation lives at keyoapi.xyz/brand/keyo-docs.html. Check that documentation for current authentication requirements before implementing a provider adapter.

Retry Only Temporary Failures

Retries are appropriate for transient conditions such as:

KeyoAPI returns rate limits, exhausted account quota, insufficient prepaid balance, unavailable model IDs, and request timeouts as distinct failure conditions. These should not all be handled in the same way:

Failure Recommended handling
Timeout Retry with exponential backoff if the request may be safely repeated
Rate limit Respect Retry-After when available and slow down
Server error Retry a limited number of times
Account quota exhausted Stop retrying and notify operations
Insufficient balance Stop retrying and check account status
Model not found Stop retrying and query the current model catalog
Invalid input Fix the request rather than retrying

For job creation, use an idempotency key if the provider documents support for it. Without idempotency, a timeout after submission creates an ambiguity: the request may have succeeded even though the client did not receive the response. Blindly retrying may create duplicate video jobs.

A stable idempotency key can be derived from your internal render request:

import crypto from "node:crypto"; function createIdempotencyKey(renderRequestId) { return crypto.createHash("sha256").update(`avatar-render:${renderRequestId}`).digest("hex");
}

This key is useful only if the provider actually supports idempotency semantics. Otherwise, store the submission attempt and reconcile the result using the provider's documented lookup mechanism.

Polling Versus Webhooks

Polling is straightforward and works when the provider offers a status endpoint. It also creates additional requests and can become expensive at scale.

Use bounded polling:

A production system can use a database record similar to:

render_request_id
provider_job_id
status
attempt_count
last_polled_at
next_poll_at
result_location
failure_code
created_at
updated_at

Prefer webhooks when the provider documents signed webhook delivery. A webhook handler should:

  1. Verify the signature before parsing the event as trusted.
  2. Reject old or duplicated events using event IDs or timestamps.
  3. Update the job only when the transition is valid.
  4. Return a fast success response.
  5. Process downloads and heavier work asynchronously.

Do not accept an arbitrary callback URL from an untrusted client. Store approved webhook destinations on the server and validate event ownership before updating a render request.

Error Handling at the Application Boundary

Convert provider-specific errors into stable application errors. A client should not need to understand every provider status code.

For example:

function toApplicationError(error) { if (error.status === 429) { return { code: "RATE_LIMITED", retryable: true, message: "The rendering service is temporarily rate-limited." }; } if (error.status >= 500) { return { code: "UPSTREAM_UNAVAILABLE", retryable: true, message: "The rendering service is temporarily unavailable." }; } return { code: "AVATAR_REQUEST_FAILED", retryable: false, message: "The avatar request could not be completed." };
}

Return useful information to the user without exposing:

Log a correlation ID, internal render ID, provider job ID, normalized error code, and timing information. Avoid logging source media, scripts containing personal information, or raw authorization headers.

Security and Media Validation

Avatar systems process user-controlled files and often produce publicly shareable media. Apply security controls before sending data upstream:

If a user can provide a remote URL, protect the upload worker against server-side request forgery. Permit only approved schemes, block private network ranges, limit redirects, and impose response-size and timeout limits.

For face and voice workflows, add product-level controls as well as technical controls. A successful API request does not establish that the requester has permission to use a person's likeness or voice.

Cost and Capacity Controls

Avatar video rendering can consume more resources than ordinary text requests. Cost controls should be part of the design:

When using KeyoAPI for supported model workflows, consult the current pricing page rather than embedding prices in application code or documentation. Pricing and model availability can change; confirm avatar-video rates on /ai-avatar-video-generator and /pricing-list before production.

A cost record should distinguish at least:

application_request_id
provider
model_or_operation
input_duration_or_size
output_duration_or_size
provider_job_id
retry_count
estimated_cost
actual_cost_if_available

Do not report an estimated cost as a confirmed charge unless the provider exposes billing data that supports it.

Model and Feature Availability

Availability must be checked at integration time and periodically in production.

For KeyoAPI's documented model-based interface, docs recommend querying:

GET https://www.keyoapi.xyz/v1/models

Use the exact model ID returned by the live response. A hard-coded model name may become unavailable or may not be enabled for the account.

For avatar and video features, a model catalog alone may not be enough. Confirm the complete operation contract in the live documentation, including:

Testing the Workflow

Test the orchestration separately from the provider adapter.

A fake provider can exercise state transitions without uploading real media:

function createFakeProvider() { let polls = 0; return { async uploadAsset(file) { return { id: `asset-${file.name}` }; }, async createJob(input) { return { id: "job-123", input }; }, async getJob() { polls += 1; if (polls < 3) { return { id: "job-123", status: "processing" }; } return { id: "job-123", status: "completed" }; }, async downloadResult() { return { location: "private://rendered-video/job-123.mp4" }; }, };
}

Cover these cases:

Contract tests should run against a provider's documented test environment when one exists. Keep them separate from unit tests so changes in external availability do not make the entire test suite unreliable.

Practical Checklist

Before shipping an AI avatar integration, verify:

Conclusion

A reliable Node.js avatar integration is a workflow and state-management problem — start from /ai-avatar-video-generator, pick a live avatar ID on /pricing-list, then verify one end-to-end render before scaling concurrency. Uploads, asynchronous jobs, retries, result storage, authentication, and media security must work together.

← Blog · Home · Docs