Guia

Webhooks

Em vez de ficar consultando GET /batches/{batch_name} até terminar, registre uma URL de callback uma vez e receba um HTTP POST no momento em que um batch for concluído. Os três cabeçalhos de assinatura e a construção do conteúdo assinado id.timestamp.body seguem a especificação aberta Standard Webhooks — a mesma abordagem usada pelos webhooks da OpenAI. Uma diferença da convenção da própria especificação: webhook_secret aqui é uma string hexadecimal simples, não um valor base64 com prefixo whsec_ — uma biblioteca verificadora Standard Webhooks/Svix pronta para uso tentará decodificá-lo como base64 e falhará. Use o segredo exatamente como retornado, como bytes brutos de chave HMAC, com o código de verificação abaixo (ou seu equivalente) em vez de uma biblioteca que espera o prefixo.

Registrando um webhook

Registre (ou atualize) o callback de um batch com PUT /api/v1/batches/{batch_name}/webhook:

curl -X PUT https://imagetotable.ai/api/v1/batches/260716-4K9P/webhook \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"callback_url": "https://example.com/hooks/imagetotable"}'

A resposta inclui um webhook_secret por webhook, gerado para você — um segredo por registro, não uma chave compartilhada da conta, então um segredo vazado afeta apenas aquele callback:

{
  "batch_name": "260716-4K9P",
  "callback_url": "https://example.com/hooks/imagetotable",
  "webhook_secret": "9f2a3b7c1d8e4f5061728394a5b6c7d81a2b3c4d5e6f7089",
  "created_at": "2026-07-16T09:12:03Z",
  "fired_at": null
}

Salve o webhook_secret — você vai precisar dele para verificar as entregas recebidas. Não existe um endpoint separado para "revelar o segredo", mas fazer um novo PUT com o mesmo callback_url é uma forma segura de obtê-lo novamente se você o perder (ele não rotaciona o segredo nem redefine nada).

Envelope do evento

Toda entrega é um objeto JSON encapsulado em um envelope — nunca dados brutos do batch — para que a estrutura possa crescer e cobrir futuros tipos de evento sem quebrar integrações existentes. Verifique o campo type para diferenciar os eventos; uma URL de callback registrada em um batch pode receber ambos os tipos abaixo, misturados.

{
  "type": "batch.completed",
  "created_at": "2026-07-16T09:14:31Z",
  "data": {
    "batch_name": "260716-4K9P",
    "status": "succeeded",
    "document_count": 3
  }
}

data.status reflete o resultado consolidado do batch (succeeded ou failed — veja Async Task Model para o vocabulário completo de status). O payload é um aviso de conclusão, não os resultados em si — chame GET /batches/{batch_name}/results após recebê-lo para buscar os dados extraídos de fato.

Ao acionar um trabalho de anotação de bbox em um documento, o webhook já registrado para o batch daquele documento é reutilizado, e entrega um evento distinto em vez de outro batch.completed:

{
  "type": "document.bbox_completed",
  "created_at": "2026-07-16T09:20:07Z",
  "data": {
    "document_id": "3f9c9e2a-1b7d-4e2b-9a3e-2f5b6c7d8e9f",
    "group_batch_id": "bx_8a3c2e1f",
    "status": "succeeded"
  }
}

Assim como batch.completed, a entrega deste evento é reivindicada atomicamente, então você não receberá duas entregas separadas de document.bbox_completed para um único trabalho de bbox, mesmo que vários grupos de linhas atinjam um status terminal em momentos próximos em threads diferentes. A deduplicação usual baseada em webhook-id (veja abaixo) ainda se aplica sobre isso para o caso comum de uma entrega com falha ser repetida.

Verificando assinaturas

Cada entrega carrega três cabeçalhos, conforme a especificação Standard Webhooks:

CabeçalhoFinalidade
webhook-idID único para esta tentativa de entrega. Use-o para desduplicar — uma entrega repetida reutiliza o mesmo ID.
webhook-timestampTimestamp Unix do envio da entrega, para proteger contra ataques de repetição (rejeite entregas com timestamp muito antigo).
webhook-signatureA própria assinatura, formatada como v1,<assinatura base64>.

A assinatura é um HMAC-SHA256 sobre a concatenação de webhook-id, webhook-timestamp e o corpo da requisição bruto — nessa ordem, unidos por . — usando como chave o webhook_secret do seu webhook:

signed_content = "{webhook_id}.{webhook_timestamp}.{raw_request_body}"
signature      = base64(hmac_sha256(webhook_secret, signed_content))

Recompute isso no seu lado e compare (usando uma comparação em tempo constante) com o valor após v1, no cabeçalho webhook-signature. Sempre verifique contra os bytes do corpo da requisição bruto, não contra uma versão re-serializada do JSON analisado — re-serializar pode alterar espaços em branco/ordem das chaves e produzir uma incompatibilidade de assinatura mesmo para uma entrega genuína.

