Magic Hour publishes this guide and provides one of the APIs below. This article was generated with AI from the cited documentation. The comparison is based on public endpoint documentation checked November 8, 2026. It does not report a head-to-head render test or measured latency.
A face swap API can accept your request without producing a clip you can use. Before choosing one, check which face it targets, how it reports a finished result, and what you pay for the clips that make it through your own review.
| If you need | Start with | Check before building |
|---|---|---|
| One API for ordinary video swaps and explicit face mappings | Magic Hour | Direct media URLs or uploaded file paths. The basic request uses all-faces; use individual-faces with face_mappings if different people need different replacements. |
| A published per-second price and one face selected by size | WaveSpeedAI | Its documented target_index selects by face size. Check that this identifies the intended person in your actual footage. |
| A separate face-detection step before supplying mappings | AKOOL | Its Video Faceswap route accepts a video and faceMapping data. Account for the detection step when estimating integration work. |
Magic Hour request schema · WaveSpeed model and pricing · AKOOL request schema
Compare cost after you can download the result
Start with the same three-second input and the same acceptance criteria. WaveSpeed publishes a $0.01-per-second rate for this route, rounded up to whole seconds with a three-second minimum. Its published formula gives $0.03 for that request, before considering extra attempts. Magic Hour returns credits_charged when it queues the project. Read the completed project and check your account's credit pricing before converting the charge to dollars. Magic Hour documents that failed jobs are refunded. A successful render that you discard for quality is a separate case to track.
For a fair comparison, divide your net charges after applicable refunds by the number of clips that pass review. Include cases where the job completed but chose the wrong face, had visible artifacts, or returned a file you couldn't use. With zero acceptable clips, record zero accepted and the actual spend instead of computing a unit cost. WaveSpeed rate calculation · Magic Hour billing and status · Magic Hour API pricing
Run one Magic Hour request in Node.js
Save the script below as face-swap.mjs and run it with Node.js 20 or newer. Set MAGIC_HOUR_API_KEY, FACE_IMAGE and TARGET_VIDEO in your shell. Each media variable can be a Magic Hour upload file_path or a direct media URL with a supported extension. Use a video of at least three seconds with one visible person. Run with node face-swap.mjs. This submits one billable request. Only use media you have permission to modify.
import { setTimeout as sleep } from 'node:timers/promises'; const token = process.env.MAGIC_HOUR_API_KEY; const image = process.env.FACE_IMAGE; const video = process.env.TARGET_VIDEO; const existingId = process.env.PROJECT_ID; const base = 'https://api.magichour.ai/v1'; async function request(path, body) { const response = await fetch(base + path, { method: body ? 'POST' : 'GET', headers: { Authorization: `Bearer ${token}`, ...(body ? { 'Content-Type': 'application/json' } : {}), }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(15_000), }); if (!response.ok) { const error = new Error(`Magic Hour returned HTTP ${response.status}`); error.status = response.status; throw error; } return response.json(); } async function main() { if (!token || (!existingId && (!image || !video))) { throw new Error('Set MAGIC_HOUR_API_KEY and either PROJECT_ID or both FACE_IMAGE and TARGET_VIDEO.'); } let id = existingId; if (!id) { const job = await request('/face-swap', { name: 'Three-second video face swap', start_seconds: 0, end_seconds: 3, assets: { face_swap_mode: 'all-faces', image_file_path: image, video_source: 'file', video_file_path: video, }, }); id = job.id; if (!id) throw new Error('No project ID in the create response. Check the account before resubmitting.'); console.error(`PROJECT_ID=${id} (queued credits: ${job.credits_charged})`); } const deadline = Date.now() + 10 * 60_000; while (Date.now() < deadline) { let job; try { job = await request(`/video-projects/${encodeURIComponent(id)}`); } catch (error) { if (!(error.status === 429 || error.status >= 500 || ['TimeoutError', 'TypeError'].includes(error.name))) throw error; await sleep(5_000); continue; } if (job.status === 'complete') { const url = job.downloads?.[0]?.url; if (!url) throw new Error(`Project ${id} is complete but has no download URL.`); console.log(JSON.stringify({ id, url, credits: job.credits_charged })); return; } if (!['queued', 'rendering'].includes(job.status)) { throw new Error(`Project ${id} reached status ${job.status}. Check project details for the reason.`); } await sleep(3_000); } throw new Error(`Stopped waiting. Resume by setting PROJECT_ID=${id}.`); } main().catch(error => { console.error(error.message); process.exitCode = 1; }); What to do when it stops
- The create request timed out. Check the account before resubmitting. The server may have accepted the request even if the response never reached you.
- The project ID exists but polling timed out. Save the ID. Set
PROJECT_IDand run the script again to check the existing job without creating another one. - The job completed. Fetch the returned file, verify it plays, and inspect the intended face at the hardest frame. For a production workflow, save the project ID and final charge with your own acceptance result. Use a webhook if polling is a poor fit for your worker.
Upload private inputs · Project statuses and download fields · Webhook docs
If a three-second clip passes your review, try the hardest clip before buying volume. Explore the Magic Hour Face Swap API
