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).

RegraDetalhe
EsquemaDeve ser https://. http:// puro é rejeitado.
HostDeve 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.
ComprimentoMáximo de 1024 caracteres.
AcessibilidadeDeve 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çalhoDescrição
X-AtlasCloud-Webhook-IdO session_id da tarefa — sua chave de correlação e de idempotência.
X-AtlasCloud-Webhook-EventO tipo do evento, por exemplo video.task.terminal, image.task.terminal ou audio.task.terminal.
X-AtlasCloud-Webhook-TimestampSegundos desde a época Unix no momento da tentativa de entrega. Coberto pela assinatura Ed25519.
X-AtlasCloud-Webhook-SignatureHMAC-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-Ed25519Assinatura Ed25519 em base64url de <timestamp>.<raw_body> (enviada durante a migração HMAC→Ed25519).
X-AtlasCloud-Webhook-Key-IdO kid da chave de assinatura Ed25519 — corresponde a uma chave do JWKS.
User-AgentAtlasCloud-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 de X-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á para X-AtlasCloud-Webhook-Signature — portanto, dê preferência ao cabeçalho -Ed25519 quando ele estiver presente e recorra a -Signature como alternativa.
  • Id da chave: X-AtlasCloud-Webhook-Key-Id é o kid do JWK que assinou a entrega.

Passos

  1. Leia o X-AtlasCloud-Webhook-Timestamp (ts), o X-AtlasCloud-Webhook-Key-Id (kid) e o cabeçalho da assinatura Ed25519.
  2. (Recomendado) Rejeite se o timestamp estiver a mais de ~5 minutos do seu relógio (proteção contra replay).
  3. Busque o JWKS e selecione a chave cujo kid corresponde. Faça cache do JWKS; diante de um kid que você não reconhece, busque novamente uma vez (a chave de assinatura pode ter sido rotacionada).
  4. Decodifique o x do JWK (base64url) → a chave pública Ed25519 de 32 bytes.
  5. 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-Signature usando 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 kid desconhecido; confira a assinatura sobre "<timestamp>.<raw_body>" e imponha uma janela de replay.
  • Responda 2xx rapidamente; mova o processamento real para uma fila em segundo plano.
  • Desduplique pelo session_id — os handlers precisam ser idempotentes.
  • Ramifique pelo status de nível superior (OK vs. ERROR); leia os resultados em payload.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

SintomaCausa provável / ação
O envio retorna 400 webhook_url ... is not a routable public addressO host é privado/loopback/CGNAT. Use uma URL HTTPS pública ou um túnel.
O envio retorna 400 webhook_url must use httpsTroque o esquema para https://.
O envio retorna 400 webhook_url exceeds the 1024-character limitEncurte a URL (mova o estado para o seu próprio armazenamento, com chave no session_id).
Nenhum webhook recebidoConfirme 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 JWKSA 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 vezesEsperado sob entrega at-least-once — desduplique pelo session_id.

Referência

  • Envio (com webhook): POST /api/v1/model/generateVideo, POST /api/v1/model/generateImage ou POST /api/v1/model/generateAudio — adicione webhook_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): Ed25519 em base64url sobre "<timestamp>.<raw_body>", em X-AtlasCloud-Webhook-Signature-Ed25519 (id da chave em X-AtlasCloud-Webhook-Key-Id).
  • Assinatura (HMAC, legada): HMAC-SHA256(signing_secret, raw_body), em hexadecimal, em X-AtlasCloud-Webhook-Signature.
  • Chave de idempotência: session_id (também em X-AtlasCloud-Webhook-Id).
  • Alternativa por polling: Predições.

Last updated on

On this page