API de PageToVid: entra una URL, sale un MP4
Envía la URL de una página web y recibe un vídeo narrado y subtitulado de ella. Un POST inicia el vídeo y un GET te dice cuándo está listo. Para automatizaciones, generación masiva y vistas previas Open Graph de vídeo.
Crear vídeos con POST /api/v1/videos requiere el plan Pro o superior; en un plan inferior responde 402 con el código UPGRADE_REQUIRED. El servidor MCP funciona en todos los planes, incluido el gratuito.
¿Usas un asistente de IA?Conecta PageToVid a Claude, Cursor, VS Code o Codex y simplemente pide un vídeo.¿Trabajas en VS Code?PageToVid Studio configura el servidor MCP para Claude Code en un clic, con comandos slash para vídeos, series y formatos.Tu primera llamada en tres pasos
1. Consigue una clave de API
Crea una clave API en tu cuenta, y envíala como token Bearer. Mantenla en secreto — gasta tus créditos.
shellexport PAGETOVID_API_KEY="cp_live_xxxxxxxxxxxxxxxx"2. Inicia un vídeo
Solo la URL es obligatoria; cada otro campo tiene un valor por defecto. La respuesta llega enseguida, con un id y un statusUrl. Un vídeo terminado cuesta 40 créditos.
curlcurl -X POST https://pagetovid.com/api/v1/videos \ -H "Authorization: Bearer $PAGETOVID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-site.com" }'3. Recoge el resultado
Llama al statusUrl cada pocos segundos, o indica un callbackUrl y espera nuestro POST. Cuando status es "done", videoUrl es tu MP4.
curlcurl https://pagetovid.com/api/v1/videos/VIDEO_ID \ -H "Authorization: Bearer $PAGETOVID_API_KEY"
URL base: https://pagetovid.com · un vídeo renderizado cuesta 40 créditos.
Crear un vídeo
POST /api/v1/videos — devuelve de inmediato un id; el renderizado se ejecuta async.
curlcurl -X POST https://pagetovid.com/api/v1/videos \ -H "Authorization: Bearer $PAGETOVID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://stripe.com", "goal": "ad", "aspect": "16:9", "language": "en", "targetSeconds": 35, "subtitles": true, "outputs": ["16:9", "9:16"], "qualityGate": { "minScore": 70, "onFail": "hold" }, "pronunciations": [{ "text": "SaaS", "say": "sass" }], "callbackUrl": "https://your-server.com/hooks/pagetovid" }'
goal— explainer · ad · demo · tutorial · articleaspect— 16:9 · 9:16 · 1:1 · 4:5targetSeconds— la duración en segundos, de 15 a 180 (45 por defecto); la narración se escribe para encajaroutputs— opcional: de 1 a 4 formatos a partir de un solo guion gráfico, cada uno con su idqualityGate— opcional: retener un vídeo con nota inferior a minScore en lugar de entregarlo (onFail: auto_fix, hold o deliver)pronunciations— opcional: cómo pronuncia la voz una palabra; los subtítulos mantienen la ortografíacallbackUrl— opcional: enviamos aquí un POST firmado cuando termina el render (done, held, error o cancelled) — mira Verifica el webhook más abajo
Respuesta (202):
json{ "id": "cmq…", "status": "analyzing", "statusUrl": "https://pagetovid.com/api/v1/videos/cmq…", "webhookSecret": "pvwh_…", // only on the first call that sets a callbackUrl: keep it "outputs": [ // only when several shapes were asked for { "aspect": "16:9", "id": "cmq…", "statusUrl": "…" }, { "aspect": "9:16", "id": "cmr…", "statusUrl": "…" } ] }
Verifica el webhook
Cada llamada lleva X-PageToVid-Timestamp, X-PageToVid-Event-Id y X-PageToVid-Signature: sha256= seguido del HMAC-SHA256 en hexadecimal de la marca de tiempo, un punto y el cuerpo bruto con tu secreto de webhook (se devuelve una sola vez como webhookSecret y se sustituye con rotate_webhook_secret). Recalcúlalo sobre el cuerpo bruto, compara en tiempo constante y rechaza una marca de tiempo de más de 300 segundos. El id del evento no cambia entre reintentos: úsalo para ignorar un duplicado.
javascriptimport { createHmac, timingSafeEqual } from "node:crypto"; // raw: the request body exactly as received, BEFORE any JSON parsing. function verifyPageToVid(raw, headers, secret) { const ts = headers["x-pagetovid-timestamp"]; const sig = headers["x-pagetovid-signature"] ?? ""; if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const want = "sha256=" + createHmac("sha256", secret).update(ts + "." + raw).digest("hex"); return sig.length === want.length && timingSafeEqual(Buffer.from(sig), Buffer.from(want)); }
Obtener un vídeo / consultar estado
GET /api/v1/videos/:id — consulta hasta status: "done".
json{ "id": "cmq…", "status": "done", // queued | analyzing | scripting | recording | voicing | rendering | done | held | error "progress": 100, "videoUrl": "https://pagetovid.com/media/cmq…/video.mp4", "posterUrl": "https://pagetovid.com/media/cmq…/scene-2.png", "subtitleUrl": "https://pagetovid.com/media/cmq…/subtitles.vtt", "durationMs": 41000, "width": 1920, "height": 1080, "score": 82, // the reviewer's score, or null "qualityGate": { "minScore": 70, "onFail": "hold", "result": "passed", "held": false }, "og": { "og:video": "…", "og:image": "…", "og:video:type": "video/mp4", … } }
Sitios, plan y programas
El plan del sitio y los programas que un asistente gestiona por el MCP también tienen puertas REST, con la misma clave. Un sitio se identifica por su dominio.
- GET/POST
/api/v1/sites/:domain/plan— el plan de un sitio entero — los vídeos que debería tener, cada uno con objetivo, brief y planos. POST lo genera o regenera (gratis). - GET/POST
/api/v1/sites/:domain/plan/videos— el conjunto de vídeos que pide el plan, con el vídeo en que ya se convirtió cada idea. POST previsualiza; POST con confirm: true los lanza, 40 créditos cada uno. - GET/POST
/api/v1/sites/:domain/shows— los programas del sitio — vídeos recurrentes desde un feed RSS, Atom o JSON: el mismo storyboard en cada episodio, datos nuevos en cada episodio. POST crea uno; empieza en espera. - POST
/api/v1/shows/:id/run— lanza un programa a mano. Sin confirm, previsualiza los episodios gratis; confirm: true los crea (40 créditos cada uno) y, en un programa en espera, lo confirma — el planificador horario lo emite después, dentro de sus límites por emisión y por día. PATCH /api/v1/shows/:id pausa, reanuda o ajusta.
POST /api/v1/sites/example.com/shows
json{ "name": "Release notes", "kind": "video", "feed_kind": "rss", "feed_url": "https://example.com/changelog.xml", "cadence": "weekly", "max_episodes_per_run": 1, "max_credits_per_day": 80, "aspect_ratio": "9:16", "language": "en", "goal": "explainer", "target_seconds": 30, "focus_template": "This episode is about {{title}}: {{summary}}. Lead with what is new." }
201 { "ok": true, "show": { "show_id", "confirmed": false, … } }, y luego POST /api/v1/shows/:id/run { "confirm": true }. max_credits_per_day limita lo que el programa puede gastar al día: aquí 2 vídeos.
Un programa nunca gasta antes de que su propietario haya visto una emisión, nunca más de max_credits_per_day al día, y nunca crea dos veces el mismo elemento del feed.
Más endpoints
La misma clave Bearer abre estos. Los campos de la petición son los que lee cada ruta; las respuestas se muestran tal como llegan.
- POST
/api/v1/videos/:id/translate— cuerpo { languages: de 1 a 6 códigos de idioma, idempotencyKey? } (o la cabecera Idempotency-Key). Un vídeo nuevo por idioma, 40 créditos cada uno: se conservan el storyboard y las imágenes y se traducen las palabras. 202 → { sourceVideoId, videos: [{ language, ok, video_id, status, credits_charged, … }], creditsCharged }. Requiere el mismo plan que POST /api/v1/videos. - PATCH
/api/v1/shows/:id— cuerpo { active?, cadence?, max_episodes_per_run?, max_credits_per_day?, name? }: pausa, reanuda o ajusta un programa → { ok: true }. - GETPOST
/api/v1/sites— GET lista tus sitios: [{ id, domain, name, createdAt }]. POST { url, name? } registra el sitio al que pertenece una URL → 201 { ok, site }. - GET
/api/v1/sites/:domain— un sitio → { known, domain, name, createdAt, videos, assets, public_videos, shows, formats, session, site_url, channel_url, feed_url, credits }. Un dominio que no has filmado responde { known: false, domain }. - GETPUT
/api/v1/sites/:domain/brand-kit— el kit de marca del sitio → { ok, domain, exists, kit, logo_url, font_url, film_theme, painted, not_painted, watermark_effective, watermark_from }. PUT es una actualización parcial: una clave enviada reemplaza, null borra, una clave ausente se conserva. Una clave desconocida se rechaza con la lista aceptada; un ajuste que tu plan no incluye responde 402. - GETPOSTDELETE
/api/v1/sites/:domain/session— la sesión de captura del sitio, que permite grabar detrás de un inicio de sesión. POST { storage_state } (un storage state de Playwright) o { login_url, email, password } de una cuenta de prueba, con origin, label y ttl_hours opcionales → 201 { ok, domain, how, session, origins }; la contraseña nunca se guarda. GET → { ok, domain, sessions, capture_allowed, capture_plan }, nunca las cookies. DELETE olvida todas las sesiones del sitio → { ok, removed }. Por debajo del plan Scale, POST responde 402. - POST
/api/v1/sites/:domain/record-requests— pide a la extensión de Chrome que grabe los recorridos del sitio. POST { flowIds? } (ninguno = todos los que aún no están grabados) → 201 { ok, request }; una petición nueva sustituye a la pendiente. El navegador la muestra y una persona pulsa Grabar; una petición que nadie toma caduca a los 30 minutos. - GETPATCH
/api/v1/record-requests— las peticiones de grabación abiertas de quien llama, cada una con sus recorridos, para el navegador: GET → { ok, requests, pollMs }. GET /api/v1/record-requests/:id sigue una (pending, claimed, done, declined o expired, con el resultado por recorrido); PATCH { status: claimed | done | declined, result? } la avanza, 409 si ya cambió. - GETPOST
/api/v1/characters— identidades que se mantienen coherentes entre clips generados. GET → { ok, characters }. POST { name, refs? (URL de imágenes de esta cuenta), generate? (dibuja una referencia por 40 créditos), description?, aspect?, site_url? } → 201 { ok, character, credits_charged }. DELETE /api/v1/characters/:name → { ok, name }. - GETPOST
/api/v1/connections— los servidores externos que pueden llamar los resolvers de un formato, por alias. GET → { ok, connections }. POST { alias, kind: mcp | http, url, secret?, note? } → 201 { ok, connection }; un secreto se acepta y nunca se devuelve. DELETE /api/v1/connections/:alias → { ok, alias }. - GET
/api/v1/commons— la biblioteca compartida, sin clave: ?kind=image|video, tag, q, limit (hasta 200) → { ok, assets: [{ id, url, kind, tags, prompt, attribution, licence, created_at }] }. Poner en un vídeo el recurso generado de otra cuenta pasa por «Usar recurso» en la aplicación, que paga a su autor. - GET
/api/v1/sites/:domain/formats— los formatos del sitio (la forma versionada que sigue un programa): format_id, name, kind, latest_version, shows, programme, updated_at. - POST
/api/v1/shows/:id/webhook— sin clave API: envía elementos a un programa cuyo feed_kind es webhook. El cuerpo es items: un array de 1 a 50 objetos, cada uno con un title y, si quieres, key, link, summary, image y date. Firma el cuerpo bruto con el secreto del propio programa: X-PageToVid-Signature es sha256= seguido de su HMAC-SHA256 en hexadecimal. Un programa sin confirmar responde held y no gasta nada. - PUT
/api/v1/uploads/:token— sin clave API: envía el archivo a la upload_url que devolvió create_upload (curl -T), hasta 30 MB; 413 por encima.
Úsalo como vídeo Open Graph (SEO / social)
Un vídeo terminado también sirve como rica vista previa og:video — tus enlaces se despliegan como un vídeo reproducible en redes y buscadores. La respuesta de estado te da las etiquetas exactas en og. Colócalas en el <head> de tu página:
html<meta property="og:image" content="POSTER_URL" /> <meta property="og:video" content="VIDEO_URL" /> <meta property="og:video:secure_url" content="VIDEO_URL" /> <meta property="og:video:type" content="video/mp4" /> <meta property="og:video:width" content="1920" /> <meta property="og:video:height" content="1080" /> <meta name="twitter:card" content="player" />
Automatízalo
Genera + espera, en cualquier lenguaje.
javascriptconst KEY = process.env.PAGETOVID_API_KEY; const create = await fetch("https://pagetovid.com/api/v1/videos", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://your-site.com", goal: "ad" }), }).then((r) => r.json()); let v; do { await new Promise((r) => setTimeout(r, 5000)); v = await fetch(create.statusUrl, { headers: { Authorization: `Bearer ${KEY}` } }).then((r) => r.json()); } while (!["done", "held", "error"].includes(v.status)); console.log(v.videoUrl, v.posterUrl, v.og);
pythonimport os, time, requests KEY = os.environ["PAGETOVID_API_KEY"] H = {"Authorization": f"Bearer {KEY}"} r = requests.post("https://pagetovid.com/api/v1/videos", headers=H, json={"url": "https://your-site.com", "goal": "ad"}).json() while True: time.sleep(5) v = requests.get(r["statusUrl"], headers=H).json() if v["status"] in ("done", "held", "error"): break print(v["videoUrl"], v["posterUrl"], v["og"])
Errores y límites
401— clave API no válida o ausente.402— sin créditos, o la API no está en tu plan (código UPGRADE_REQUIRED). Recarga o mejora tu plan en precios.400— entrada no válida (se requiere una url).404— no encontrado: el vídeo, el sitio, el programa, el personaje o la conexión no está en esta cuenta.409— un conflicto: un render con esta Idempotency-Key aún está arrancando, la clave se usó para otra petición, o el vídeo está ocupado.422— entendido pero no hecho: un inicio de sesión que no entró, o una traducción en la que ningún idioma pudo arrancar.429— demasiadas peticiones; cuando se envía una cabecera Retry-After, indica cuántos segundos esperar.500/502— falló algo de nuestro lado, o un servicio al que llamábamos. Un crédito gastado en lo que falló se devuelve; vuelve a intentarlo en un momento.- El renderizado es async — tarda unos minutos; consulta la URL de estado.
- CORS está habilitado en /api/v1/videos (crear, consultar, traducir), así que puedes llamarlos directamente desde el navegador; las demás rutas son para servidores.
Obtén tu clave API
La API REST viene con el plan Pro y superiores, desde 49 $ al mes. En todos los planes, el gratuito incluido, tu asistente de IA crea vídeos por el MCP — empieza por ahí, gratis.