Um webhook é uma dica assinada de que uma tarefa atingiu um estado terminal. Não é o registro em si. Portanto, a regra é curta: verifique os bytes brutos, deduplique pelo ID do evento, responda 2xx rapidamente e, em seguida, leia GET /v1/tasks/{id} para obter o resultado e o status de faturamento.
Os webhooks de tarefas de workspace foram lançados em 26/09/2026. Você obtém uma Management API para o ciclo de vida do webhook, entrega de teste, rotação de segredo e histórico de entrega. O Dashboard e o gerenciamento via MCP também estão disponíveis.
Uma correção inicial. Nosso guia de geração de imagens assíncronas anterior dizia que o TokenLab não tinha callback de tarefa; isso era verdade antes de 26/09/2026, e esse guia foi atualizado juntamente com este.
Webhooks ou polling? Use ambos
Eles resolvem problemas diferentes, e nenhum substitui o outro.
| Situação | Use |
|---|---|
| Você quer reagir no momento em que uma tarefa termina | Webhook |
| Você precisa do resultado autoritativo ou do custo | GET /v1/tasks/{id} |
| Seu receptor ficou fora do ar por um tempo | Polling com IDs de tarefa armazenados |
| Você quer um fallback caso as entregas desapareçam | Polling em um intervalo lento |
Webhooks não eliminam consultas de status e não adicionam um limite de polling. Mantenha ambos. Mesmo com webhooks ativos, um loop de reconciliação lento que lê seus IDs de tarefa armazenados é um seguro barato.
Se você usar polling, utilize poll_url, faça backoff enquanto a tarefa estiver pendente e pare em estados terminais. Pare em 401, 403, 404 ou quando error.retryable == false. Tente novamente 503 async_task_owner_unavailable com backoff. Uma tarefa ausente ou expirada retorna 404 async_task_not_found. Veja o guia de jobs assíncronos e polling para o contrato de polling.
Três credenciais, três funções
Misturá-las é a maneira mais rápida de quebrar seu receptor.
| Credencial | Prefixo | O que faz | Notas |
|---|---|---|---|
| Management Token | mt-… |
Cria, lista, atualiza, deleta, testa e rotaciona webhooks em /v1/management/webhooks* |
Enviado como Authorization: Bearer mt-…. Escopo de workspace |
| API key | sk-… |
Envia solicitações de modelo e lê o status da tarefa via GET /v1/tasks/{id} |
Rejeitado pela Management API |
| Signing secret | whsec_… |
Verifica entregas no seu receptor | Nunca é um token Bearer |
Duas coisas sobre o Management Token. Primeiro, ele também autoriza outras operações de gerenciamento de workspace, portanto, não é uma credencial exclusiva para webhooks. Escolha o mesmo workspace da API key que envia suas tarefas. Segundo, você o cria em Dashboard → API → Management Tokens. Veja outro exemplo de Management API.
Mantenha mt-… e whsec_… apenas no seu backend. Nunca envie nenhum deles para um navegador ou cliente móvel.
Crie um endpoint e armazene o segredo imediatamente
A chamada de criação retorna 201 com o id do webhook e um secret de uso único começando com whsec_…. Listar, obter e atualizar nunca mostrarão esse segredo novamente. Armazene-o no momento em que o vir.
export TOKENLAB_MANAGEMENT_TOKEN="mt-your-management-token"
curl https://api.tokenlab.sh/v1/management/webhooks \
-H "Authorization: Bearer $TOKENLAB_MANAGEMENT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"url":"https://your-app.example/webhooks/tokenlab","events":["task.completed","task.failed","task.timeout"],"description":"Production task results"}'
Os mesmos endpoints podem ser gerenciados de três maneiras, e todos os três editam os mesmos objetos:
- Dashboard → API → Webhooks
- A Management API
- MCP
As regras de URL são rígidas. O endpoint deve ser HTTPS público. Sem credenciais, query string ou fragmento na URL. Redirecionamentos não são seguidos, portanto, um 301 conta como uma entrega falha.
Você pode ter até 10 endpoints por workspace. Uma 11ª criação retorna 409 webhook_limit_reached.
| Método | Caminho | Objetivo |
|---|---|---|
GET |
/v1/management/webhooks |
Listar endpoints |
POST |
/v1/management/webhooks |
Criar um endpoint |
GET |
/v1/management/webhooks/{webhookId} |
Ler um endpoint |
PATCH |
/v1/management/webhooks/{webhookId} |
Atualizar, pausar ou retomar |
DELETE |
/v1/management/webhooks/{webhookId} |
Deletar |
POST |
/v1/management/webhooks/{webhookId}/rotate-secret |
Rotacionar o segredo de assinatura |
POST |
/v1/management/webhooks/{webhookId}/test |
Enviar um webhook.test |
GET |
/v1/management/webhooks/{webhookId}/deliveries?page=1&limit=50 |
Histórico de entrega, limite até 100 |
Pause com PATCH {"is_active": false}. Retome com PATCH {"is_active": true}. Retomar redefine a contagem de falhas consecutivas, o que é importante após uma interrupção.
O que realmente chega
Cada entrega é um POST com um envelope JSON. Os campos da Management API são snake_case, mas os campos de callback são camelCase. Não presuma que um formato de nomenclatura se aplica ao outro.
| Campo | Significado |
|---|---|
id |
ID do evento. Use-o para deduplicar |
type |
Tipo de evento |
created |
Segundos Unix |
data |
Payload do evento, a forma depende do evento |
| Evento | Dispara quando |
|---|---|
task.completed |
Tarefa finalizada com sucesso |
task.failed |
Tarefa terminou em falha |
task.timeout |
Tarefa atingiu seu limite de tempo |
webhook.test |
Enviado apenas pela operação de teste |
task.completed carrega taskType (por exemplo, video ou image), taskId, um model opcional, durationMs, resultUrls e settledCost.
task.failed carrega taskType, taskId, error, errorCode, retryable e refundOutcome.
task.timeout carrega taskType, taskId, refundOutcome e campos de tempo de espera. Leia o registro da tarefa para esses valores; o conjunto de campos depende da tarefa.
As assinaturas cobrem futuros eventos terminais para tarefas assíncronas no workspace. Resultados síncronos e tarefas históricas não são reproduzidos. Você recebe todas as tarefas do workspace para os tipos de evento que selecionou, então combine data.taskId com o ID que você armazenou quando criou a tarefa.
Campos podem estar ausentes dependendo da tarefa. É por isso que GET /v1/tasks/{id} com a chave sk-… do workspace original permanece a fonte da verdade para o resultado e o status de faturamento. O evento informa que algo terminou. O registro da tarefa informa o que foi produzido e quanto custou.
Mais uma coisa sobre retryable em um evento de falha. Ele descreve a falha na geração, não uma instrução para reenviar automaticamente. Um novo envio é uma nova tarefa faturável.
Verifique os bytes brutos, depois processe uma vez
Cada POST carrega três cabeçalhos:
X-Webhook-IDX-Webhook-Timestamp, segundos UnixX-Webhook-Signature, formatado comosha256=
A assinatura é HMAC-SHA256 sobre a string de timestamp exata, um ponto e os bytes do corpo da requisição bruta, codificados com o segredo whsec_… completo. A ordem importa, assim como o corpo.
Dois erros quebram as verificações de assinatura mais do que qualquer outra coisa:
- Verificar JSON analisado. Se você analisar o corpo e serializá-lo novamente, os bytes mudam e o HMAC não corresponderá. Leia o corpo bruto. Mantenha-o como bytes até que a verificação passe.
- Verificar com apenas um segredo durante a rotação. Após rotacionar, as entregas que já estão em trânsito ainda podem carregar a assinatura anterior. Aceite uma lista de segredos por uma janela curta.
O receptor Node abaixo é livre de dependências e usa node:http. Ele lê o corpo bruto, verifica em uma lista de segredos, verifica a janela de 300 segundos, compara o id do corpo com X-Webhook-ID, deduplica pelo ID do evento, enfileira e retorna 204. A deduplicação na amostra é um conjunto em memória; use uma restrição de banco de dados exclusiva em produção.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
// Durante a rotação, liste tanto o novo quanto o anterior segredo whsec_.
const SECRETS = (process.env.TOKENLAB_WEBHOOK_SECRETS ?? '').split(',').filter(Boolean);
const TOLERANCE_SECONDS = 300;
const seen = new Set(); // Use uma restrição de DB única em produção, não memória.
function verify(rawBody, headers) {
const timestamp = headers['x-webhook-timestamp'];
const signature = headers['x-webhook-signature'];
if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
if (typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
const received = Buffer.from(signature.slice(7), 'hex');
return SECRETS.some((secret) => {
const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
return timingSafeEqual(expected, received);
});
}
const server = createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/webhooks/tokenlab') {
res.writeHead(404).end();
return;
}
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const rawBody = Buffer.concat(chunks); // verifique os bytes exatos, antes do JSON.parse
if (!verify(rawBody, req.headers)) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(rawBody.toString('utf8'));
if (event.id !== req.headers['x-webhook-id']) {
res.writeHead(400).end();
return;
}
if (!seen.has(event.id)) {
seen.add(event.id);
enqueue(event); // entregue; faça o trabalho lento fora da requisição
}
res.writeHead(204).end();
});
});
function enqueue(event) {
console.log('queued', event.type, event.data?.taskId);
}
server.listen(Number(process.env.PORT ?? 3000));
O receptor foi testado localmente em 28/09/2026 contra requisições assinadas exatamente como o remetente de produção: entrega válida, entrega duplicada, segredo anterior durante a rotação, segredo errado, timestamp obsoleto, incompatibilidade de ID de cabeçalho e corpo, corpo adulterado e JSON re-serializado. Oito casos, todos aprovados. Uma duplicata foi enfileirada uma vez.
O lado Python é uma única função de verificação. Ele compara assinaturas com hmac.compare_digest e espera os bytes do corpo bruto do request.get_data() do Flask ou await request.body() do FastAPI.
import hashlib
import hmac
import re
import time
TOLERANCE_SECONDS = 300
SIGNATURE_RE = re.compile(r"^sha256=[a-f0-9]{64}$")
def verify_webhook(raw_body: bytes, headers, secrets: list[str]) -> bool:
"""Verifique um webhook do TokenLab contra um ou mais segredos whsec_.
raw_body deve ser os bytes exatos da requisição (Flask: request.get_data(),
FastAPI/Starlette: await request.body()), lidos antes de qualquer análise JSON.
"""
timestamp = headers.get("x-webhook-timestamp", "")
signature = headers.get("x-webhook-signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
if not SIGNATURE_RE.match(signature):
return False
received = signature.removeprefix("sha256=")
for secret in secrets:
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, received):
return True
return False
Testado em 28/09/2026: válido, segredo anterior, segredo errado, timestamp obsoleto, corpo adulterado e um corpo re-serializado com espaçamento padrão json.dumps. Seis casos, todos aprovados.
Além da assinatura, faça três coisas em cada requisição:
- Rejeite timestamps com mais de 300 segundos de diferença do momento atual. Isso são 5 minutos, e limita quão antigo um replay pode ser.
- Confirme que o
iddo corpo é igual aX-Webhook-ID. - Armazene o ID do evento junto com seu item de trabalho em uma gravação atômica, apoiada por uma restrição única. Em seguida, retorne
2xxrapidamente e faça o trabalho pesado a partir de sua própria fila.
As entregas podem se repetir e a ordem não é garantida. A janela de timestamp limita a idade do replay. A deduplicação por ID de evento evita o processamento duplo.
Retentativas, pausa automática e o runbook de recuperação
Cada ciclo de entrega faz até três tentativas.
| Tentativa | Espera antes dela | Timeout da tentativa |
|---|---|---|
| 1 | nenhuma | 10 s |
| 2 | 1 s | 10 s |
| 3 | 4 s | 10 s |
Fonte: Guia de webhook do TokenLab, observado em 28/09/2026.
Cada tentativa recebe um novo timestamp e assinatura. Isso significa que sua verificação de assinatura deve usar o timestamp da mesma requisição, não um valor em cache.
Respostas que permitem retentativa: falhas de rede, 429 e 5xx. Não são tentadas novamente dentro do ciclo: outros 4xx, redirecionamentos e destinos de rede inválidos. Falhas transitórias podem acionar retentativas posteriores do mesmo evento com o mesmo ID de entrega, o que é outra razão pela qual a deduplicação não é opcional.
Dez ciclos falhos consecutivos pausam o endpoint automaticamente.
Quando seu receptor estiver fora do ar, trabalhe nisso nesta ordem:
- Corrija o receptor. Confirme se ele lê bytes brutos e retorna
2xxrapidamente. - Retome o endpoint com
PATCH {"is_active": true}. Isso redefine a contagem de falhas. - Envie um teste com
POST …/test. Um200da API de teste apenas significa que a tentativa foi registrada. Verifique o histórico de entrega e confirmeoutcome == "delivered". - Reconcilie a lacuna. Pegue os IDs de tarefa que você armazenou enquanto o endpoint estava pausado e chame
GET /v1/tasks/{id}para cada um. - Somente então confie no fluxo de webhook novamente.
O histórico de entrega fornece outcome, http_status, attempts e delivered_at. Ele armazena apenas metadados, sem payloads. Eventos antigos não podem ser reproduzidos manualmente, portanto, o passo 4 não é opcional. Seus IDs de tarefa armazenados são o caminho de recuperação.
Rotacione um segredo sem perder eventos
A rotação não é reversível, então planeje a janela antes de começar.
- Chame
POST /v1/management/webhooks/{webhookId}/rotate-secret. A resposta retorna o novo segredo uma vez. - Adicione o novo segredo à sua lista de verificação no receptor. Mantenha o antigo nessa lista também.
- Implante a alteração do receptor antes de descartar qualquer coisa. A lista deve conter ambos os segredos ao mesmo tempo.
- Envie um teste e confirme
outcome == "delivered"no histórico. - Após uma janela curta, remova o segredo antigo e reimplante.
As entregas em trânsito ainda podem carregar a assinatura anterior. Se você trocar segredos em uma etapa, perderá esses eventos. Um verificador que mantém apenas um segredo pode rejeitar entregas que foram assinadas logo antes da rotação.
Gerencie webhooks via MCP
Se você controla o TokenLab a partir de um agente, o servidor MCP expõe o mesmo ciclo de vida. Use @tokenlabai/mcp-server com o perfil full. As ferramentas são list_webhooks, create_webhook, get_webhook, update_webhook, delete_webhook, rotate_webhook_secret, test_webhook e list_webhook_deliveries.
O servidor lê o Management Token de TOKENLAB_MANAGEMENT_TOKEN. O pacote publicado mais recente observado em 28/09/2026 é 0.6.24. O MCP edita os mesmos endpoints que você vê no Dashboard, portanto, não há estado separado para reconciliar.
FAQ
Tarefas de imagem enviam webhooks?
Sim. Toda tarefa assíncrona no workspace, incluindo tarefas de imagem, envia seu evento terminal para os endpoints inscritos nesse tipo de evento. O campo taskType no payload informa que tipo de tarefa era, por exemplo, video ou image. Resultados síncronos não são cobertos.
O que acontece se meu endpoint estiver fora do ar?
Cada ciclo tenta novamente até três vezes. Dez ciclos falhos consecutivos pausam o endpoint automaticamente. Falhas de entrega transitórias podem ser tentadas novamente mais tarde com o mesmo ID de entrega. Uma vez que o endpoint é pausado, eventos da pausa não são entregues posteriormente e não podem ser reproduzidos manualmente. Corrija o receptor, retome o endpoint, envie um teste e, em seguida, reconcilie as tarefas que você criou durante a lacuna chamando GET /v1/tasks/{id} com seus IDs de tarefa armazenados.
Posso reproduzir um evento antigo?
Não. O histórico de entrega contém apenas metadados, não payloads, e não há reprodução manual. A janela de timestamp também rejeita qualquer coisa com mais de 300 segundos. A reconciliação através da API de tarefas é a maneira suportada de colocar o processamento em dia.
É seguro reenviar automaticamente task.failed com retryable: true?
Não. retryable descreve a falha na geração. Não é uma instrução para reenviar. Um novo envio é uma nova tarefa faturável, então decida sobre a retentativa você mesmo e considere o custo.
A API de compatibilidade Seedance usa esses webhooks?
Não. Seu callback_url por requisição é um contrato separado com seu próprio payload. Ele não usa eventos de workspace ou esses cabeçalhos HMAC, então não aponte um verificador para ambos.
Comece com o contrato completo no guia de webhooks, crie uma API key e ative seu primeiro endpoint no workspace que envia suas tarefas.
Fontes
- https://docs.tokenlab.sh/guides/webhooksObservado em 2026-09-28
- https://docs.tokenlab.sh/guides/async-jobs-pollingObservado em 2026-09-28
- https://www.npmjs.com/package/@tokenlabai/mcp-serverObservado em 2026-09-28