# A verificação de assinatura não é uma chamada HTTP, então recriamos ela com
# openssl contra uma entrega que você já salvou em disco (headers.txt /
# body.json) — útil para confirmar seu entendimento do esquema manualmente
# antes de implementar o código de verificação real no seu handler de webhook.
WEBHOOK_ID=$(grep -i '^webhook-id:' headers.txt | cut -d' ' -f2 | tr -d '\r')
WEBHOOK_TS=$(grep -i '^webhook-timestamp:' headers.txt | cut -d' ' -f2 | tr -d '\r')
SIGNED_CONTENT="${WEBHOOK_ID}.${WEBHOOK_TS}.$(cat body.json)"

echo -n "$SIGNED_CONTENT" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary \
  | base64
import base64
import hashlib
import hmac
import os

headers = dict(line.split(": ", 1) for line in open("headers.txt") if ": " in line)
webhook_id = headers["webhook-id"].strip()
webhook_timestamp = headers["webhook-timestamp"].strip()
body = open("body.json", "rb").read()

signed_content = f"{webhook_id}.{webhook_timestamp}.".encode() + body
signature = base64.b64encode(
    hmac.new(os.environ["WEBHOOK_SECRET"].encode(), signed_content, hashlib.sha256).digest()
)
print(signature.decode())
import { createHmac } from "node:crypto";
import { readFileSync } from "node:fs";

const headers = Object.fromEntries(
  readFileSync("headers.txt", "utf8")
    .split("\n")
    .filter((line) => line.includes(": "))
    .map((line) => line.split(": "))
);
const webhookId = headers["webhook-id"].trim();
const webhookTimestamp = headers["webhook-timestamp"].trim();
const body = readFileSync("body.json", "utf8");

const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
const signature = createHmac("sha256", process.env.WEBHOOK_SECRET)
  .update(signedContent)
  .digest("base64");
console.log(signature);

Se você está testando manualmente — salvando o corpo de uma entrega em body.json antes de executar um dos trechos acima — cuidado com uma quebra de linha extra adicionada pelo seu editor ou método de captura. A versão com curl/openssl acima é acidentalmente imune a isso (o $(cat ...) do bash remove quebras de linha finais), mas as versões Python e JavaScript leem os bytes exatos do arquivo e vão silenciosamente produzir uma assinatura que não corresponde se um \n extra aparecer — o sintoma clássico é "curl diz que é válido, mas meu verificador Python diz que não é", o que é confuso justamente porque o código em si não tem bug. Capture o corpo através do acessor de corpo bruto da requisição do seu framework web (request.get_data() do Flask, middleware raw-body do Express) no seu handler de webhook real, em vez de copiar/colar ou salvar manualmente — isso não acontece em produção, é só uma armadilha de teste manual.

Reintentos

A entrega é tentada uma vez imediatamente após o batch ser concluído. Se essa tentativa falhar (timeout, erro de conexão ou resposta diferente de 2xx), ela é repetida com backoff exponencial: 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas, 24 horas — até 7 tentativas no total (a tentativa inicial mais 6 reintentos). Se nenhuma delas for bem-sucedida dentro de 24 horas após a primeira tentativa, a entrega é marcada como falha e não ocorrem mais reintentos. Faça seu endpoint responder com um status 2xx assim que você tiver registrado o evento de forma durável — faça o processamento lento (como buscar e analisar os resultados do batch) após responder, não antes, para que uma etapa downstream lenta não cause por si só um reintento.

Reprocessando um batch: uma notificação por onda

Uma notificação batch.completed é disparada uma vez, no momento em que todos os documentos do batch atingiram um status terminal. Se você adicionar mais documentos ao mesmo batch_name e chamar process novamente — "uma nova onda" — você será notificado novamente assim que essa onda terminar também, automaticamente. O process_batch rearma o próprio webhook no momento em que encontra novos documentos elegíveis em um batch cuja onda anterior já notificou você; você não precisa refazer o PUT do webhook ou fazer mais nada entre as ondas.

Se duas ondas se sobrepuserem — você adiciona e processa mais documentos enquanto uma onda anterior ainda está em execução, em vez de após ela terminar — você receberá exatamente uma notificação, cobrindo todos os documentos de ambas as ondas, entregue assim que o último deles (de qualquer uma das ondas) terminar. Você só recebe duas notificações separadas quando o batch realmente ficou ocioso (todos os documentos atingiram um status terminal pelo menos uma vez) entre as duas chamadas de process.

Versões anteriores deste documento descreviam o disparo único como uma limitação permanente que você precisava contornar refazendo o PUT do webhook antes de cada onda. Esse conselho nunca funcionou de fato — o endpoint de registro era, e ainda é, deliberadamente descomplicado em relação ao fired_at (veja a referência de Webhooks) — e o rearmamento agora é tratado automaticamente pelo process. Se você está em uma integração baseada no conselho antigo, pode parar de registrar novamente antes de cada onda com segurança; isso nunca estava fazendo nada.

📮 contact email: [email protected]