Webhooks
Em vez de fazer polling em GET /batches/{batch_name} até terminar, registre uma URL de callback uma vez e receba um HTTP POST no momento em que o lote terminar. Os três cabeçalhos de assinatura e a construção de conteúdo assinado id.timestamp.body seguem a Standard Webhooks — a mesma abordagem usada pelos webhooks da OpenAI. Uma diferença da convenção da própria spec: webhook_secret aqui é uma string hex simples, não um valor base64 com prefixo whsec_ — uma biblioteca verificadora Standard Webhooks/Svix pronta tentará decodificar em 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 prefixo.
Registrando um webhook
Registre (ou atualize) o callback de um lote 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 há um endpoint separado de "revelar segredo", mas reenviar um PUT com o mesmo callback_url é uma forma segura de buscá-lo novamente se você o perder (isso não rotaciona o segredo nem redefine nada).
Envelope de eventos
Cada entrega é um objeto JSON envolvido 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 type para distinguir 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.
Disparar um trabalho de anotação de bbox em um documento reutiliza o webhook já registrado para o batch daquele documento 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 por cima disso para o caso comum de uma entrega com falha sendo repetida.
Feed de eventos para triggers de polling
Webhooks de callback de batch são limitados a um batch_name conhecido. Triggers de polling, como o "New Completed Batch" ou "Failed Extraction" do Zapier, precisam de um feed com escopo de conta: GET /api/v1/events. Ele retorna eventos persistidos do mais recente para o mais antigo, com um campo id estável que o Zapier pode usar para deduplicação.
curl "https://imagetotable.ai/api/v1/events?type=batch.completed&limit=20" \
-H "Authorization: Bearer $API_KEY"{
"events": [
{
"id": "evt_3f9c9e2a1b7d4e2b9a3e2f5b6c7d8e9f",
"type": "batch.completed",
"created_at": "2026-08-04T08:31:22+00:00",
"batch_name": "260804-4K9P",
"document_id": null,
"status": "succeeded",
"document_count": 3,
"filename": null,
"error": null,
"batch_url": "https://imagetotable.ai/api/v1/batches/260804-4K9P",
"batch_results_url": "https://imagetotable.ai/api/v1/batches/260804-4K9P/results"
}
],
"has_more": false,
"next_page_token": null
}Os tipos de evento suportados são batch.completed, document.completed e document.failed. Os eventos são gravados somente após o worker confirmar o estado terminal do documento, então um consumidor pode seguir com segurança document_url ou batch_results_url imediatamente. Reprocessar um documento com falha ou processar uma onda posterior no mesmo batch gera um novo id de evento; não deduplique apenas por batch_name ou document_id.
Use callbacks quando você já conhece o batch e quer retomar uma ação assíncrona em espera. Use /events quando um trigger precisar descobrir novos trabalhos em toda a conta. Um trigger que pede ao usuário para digitar manualmente um batch_name geralmente é o modelo errado.
Verificando assinaturas
Cada entrega carrega três cabeçalhos, conforme a especificação Standard Webhooks:
| Cabeçalho | Finalidade |
|---|---|
webhook-id | ID único para esta tentativa de entrega. Use-o para desduplicar — uma entrega repetida reutiliza o mesmo ID. |
webhook-timestamp | Timestamp Unix do envio da entrega, para proteger contra ataques de repetição (rejeite entregas com timestamp muito antigo). |
webhook-signature | A 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 \
| base64import 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.