Webhooks
Seja notificado no instante em que uma tarefa de geração assíncrona termina — sem precisar de polling
Visão geral
Quando você envia uma tarefa de geração assíncrona, o Atlas Cloud a processa em segundo plano e o resultado fica disponível algum tempo depois. Em vez de chamar repetidamente o endpoint de Predições até a tarefa terminar, você pode pedir que o Atlas Cloud chame você de volta no instante em que a tarefa atinge um estado terminal.
Para isso, basta informar um webhook_url ao enviar a tarefa. Quando a tarefa termina — tendo ela êxito, falhado ou expirado — o Atlas Cloud envia um único POST assinado para essa URL contendo o resultado final.
Tipos de tarefa suportados
Os webhooks estão disponíveis para geração assíncrona de vídeo, imagem e áudio. O mecanismo de entrega é agnóstico ao tipo de tarefa — o event_type (e o cabeçalho X-AtlasCloud-Webhook-Event) identifica a modalidade: video.task.terminal, image.task.terminal ou audio.task.terminal.
Os webhooks complementam o polling — eles não o substituem. O endpoint de Predições continua funcionando exatamente como antes, e o payload do webhook traz o mesmo formato de resultado que você obteria via polling. Use um dos dois, ou ambos.
Início rápido
Adicione um campo webhook_url à sua solicitação de envio já existente:
curl -X POST https://api.atlascloud.ai/api/v1/model/generateVideo \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/seedance-2.0/text-to-video",
"prompt": "A calico kitten chasing a butterfly in a garden, cinematic",
"duration": 5,
"resolution": "1080p",
"webhook_url": "https://your-app.example.com/hooks/atlascloud"
}'O mesmo campo webhook_url funciona de forma idêntica em POST /api/v1/model/generateImage (imagem assíncrona, event_type: "image.task.terminal") e em POST /api/v1/model/generateAudio (áudio assíncrono, event_type: "audio.task.terminal").
A resposta ao envio não muda — você continua recebendo de imediato um id de tarefa (o session_id). O webhook_url é consumido pelo Atlas Cloud e nunca encaminhado ao provedor do modelo upstream. Quando a tarefa é concluída, seu endpoint recebe um POST.
Requisitos para o webhook_url
Sua URL de callback é validada no momento do envio. Se ela não passar na validação, a solicitação de envio é rejeitada com HTTP 400 e nenhuma tarefa é criada (você não é cobrado).
| Regra | Detalhe |
|---|---|
| Esquema | Deve ser https://. http:// puro é rejeitado. |
| Host | Deve ser um endereço público e roteável. Faixas privadas, de loopback, link-local e CGNAT (100.64.0.0/10) são rejeitadas. |
| Comprimento | Máximo de 1024 caracteres. |
| Acessibilidade | Deve ser acessível pela internet pública para que o Atlas Cloud consiga enviar o POST. |
Essas verificações são uma proteção contra SSRF. Para desenvolvimento local, use um túnel público (um serviço de teste de webhooks, o ngrok ou um túnel da Cloudflare) em vez de um endereço privado.
A solicitação de callback
Quando a tarefa atinge um estado terminal, o Atlas Cloud envia um POST com Content-Type: application/json e os seguintes cabeçalhos:
| Cabeçalho | Descrição |
|---|---|
X-AtlasCloud-Webhook-Id | O session_id da tarefa — sua chave de correlação e de idempotência. |
X-AtlasCloud-Webhook-Event | O tipo do evento, por exemplo video.task.terminal, image.task.terminal ou audio.task.terminal. |
X-AtlasCloud-Webhook-Timestamp | Segundos desde a época Unix no momento da tentativa de entrega. Coberto pela assinatura Ed25519. |
X-AtlasCloud-Webhook-Signature | HMAC-SHA256 codificado em hexadecimal do corpo bruto da solicitação (esquema HMAC). No esquema puramente Ed25519, este cabeçalho carrega a assinatura Ed25519 — consulte Verificação de assinaturas. |
X-AtlasCloud-Webhook-Signature-Ed25519 | Assinatura Ed25519 em base64url de <timestamp>.<raw_body> (enviada durante a migração HMAC→Ed25519). |
X-AtlasCloud-Webhook-Key-Id | O kid da chave de assinatura Ed25519 — corresponde a uma chave do JWKS. |
User-Agent | AtlasCloud-Webhook/1.0 |
Payload
{
"session_id": "string", // o id da tarefa; sua chave de idempotência
"event_type": "string", // por exemplo "video.task.terminal"
"status": "OK" | "ERROR", // resultado de nível superior — ramifique por aqui
"created_at": 1782295062952, // horário de criação da tarefa (época em ms)
"payload": { // o resultado, mesmo formato da API de Predições
"model": "string",
"status": "completed" | "failed" | "timeout",
"outputs": ["https://..."], // presente em caso de sucesso
"error_code": 0 // presente em caso de falha
},
"error": "string" // presente apenas quando status == "ERROR"
}Ramifique seu handler pelo campo status de nível superior: OK significa que há um resultado utilizável em payload.outputs; ERROR significa que a tarefa não produziu resultado e error explica o motivo.
Resultados de transcrição de áudio
Tarefas de texto para fala e outras tarefas generativas de áudio retornam as URLs dos arquivos de áudio em payload.outputs, exatamente como vídeo e imagem. Para modelos de fala para texto (transcrição), um payload concluído traz adicionalmente um objeto estruturado stt_result (texto completo, idioma detectado e marcações de tempo por palavra) — o mesmo campo que o endpoint de Predições retorna.
Exemplo de sucesso
Uma entrega real para uma tarefa bytedance/seedance-2.0/text-to-video concluída:
{
"session_id": "6a0c02cdb4b147b7bc78881eb7229ece",
"event_type": "video.task.terminal",
"status": "OK",
"created_at": 1782295062952,
"payload": {
"model": "bytedance/seedance-2.0/text-to-video",
"status": "completed",
"outputs": [
"https://atlas-media.oss-us-west-1.aliyuncs.com/videos/cgt-20260624-0.mp4"
]
}
}Exemplo de falha
{
"session_id": "9b2f4e7a1c0d4f5e8a6b3c2d1e0f9a8b",
"event_type": "video.task.terminal",
"status": "ERROR",
"created_at": 1782200000000,
"payload": {
"model": "bytedance/seedance-2.0/text-to-video",
"status": "failed",
"error_code": 1039
},
"error": "the input was rejected by content moderation"
}Um desfecho de timeout tem a mesma aparência, com payload.status: "timeout" e uma mensagem error genérica.
Verificação de assinaturas
Sempre verifique a assinatura antes de confiar em um webhook. Ela prova que a solicitação veio do Atlas Cloud e não foi adulterada.
Dois esquemas durante a migração
O Atlas Cloud está migrando as assinaturas de webhook de um segredo HMAC compartilhado para Ed25519 com um endpoint JWKS público. Durante a transição, as entregas carregam ambas as assinaturas: a HMAC (X-AtlasCloud-Webhook-Signature) e a Ed25519 (X-AtlasCloud-Webhook-Signature-Ed25519). Prefira Ed25519 — ela é verificada contra uma chave pública que você busca por uma URL, sem nenhum segredo compartilhado para armazenar.
Ed25519 + JWKS (recomendado)
O Atlas Cloud assina cada entrega com uma chave privada Ed25519 e publica a chave pública correspondente em um endpoint JWKS. Você verifica contra essa chave pública — não há nenhum segredo para provisionar ou rotacionar do seu lado.
- Chaves públicas (JWKS):
GET https://api.atlascloud.ai/api/v1/webhooks/jwks.json(sem autenticação):
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "Wn_rgZDBO6nv4-ka97PPf5z8WXbM25o75dR6YukTqqI",
"use": "sig",
"alg": "EdDSA",
"kid": "cnut8IN2blKoB5zRJr9th0HoLzH-iBH3WYoUtkcJZQE"
}
]
}- Mensagem assinada:
"<timestamp>.<raw_body>"— o valor deX-AtlasCloud-Webhook-Timestamp, um.literal e, em seguida, o corpo bruto exato da solicitação. Diferentemente do HMAC, o timestamp é coberto pela assinatura, então você pode impor uma janela de replay. - Cabeçalho da assinatura:
X-AtlasCloud-Webhook-Signature-Ed25519(base64url). Depois que o HMAC for descontinuado, a assinatura Ed25519 passará paraX-AtlasCloud-Webhook-Signature— portanto, dê preferência ao cabeçalho-Ed25519quando ele estiver presente e recorra a-Signaturecomo alternativa. - Id da chave:
X-AtlasCloud-Webhook-Key-Idé okiddo JWK que assinou a entrega.
Passos
- Leia o
X-AtlasCloud-Webhook-Timestamp(ts), oX-AtlasCloud-Webhook-Key-Id(kid) e o cabeçalho da assinatura Ed25519. - (Recomendado) Rejeite se o timestamp estiver a mais de ~5 minutos do seu relógio (proteção contra replay).
- Busque o JWKS e selecione a chave cujo
kidcorresponde. Faça cache do JWKS; diante de umkidque você não reconhece, busque novamente uma vez (a chave de assinatura pode ter sido rotacionada). - Decodifique o
xdo JWK (base64url) → a chave pública Ed25519 de 32 bytes. - Verifique a assinatura decodificada de base64url sobre
ts + "." + raw_body.
const crypto = require("crypto");
const JWKS_URL = "https://api.atlascloud.ai/api/v1/webhooks/jwks.json";
let jwks = {}; // kid -> jwk
async function publicKey(kid) {
if (!jwks[kid]) {
const { keys } = await (await fetch(JWKS_URL)).json();
jwks = Object.fromEntries(keys.map((k) => [k.kid, k]));
}
const jwk = jwks[kid];
return jwk && crypto.createPublicKey({ key: jwk, format: "jwk" });
}
// req.body precisa ser o Buffer do corpo BRUTO (por exemplo, express.raw()).
async function verifyEd25519(req) {
const ts = req.get("X-AtlasCloud-Webhook-Timestamp");
const kid = req.get("X-AtlasCloud-Webhook-Key-Id");
const sig =
req.get("X-AtlasCloud-Webhook-Signature-Ed25519") ||
req.get("X-AtlasCloud-Webhook-Signature");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // janela de replay
const pub = await publicKey(kid);
if (!pub) return false;
const msg = Buffer.concat([Buffer.from(`${ts}.`), req.body]);
return crypto.verify(null, msg, pub, Buffer.from(sig, "base64url"));
}A assinatura prova que o callback teve origem no Atlas Cloud — não a qual conta a tarefa pertence. Como o webhook_url é informado por solicitação, correlacione também o session_id a uma tarefa que você realmente criou antes de agir sobre o resultado.
HMAC (legado)
A assinatura HMAC está sendo descontinuada em favor do Ed25519/JWKS descrito acima; novas integrações devem usar Ed25519. O X-AtlasCloud-Webhook-Signature HMAC continua sendo enviado durante a janela de migração.
A assinatura é calculada assim:
HMAC-SHA256( signing_secret, raw_request_body ) → lowercase hex- A chave é o seu segredo de assinatura compartilhado (provisionado para a sua conta).
- A mensagem são os bytes brutos exatos do corpo da solicitação — verifique antes de qualquer reserialização de JSON, que poderia alterar a ordem dos bytes ou os espaços em branco.
- Compare o resultado com o cabeçalho
X-AtlasCloud-Webhook-Signatureusando uma comparação em tempo constante.
O cabeçalho X-AtlasCloud-Webhook-Timestamp é informativo (útil para uma verificação opcional de janela de replay). Ele não faz parte do conteúdo assinado — apenas o corpo bruto é assinado.
const crypto = require("crypto");
const express = require("express");
const app = express();
const SIGNING_SECRET = process.env.ATLASCLOUD_WEBHOOK_SECRET;
// Capture o corpo BRUTO — não deixe um parser de JSON rodar antes.
app.use("/hooks/atlascloud", express.raw({ type: "*/*" }));
app.post("/hooks/atlascloud", (req, res) => {
const sig = req.get("X-AtlasCloud-Webhook-Signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.body) // req.body é um Buffer (bytes brutos)
.digest("hex");
// Compare como Buffers e proteja pelo comprimento em BYTES: timingSafeEqual
// lança erro com entradas de tamanhos diferentes, e um cabeçalho multibyte
// malformado pode coincidir no tamanho da string em JS e diferir em bytes.
const sigBuf = Buffer.from(sig);
const expBuf = Buffer.from(expected);
const ok =
sigBuf.length === expBuf.length &&
crypto.timingSafeEqual(sigBuf, expBuf);
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// ... enfileire pelo event.session_id e depois responda rápido ...
res.status(200).send("ok");
});Semântica de entrega
Confirmar uma entrega
Responda com qualquer código de status 2xx para confirmar o recebimento. Qualquer outro código de status — ou um timeout de conexão — é tratado como falha, e a entrega é repetida.
Responda rapidamente (bem dentro de poucos segundos). Faça o trabalho real de forma assíncrona: verifique a assinatura, enfileire o evento com chave no session_id e retorne 200 imediatamente. Respostas lentas correm o risco de expirar e disparar novas tentativas desnecessárias.
Novas tentativas e backoff
Se uma entrega não for confirmada, o Atlas Cloud tenta novamente com backoff exponencial (aproximadamente 10s → 20s → 40s → …, limitado a cerca de 30 minutos), por até ~10 tentativas. Esgotadas as tentativas, a entrega é marcada como não entregável e deixa de ser repetida.
Pelo menos uma vez (at-least-once) — desduplique pelo session_id
A entrega é at-least-once (pelo menos uma vez). Em casos raros, você pode receber o mesmo webhook mais de uma vez. Torne seu handler idempotente e desduplique pelo session_id.
O session_id é estável entre as novas tentativas. Trate um webhook de um session_id que você já processou por completo como uma operação sem efeito e retorne 200.
Pontualidade
A grande maioria dos webhooks é entregue em questão de segundos após o término da tarefa. Uma rede de segurança de reconciliação embutida garante a entrega mesmo que o caminho rápido falhe (por exemplo, durante uma implantação do serviço), ao custo de um atraso de até ~30 minutos nesses casos incomuns. Projete para uma entrega eventual e at-least-once, e não instantânea e exatamente uma vez.
Boas práticas
- Sirva o callback por HTTPS em um host publicamente acessível.
- Verifique a assinatura do webhook antes de confiar no evento — de preferência com Ed25519/JWKS (nenhum segredo para armazenar; verifique contra a chave pública obtida pelo
kid). O HMAC é a alternativa legada durante a janela de migração. - Ao verificar com Ed25519, faça cache do JWKS e busque novamente diante de um
kiddesconhecido; confira a assinatura sobre"<timestamp>.<raw_body>"e imponha uma janela de replay. - Responda
2xxrapidamente; mova o processamento real para uma fila em segundo plano. - Desduplique pelo
session_id— os handlers precisam ser idempotentes. - Ramifique pelo
statusde nível superior (OKvs.ERROR); leia os resultados empayload.outputs. - Não pressuponha ordenação nem entrega exatamente uma vez; projete para at-least-once.
- Mantenha o endpoint de Predições como caminho de fallback / reconciliação.
- Somente para o HMAC legado: mantenha seu segredo de assinatura confidencial e rotacione-o se ele for exposto. (Ed25519/JWKS não tem segredo do seu lado.)
Solução de problemas
| Sintoma | Causa provável / ação |
|---|---|
O envio retorna 400 webhook_url ... is not a routable public address | O host é privado/loopback/CGNAT. Use uma URL HTTPS pública ou um túnel. |
O envio retorna 400 webhook_url must use https | Troque o esquema para https://. |
O envio retorna 400 webhook_url exceeds the 1024-character limit | Encurte a URL (mova o estado para o seu próprio armazenamento, com chave no session_id). |
| Nenhum webhook recebido | Confirme que o endpoint é publicamente acessível e retorna 2xx; verifique pelo endpoint de Predições se a tarefa realmente atingiu um estado terminal. |
| Assinatura não confere (Ed25519) | Assine sobre "<timestamp>.<raw_body>" (não apenas o corpo), decodifique a assinatura de base64url e localize a chave pelo kid no JWKS. |
| Assinatura não confere (HMAC) | Garanta que o HMAC é calculado sobre os bytes brutos do corpo (não sobre um objeto JSON reserializado) e use o segredo de assinatura correto. |
kid ausente no JWKS | A chave de assinatura foi rotacionada — busque o JWKS novamente (não faça cache de uma única chave para sempre). |
| O mesmo evento foi recebido duas vezes | Esperado sob entrega at-least-once — desduplique pelo session_id. |
Referência
- Envio (com webhook):
POST /api/v1/model/generateVideo,POST /api/v1/model/generateImageouPOST /api/v1/model/generateAudio— adicionewebhook_url. - Tipos de evento:
video.task.terminal,image.task.terminal,audio.task.terminal. - JWKS (chaves públicas):
GET /api/v1/webhooks/jwks.json. - Assinatura (Ed25519, recomendada):
Ed25519em base64url sobre"<timestamp>.<raw_body>", emX-AtlasCloud-Webhook-Signature-Ed25519(id da chave emX-AtlasCloud-Webhook-Key-Id). - Assinatura (HMAC, legada):
HMAC-SHA256(signing_secret, raw_body), em hexadecimal, emX-AtlasCloud-Webhook-Signature. - Chave de idempotência:
session_id(também emX-AtlasCloud-Webhook-Id). - Alternativa por polling: Predições.
Last updated on