API do Polf
Transforme vídeo longo em cortes verticais (Reels/TikTok/Shorts) com legenda PT-BR queimada — por API. Você manda a URL do vídeo, a gente devolve os melhores cortes prontos, com título, gancho, legenda social e nota honesta de 0 a 10.
- REST + JSON, assíncrona:
create→ webhook ou polling →download. Base:https://api.polf.ai. - Contrato compatível com a Vizard (mesmos nomes no create/query) — quem já integra Vizard troca a base URL e a chave.
- Extras v2: webhooks nomeados e assinados (HMAC), link de download durável (não expira),
idempotencyKeyanti-cobrança-dupla, sandboxpolf-test://pra testar tudo em CI sem gastar crédito, e publicação social direto da API. - Multi-tenant: mande
externalUserId(o ID do SEU usuário) no create — ele volta ecoado em toda resposta e webhook, pra você rotear sem consulta.
Precisa de uma chave? A API é liberada por cliente. Fale com a gente pelo app ou pelo e-mail de suporte — você recebe a INSTACUT_API_KEY e o webhook_secret (mostrados uma única vez).
Quickstart
Fluxo completo em 3 chamadas: criar → acompanhar → baixar. Cole no terminal trocando a chave e a URL:
# 1) Crie o projeto — o vídeo longo entra, os cortes saem
curl -s -X POST https://api.polf.ai/api/v1/project/create \
-H "Content-Type: application/json" \
-H "INSTACUT_API_KEY: ik_SUA_CHAVE" \
-d '{
"videoUrl": "https://www.youtube.com/watch?v=XXXX",
"lang": "pt",
"maxClipNumber": 6,
"idempotencyKey": "meu-job-001",
"webhookUrl": "https://seu-app.com/webhooks/polf"
}'
# → {"code": 2000, "projectId": "3021", "errMsg": ""}
# 2) Acompanhe: por WEBHOOK (recomendado) ou polling a cada 20-30s
curl -s https://api.polf.ai/api/v1/project/query/3021 \
-H "INSTACUT_API_KEY: ik_SUA_CHAVE"
# processando → {"code": 1000, "status": "processing", "stage": "render",
# "clipsReady": 2, "clipsTotal": 6, ...}
# pronto → {"code": 2000, "status": "done", "videos": [...], ...}
# 3) Baixe o mp4 pelo link DURÁVEL (não expira; -L segue o redirect 302)
curl -L -o clip.mp4 \
-H "INSTACUT_API_KEY: ik_SUA_CHAVE" \
"https://api.polf.ai/api/v1/clips/CLIP_ID/download"Quer testar sem gastar crédito? Use "videoUrl": "polf-test://success" — o fluxo inteiro (webhooks, status, download) roda simulado em ~5 segundos. Veja Sandbox.
Autenticação
Toda chamada leva a sua chave no header INSTACUT_API_KEY (formato ik_…):
curl https://api.polf.ai/api/v1/project/query/3021 \
-H "INSTACUT_API_KEY: ik_SUA_CHAVE"- Chave ausente/inválida →
HTTP 401. - Junto com a chave você recebe o
webhook_secret(whsec_…), usado pra verificar a assinaturaPolf-Signaturedos webhooks — os dois são exibidos uma única vez; guarde num secret manager. - Nunca exponha a chave no front-end: chame a API só do seu servidor.
Workflow
- Create —
POST /api/v1/project/createcom avideoUrl(YouTube, Google Drive ou mp4 direto). Resposta imediata comprojectId; o processamento roda assíncrono. - Acompanhe — registre um
webhookUrlno create e receba os eventosproject.selected→clip.rendered(um por clip) →project.completed. Sem webhook, faça polling doquerya cada 20–30s. O polling é sempre o fallback confiável — reconcilie por ele se um webhook se perder. - Baixe — cada clip tem um
downloadUrldurável (não expira) e umstreamUrlpresigned pra tocar no seu player (expira ~1h). - (Opcional) Publique — conecte as contas sociais do seu usuário final e poste/agende o clip por
POST /api/v1/posts.
Estados do projeto (status, string): queued → processing → done | failed. O campo stage diz a fase atual (download → transcribe → select → render), e clipsReady/clipsTotal é contagem real (não inventamos porcentagem).
Criar projeto
Submete um vídeo longo pra clipagem. Nomes compatíveis com a Vizard; campos [hint] são aceitos por compatibilidade mas não alteram a saída hoje (honestidade > marketing).
| Campo | Tipo | Descrição |
|---|---|---|
videoUrl | string · obrigatório | YouTube, Google Drive ou URL direta de mp4. Aceita as fontes mágicas polf-test://… do sandbox. |
lang | string | Idioma da transcrição/legenda (default pt — PT-BR nativo). |
externalUserId | string | ID do SEU usuário final (multi-tenant). Ecoado em toda resposta e webhook; entra na chave de idempotência default. |
idempotencyKey | string | Dedupe anti-cobrança-dupla: reenvio com a MESMA chave devolve o MESMO projeto, sem nova cobrança. Sem ela, geramos uma chave estável a partir dos inputs (fonte + janela + seleção) — retry de rede idêntico também não recobra. |
webhookUrl | string | Recebe os webhooks v2 + o aviso v1 de conclusão. Precisa ser host público (anti-SSRF). |
maxClipNumber | int 1–100 | Corta os top-N clips por nota. Fora da faixa → código 4006. |
ratioOfClip | int 1–4 | 1 = 9:16 · 2 = 1:1 · 3 = 4:5 · 4 = 16:9. |
preferLength | int[] | 0 auto · 1 <30s · 2 30–60 · 3 60–90 · 4 >90s. Best-effort: prioriza a banda, não garante. |
keyword | string | Clipagem direcionada: foca a seleção num tema/momento específico. |
videoQuality | string | Qualidade do download da fonte: "720p" (≈metade do tempo) · "1080p" (default). |
removeSilenceSwitch | int 0/1 | Remove silêncio e filler words. |
headlineSwitch / emojiSwitch | int 0/1 | Headline/gancho por IA no topo do quadro · emoji na legenda. |
captionPreset / captionAnim | string | Estilo da legenda (word-focus/vizard/hype/minimalist/line-focus) e animação (karaoke/word_pop/typewriter/bounce/fade). |
clipFormat / headlineStyle | string | Layout do quadro (full/bars/blur) · estilo da headline (vermelha/branca/rasgada/pincelada/fita). |
processStartS / processDurationS | float | Janela: começa em N s / processa só N s da fonte (capado a um teto server-side). Economiza crédito em vídeos gigantes. |
projectName | string | Nome do projeto (aparece no query/list). |
videoType / ext / subtitleSwitch / highlightSwitch / templateId | [hint] | Aceitos por compat-Vizard; NÃO alteram a saída hoje. |
{
"code": 2000,
"projectId": "3021",
"shareLink": null,
"errMsg": "",
"externalUserId": "user_42"
}A cobrança é por segundos de fonte processados, reservados atomicamente no create (sem crédito → código 4007, nada é criado).
Consultar projeto
Devolve o envelope compat-Vizard (code + videos[]) MAIS os campos v2: status string, stage, clipsReady/clipsTotal, externalUserId ecoado e — na falha — o objeto error estruturado.
code | Significado |
|---|---|
1000 | Processando — continue o polling (20–30s). stage diz a fase. |
2000 | Pronto. videos[] pode vir vazio (= não achamos corte bom o bastante; ainda é sucesso — clipsTotal: 0 sinaliza). |
4006 | Projeto não encontrado (ou não é da sua chave). |
4xxx | Falhou — veja o catálogo de erros (o campo error traz errorCode/retriable). |
{
"code": 2000,
"projectId": "3021",
"projectName": "Integração",
"status": "done",
"stage": "render",
"externalUserId": "user_42",
"clipsReady": 6,
"clipsTotal": 6,
"videosReady": 6,
"videosTotal": 6,
"videos": [
{
"videoId": "8a1f0c02-7e11-4a9e-9a71-3f2f6f1b2c9d",
"title": "O erro que derruba 9 em cada 10 canais",
"videoMsDuration": 42000,
"viralScore": "8.5",
"viralReason": "gancho forte nos 3 primeiros segundos",
"transcript": "…",
"ready": true,
"videoUrl": "https://…presigned… (expira ~1h)",
"streamUrl": "https://…presigned… (expira ~1h)",
"downloadUrl": "https://api.polf.ai/api/v1/clips/8a1f0c02-…/download",
"thumbUrl": "https://…presigned…",
"thumbnailUrl": "https://…presigned…",
"status": "rendered",
"hook": "Você está fazendo isso errado",
"socialCaption": "O corte que muda o jogo #cortes",
"layoutType": "fill",
"captionLang": "pt",
"clipEditorUrl": "/editor?clip=8a1f0c02-…",
"relatedTopic": "[]",
"disliked": false,
"starred": false
}
]
}ready: false em um clip = selecionado mas o mp4 ainda está queimando (chega sozinho; ou force com o export). videoUrl/streamUrl são presigned e expiram (~1h) — pra guardar/baixar use sempre o downloadUrl durável.
{
"code": 4008,
"projectId": "3022",
"projectName": "Integração",
"videos": [],
"errMsg": "Não conseguimos baixar este vídeo — ele pode estar privado, removido, restrito por idade/região, ou ser muito longo. Confira o link e as permissões.",
"status": "failed",
"stage": "download",
"externalUserId": "user_42",
"clipsReady": 0,
"clipsTotal": 0,
"error": {
"errorCode": "SOURCE_UNAVAILABLE",
"message": "Não conseguimos baixar este vídeo — ele pode estar privado, removido, restrito por idade/região, ou ser muito longo. Confira o link e as permissões.",
"retriable": false,
"stage": "download"
}
}Download durável v2
O link que não expira: responde 302 pra uma URL presigned fresca (1h) com Content-Disposition: attachment (no celular o arquivo salva em vez de abrir numa aba). É o downloadUrl que o query e os webhooks entregam — guarde ele no seu banco em vez do presigned.
| HTTP | Significado |
|---|---|
302 | Redirect pra presigned fresca — use curl -L / siga o redirect. |
409 | Clip existe mas ainda sem mp4 — dispare o export e tente de novo. |
410 | Arquivo passou da retenção de 30 dias — reprocesse o projeto. |
404 | Clip não encontrado (ou não é da sua chave). |
Export de um clip
Garante a queima do mp4 de UM clip (quando o query devolveu videoUrl: null). Idempotente: re-POST não re-queima nem recobra — devolve o mesmo renderJobId (determinístico, clipId:formato) com o status atual.
{
"code": 2000,
"clipId": "8a1f0c02-…",
"renderJobId": "8a1f0c02-…:9x16",
"status": "exporting"
}status ∈ done | exporting | failed. Acompanhe pelo query (ou pelo webhook clip.rendered) até videoUrl != null.
Listar projetos
Lista paginada dos projetos da sua chave (cursor keyset, estável sob inserção — padrão Stripe/GitHub).
| Query param | Descrição |
|---|---|
externalUserId | Filtra pelos projetos de UM usuário final seu. |
status | queued | processing | done | failed. |
limit | 1–50 (default 20). |
cursor | Opaco. Repasse o nextCursor da página anterior; nextCursor: null = acabou. |
{
"code": 2000,
"projects": [
{
"projectId": "3021",
"projectName": "Integração",
"createdAt": "2026-07-02T14:03:07+00:00",
"status": "done",
"stage": "render",
"externalUserId": "user_42",
"clipsReady": 6,
"clipsTotal": 6
},
{
"projectId": "3020",
"projectName": "Integração",
"createdAt": "2026-07-02T11:41:22+00:00",
"status": "failed",
"stage": "download",
"externalUserId": "user_17",
"clipsReady": 0,
"clipsTotal": 0,
"error": {
"errorCode": "SOURCE_UNAVAILABLE",
"message": "O link enviado é inválido ou não aponta para um vídeo.",
"retriable": false,
"stage": "download"
}
}
],
"nextCursor": "MjAyNi0wNy0wMlQxMTo0MToyMiswMDowMHwzMDIw",
"errMsg": ""
}Uso & saldo
Meça seu próprio consumo sem sair da API: créditos em minutos de vídeo fonte (usados / limite / restantes no período) e, quando a publicação social está habilitada pra sua chave, posts do mês vs cota. Cheque o saldo antes de criar um projeto grande.
{
"code": 1000,
"credits": {
"unit": "source_minutes",
"used": 42.5,
"limit": 300,
"remaining": 257.5,
"period": "month",
"periodStart": "2026-07-01T00:00:00+00:00"
},
"posts": { "month": 12, "limit": 300 },
"suspended": false
}Webhooks
Registre um webhookUrl no create e receba POSTs a cada transição relevante — sem polling. Todo evento v2 usa o mesmo envelope:
{
"event": "clip.rendered",
"eventId": "b1e6a7a2-4a4e-4c1b-9d2f-8f0f4b6a2e11",
"createdAt": "2026-07-02T14:07:31+00:00",
"apiVersion": "2026-07-01",
"projectId": "3021",
"externalUserId": "user_42",
"data": { }
}- Entrega at-least-once: deduplique por
eventId. A ordem NÃO é garantida (umclip.renderedpode chegar antes doproject.selected) — trate cada evento de forma independente. - Responda 2xx rápido (menos de 15s) e processe async. Falhou? A gente re-tenta 3× na hora (imediata, +5s, +30s) e depois segue re-tentando em background por até 24 horas (a cada ~15min, até 8 tentativas no total) — o MESMO
eventIdem toda re-entrega, então sua deduplicação já resolve. O polling continua sendo o fallback final — reconcilie peloquery. - Todo projeto termina com um evento terminal:
project.completedouproject.failed— inclusive quando o processamento é interrompido no servidor. Você nunca fica esperando pra sempre. - apiVersion (
2026-07-01): campos novos podem ser adicionados sem aviso; nomes existentes nunca mudam.
O objeto clip canônico
O MESMO shape em todos os eventos e no query (nomes idênticos — sem surpresa thumbUrl×thumbnailUrl):
{
"clipId": "8a1f0c02-7e11-4a9e-9a71-3f2f6f1b2c9d",
"projectId": "3021",
"title": "O erro que derruba 9 em cada 10 canais",
"hook": "Você está fazendo isso errado",
"socialCaption": "O corte que muda o jogo #cortes",
"viralScore": "8.5",
"viralReason": "gancho forte nos 3 primeiros segundos",
"durationMs": 42000,
"ratio": "9:16",
"captionLang": "pt",
"status": "rendered",
"streamUrl": "https://…presigned… (expira ~1h; null enquanto status=selected)",
"downloadUrl": "https://api.polf.ai/api/v1/clips/8a1f0c02-…/download",
"thumbnailUrl": "https://…presigned… (existe DESDE a seleção)",
"width": null,
"height": null,
"sizeBytes": null
}status ∈ selected | rendering | rendered | failed. Antes de rendered, streamUrl é null (o thumbnail existe desde a seleção). width/height/sizeBytes ainda não são medidos — null honesto por enquanto.
Eventos
| Evento | Quando dispara | data |
|---|---|---|
project.selected | A seleção terminou: os cortes foram escolhidos (mp4 ainda queimando). | clips[] (canônicos, sem mp4) + clipsTotal/clipsReady |
clip.rendered | UM clip terminou de queimar (dispara por clip). | clip completo (com streamUrl/downloadUrl) + clipsReady/clipsTotal |
clip.failed | A queima de UM clip falhou. | clipId + error |
project.completed | Todos os clips do projeto queimados (ou sucesso-vazio, sem cortes bons). | clipsReady/clipsTotal |
project.failed | Falha PERMANENTE do projeto (transitórias re-tentam sozinhas antes). | error (errorCode/message/retriable/stage) |
post.published | Post social chegou em terminal publicado (inclui parcial). | postId/status/targets[] |
post.failed | Post social falhou em todas as redes. | postId/status/targets[] (erro por rede) |
{
"event": "project.selected",
"eventId": "…",
"createdAt": "2026-07-02T14:05:02+00:00",
"apiVersion": "2026-07-01",
"projectId": "3021",
"externalUserId": "user_42",
"data": {
"clips": [ { "clipId": "…", "status": "selected", "streamUrl": null,
"thumbnailUrl": "https://…", "viralScore": "8.5", "…": "…" } ],
"clipsTotal": 6,
"clipsReady": 0
}
}{
"event": "clip.rendered",
"eventId": "…",
"createdAt": "2026-07-02T14:07:31+00:00",
"apiVersion": "2026-07-01",
"projectId": "3021",
"externalUserId": "user_42",
"data": {
"clip": { "clipId": "…", "status": "rendered",
"streamUrl": "https://…presigned…", "downloadUrl": "https://…/download", "…": "…" },
"clipsReady": 1,
"clipsTotal": 6
}
}{
"event": "clip.failed",
"eventId": "…",
"projectId": "3021",
"externalUserId": "user_42",
"apiVersion": "2026-07-01",
"createdAt": "…",
"data": {
"clipId": "8a1f0c02-…",
"error": {
"errorCode": "RENDER_FAILED",
"message": "Falha ao renderizar os cortes. Tente gerar de novo.",
"retriable": true,
"stage": "render"
}
}
}{
"event": "project.completed",
"eventId": "…",
"projectId": "3021",
"externalUserId": "user_42",
"apiVersion": "2026-07-01",
"createdAt": "…",
"data": { "clipsReady": 6, "clipsTotal": 6 }
}{
"event": "project.failed",
"eventId": "…",
"projectId": "3022",
"externalUserId": "user_42",
"apiVersion": "2026-07-01",
"createdAt": "…",
"data": {
"error": {
"errorCode": "SOURCE_UNAVAILABLE",
"message": "Não conseguimos baixar este vídeo — ele pode estar privado, removido, restrito por idade/região, ou ser muito longo. Confira o link e as permissões.",
"retriable": false,
"stage": "download"
}
}
}{
"event": "post.published",
"eventId": "…",
"projectId": null,
"externalUserId": "user_42",
"apiVersion": "2026-07-01",
"createdAt": "…",
"data": {
"postId": "p_9f31…",
"status": "published",
"targets": [
{ "targetId": "p_9f31…:TIKTOK", "platform": "tiktok",
"status": "published", "platformPostUrl": "https://www.tiktok.com/@user/video/…" },
{ "targetId": "p_9f31…:INSTAGRAM", "platform": "instagram",
"status": "failed", "platformPostUrl": null,
"error": { "errorCode": "PUBLISH_FAILED",
"message": "…detalhe da rede…", "retriable": true } }
]
}
}Webhook v1 (legado, continua saindo)
Além dos eventos v2, a MESMA URL recebe o aviso magro de conclusão do contrato original (sem envelope, sem event):
{ "code": 2000, "projectId": "3021", "jobId": "uuid", "clips": 6 }clips: 0 = processou mas sem cortes bons (ainda sucesso). Integrações novas devem usar os eventos v2 — o v1 se mantém por compatibilidade.
Verificação HMAC (Polf-Signature)
Todo webhook (v1 e v2) leva o header Polf-Signature: t=<unix>,v1=<hmac_hex>, onde v1 = HMAC-SHA256(webhook_secret, "<t>.<corpo_cru>") — formato Stripe-style. Verifique SEMPRE: assinatura sobre o corpo cru, comparação em tempo constante e janela anti-replay de 5 minutos.
import crypto from "node:crypto";
// IMPORTANTE: a assinatura é do corpo CRU. No Express, use
// express.raw({ type: "application/json" }) NESTA rota (não o json parser).
export function verifyPolfSignature(
rawBody: string,
signatureHeader: string, // header "Polf-Signature": t=1751464051,v1=5257a86…
secret: string, // whsec_… (entregue UMA vez junto com a sua API key)
toleranceS = 300
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split(/=(.+)/).slice(0, 2))
);
if (!parts.t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceS) return false; // anti-replay
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b); // tempo-constante
}
// app.post("/webhooks/polf", express.raw({ type: "application/json" }), (req, res) => {
// if (!verifyPolfSignature(req.body.toString("utf8"),
// req.header("Polf-Signature") ?? "", process.env.POLF_WEBHOOK_SECRET!)) {
// return res.status(400).end();
// }
// const evt = JSON.parse(req.body.toString("utf8"));
// // deduplique por evt.eventId (entrega at-least-once) e responda 2xx RÁPIDO
// res.status(200).end();
// });import hmac, hashlib, time
def verify_polf_signature(raw_body: bytes, header: str, secret: str, tol: int = 300) -> bool:
"""header = request.headers["Polf-Signature"] (formato: t=<unix>,v1=<hmac_hex>)
secret = whsec_… (entregue UMA vez junto com a sua API key)."""
p = dict(kv.split("=", 1) for kv in header.split(","))
if abs(time.time() - int(p["t"])) > tol: # anti-replay (janela de 5 min)
return False
expected = hmac.new(secret.encode(),
p["t"].encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, p["v1"]) # comparação tempo-constante
# FastAPI:
# @app.post("/webhooks/polf")
# async def hook(request: Request):
# raw = await request.body() # corpo CRU (antes de parsear)
# if not verify_polf_signature(raw, request.headers["Polf-Signature"], SECRET):
# raise HTTPException(400)
# evt = json.loads(raw) # dedup por evt["eventId"]
# return {"ok": True} # 2xx rápido; processe asyncSandbox exclusivo Polf
Teste o fluxo INTEIRO — create, webhooks, polling, download e os erros — no seu CI, sem gastar crédito, GPU ou download. Basta usar uma fonte mágica polf-test:// no lugar da videoUrl (padrão test-cards do Stripe, transposto pro clipping):
| Fonte mágica | O que simula |
|---|---|
polf-test://success | Sucesso em ~5s: 2 clips fake ("[SANDBOX] …") com mp4/download REAIS (asset fixo 3s 9:16) e webhooks na ordem de produção: project.selected → clip.rendered ×2 → project.completed. |
polf-test://fail-source | Falha no download em ~2s: project.failed com { "errorCode": "SOURCE_UNAVAILABLE", "retriable": false }. |
polf-test://fail-render | Seleção ok → queima falha: project.selected → clip.failed → project.failed com { "errorCode": "RENDER_FAILED", "retriable": true }. |
polf-test://slow | Fica processing (stage render) por ~10 min SEM evento terminal — teste o SEU teto anti-limbo/timeout — e então completa como o success. |
# roda no seu CI sem gastar crédito, GPU ou download
curl -s -X POST https://api.polf.ai/api/v1/project/create \
-H "Content-Type: application/json" \
-H "INSTACUT_API_KEY: ik_SUA_CHAVE" \
-d '{"videoUrl": "polf-test://success", "webhookUrl": "https://seu-app.com/webhooks/polf"}'
# em ~5s: project.selected → clip.rendered ×2 → project.completed
# os clips fake vêm marcados "[SANDBOX] …" e o mp4/downloadUrl funcionam DE VERDADE
# (asset fixo de 3s 9:16) — dá pra testar o download durável de ponta a ponta.
# Todos os payloads de sandbox levam "sandbox": true no data.- Custo zero de verdade: não reserva nem debita crédito, não usa GPU/download.
- Todos os payloads de sandbox levam
"sandbox": truenodata— filtre nos seus handlers se precisar. - Sem
idempotencyKey, cada chamada de sandbox cria um job NOVO (o CI pode repetir a mesma URL); com a chave explícita, a idempotência vale normal. - Contrato público: os nomes
polf-test://success|fail-source|fail-render|slownunca mudam — só adicionamos modos novos.
Catálogo de erros
Dois níveis, os dois estáveis: o code numérico do envelope (compat-Vizard) e o errorCode string do objeto error v2 (em query, list e webhooks). retriable: true = falha transitória (timeout/instabilidade de provedor) — vale reprocessar; false = permanente (arrume a causa antes). Pra reprocessar um projeto que falhou, mande um novo create com um idempotencyKey NOVO (a chave antiga devolve o projeto falho).
errorCode (v2 — string estável)
| errorCode | Quando acontece | retriable típico |
|---|---|---|
SOURCE_UNAVAILABLE | Link inválido/privado/removido/restrito, arquivo corrompido, fonte expirada ou download falhou. | false (link ruim) · true quando o serviço de download caiu temporariamente |
NO_CREDITS | Créditos insuficientes pra processar. | false — recarregue e reenvie |
LANGUAGE_UNSUPPORTED | O vídeo não é falado em português (por enquanto só PT-BR). | false |
RENDER_FAILED | A queima (render/legendas) falhou. | geralmente true (timeout/5xx) |
INTERNAL | Falha interna do pipeline (transcrição, seleção, interrupção). | varia — confie no retriable do payload |
NO_CLIPS | Reservado. Hoje “sem cortes bons” NÃO é falha: vem como status: done com clipsTotal: 0 (code 2000, videos: []). | — |
PUBLISH_FAILED | Só em posts sociais: a rede recusou a publicação (o detalhe vem no error.message do target). | repassado da rede (retriable honesto por alvo) |
RATE_PROTECTED | Só em posts sociais (HTTP 429): a trava segura da conta foi atingida (teto/dia, espaçamento ou rampa de conta nova) — vem com nextSlot. Veja Travas de postagem. | true — re-tente no nextSlot ou use scheduledAt: "auto" |
code do envelope (compat-Vizard)
| code | Significado |
|---|---|
2000 | Sucesso (create aceito / projeto pronto). |
1000 | Processando (continue o polling). |
4002 | Falha ao enfileirar / clipagem falhou — reprocessável. |
4003 | Integração temporariamente indisponível (kill-switch) — re-tente depois. |
4004 / 4005 | Formato não suportado / arquivo corrompido. |
4006 | Parâmetro inválido, vídeo longo demais ou recurso não encontrado. |
4007 | Créditos insuficientes. |
4008 / 4009 | Download da fonte falhou / URL inválida. |
4010 | Idioma não suportado. |
4099 | Falha interna do pipeline. |
Nota de transporte: os endpoints de clipping respondem HTTP 200 com o code no corpo (padrão Vizard — parseie response.code). Os endpoints novos (download, social) usam status HTTP semânticos (302/401/402/404/409/410/429/503).
Rate limits
Limites por chave (não por IP), janela de 60s. Toda resposta autenticada de /api/v1/* leva os headers IETF RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset (segundos até liberar 1 slot) — programe o seu client por eles em vez de chutar.
| Rotas | Teto |
|---|---|
POST /api/v1/project/create | 20 req/min |
POSTs caros (/export, /publish…) | 20 req/min |
| Leitura B2B (query, projects, download, posts…) | 120 req/min (polling contínuo de 2 req/s não encosta) |
Estourou → HTTP 429 com Retry-After (em segundos). Respeite o header e re-tente — não martele.
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 23
Retry-After: 23
{"detail": "rate limit excedido, tente em instantes"}Retenção de arquivos
- MP4s e fontes: 30 dias. Depois disso o
downloadUrlresponde410 Gone— se precisar de novo, reprocesse o projeto. Se o seu fluxo guarda os vídeos, baixe e armazene do seu lado dentro da janela. - URLs presigned (
streamUrl/videoUrl/thumbnailUrl): ~1 hora. São pra tocar/exibir na hora. Nunca as persista — persista odownloadUrl(durável) e oclipId. - Metadados (projetos, clips, notas, transcrição): permanecem consultáveis pelo query/list normalmente.
- Métricas sociais: espelhadas 1×/dia; retenção de 30 dias do provedor.
OpenAPI & llms.txt
openapi.json— spec OpenAPI 3.1 das rotas públicas, gerada do código real (gere seus tipos comopenapi-typescriptou similar).llms.txt/llms-full.txt— esta documentação em formato pra agentes/LLMs (padrão llmstxt.org). Aponte seu agente de código pra cá e ele integra sozinho.
SDK TypeScript oficial
@polf_ai/sdk — cliente tipado (ESM+CJS, Node ≥ 18, zero dependências) com polling que respeita RateLimit-*/Retry-After e verificação de webhook estilo Stripe.
npm install @polf_ai/sdkimport Polf from "@polf_ai/sdk";
const polf = new Polf({ apiKey: process.env.POLF_API_KEY! });
const { projectId } = await polf.projects.create({
videoUrl: "https://www.youtube.com/watch?v=XXXX", // ou polf-test://success em CI
externalUserId: "user_42",
});
const project = await polf.projects.waitUntilDone(projectId);
for (const clip of project.videos) {
console.log(clip.title, clip.downloadUrl); // URL DURÁVEL (302 → fresca)
}const event = Polf.webhooks.constructEvent(rawBody, signatureHeader, secret);
// evento TIPADO: narrow por event.event (project.selected | clip.rendered |
// project.completed | project.failed | clip.failed | post.published | post.failed)
if (event.event === "clip.rendered") console.log(event.data.clip.downloadUrl);
// assinatura inválida/replay → lança WebhookVerificationError (responda 400)Feedback ou campo faltando? Fala com a gente — o contrato evolui só de forma aditiva.
Publicação social em rollout
Poste ou agende um clip nas redes conectadas do SEU usuário final (TikTok, Instagram, YouTube, Facebook, LinkedIn, X…) sem sair da API. Em rollout controlado: enquanto não estiver liberada pra sua chave, os endpoints respondem
HTTP 503 — “publicação social via API em breve”. O contrato abaixo é o definitivo.Conectar contas (OAuth hospedado)
O connect recebe
{ "returnUrl": "https://seu-app.com/volta", "platform": "tiktok"? }e devolve umaconnectUrl— redirecione seu usuário pra ela (portal OAuth hospedado; semplatformabre o portal com todas as redes). Depois, o GET lista as contas:{ accountId, platform, username, displayName, status }comstatus∈connected | pending_channel(OAuth ok mas falta escolher a página/canal — refaça a conexão).Publicar / agendar
targets[]: cada alvo apontaaccountId(preferido) OUplatform;captionausente = usa asocialCaptiondo clip.scheduledAtausente = publica já;"auto"= o Polf escolhe o próximo horário SEGURO da conta (a resposta devolve o horário resolvido). Agendado dá pra cancelar comDELETE /api/v1/posts/{postId}.409caso contrário — dispare o export antes) e o usuário precisa ter conectado contas (409com instrução).HTTP 402.Travas de postagem & conta aquecida
Pra proteger a conta do seu usuário final de flag/ban, o Polf aplica travas seguras por conta e por rede: teto de posts/dia, espaçamento mínimo entre posts e rampa de aquecimento pra conta recém-conectada (o teto começa em 1 post/dia e sobe a cada semana, chegando ao padrão da rede em ~14 dias). São proteções padrão Polf — nenhuma rede publica esses números oficialmente.
HTTP 429com{ "errorCode": "RATE_PROTECTED", "retriable": true, "nextSlot": "…" }: é transitório — re-tente nonextSlot, ou mandescheduledAt: "auto"pra agendar direto no próximo horário seguro.scheduledAt: "auto"resolve o próximo horário que respeita teto, espaçamento e rampa de TODAS as redes do post — a resposta ecoa o horário escolhido emscheduledAt.Acompanhar
Status do post:
processing | scheduled | published | partial | failed | draft | canceled(partial= publicou em parte das redes; o detalhe por rede vem emtargets[], complatformPostUrlreal e erro{ errorCode, message, retriable }repassado da rede). A lista pagina igual/api/v1/projects(cursor + filtrosexternalUserId/status). As métricas (views/likes/…por alvo) são espelhadas 1×/dia; X (Twitter) não expõe analytics.