Guia

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 batch terminar. Os três cabeçalhos de assinatura e a construção de conteúdo assinado id.timestamp.body seguem a Standard Webhooks especificação aberta — a mesma abordagem usada pelos webhooks da OpenAI. Uma diferença da convenção da própria especificação: webhook_secret aqui é uma string hex simples, não um valor base64 com prefixo whsec_ — uma biblioteca verificadora Standard Webhooks/Svix pronta para uso 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 reconhece prefixos.

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 esse 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 o PUT com o mesmo callback_url é uma forma segura de obtê-lo novamente se você o perder (isso não rotaciona o segredo nem redefine nada).

Envelope de eventos

Cada entrega é um objeto JSON embrulhado 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 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.

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 uns dos outros 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 novo 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 produz 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 inserir manualmente um batch_name geralmente é o modelo errado.

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 isto a recria 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 real de verificação 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 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 entrar — 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 via o acessor de corpo de requisição bruto 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 — é apenas uma armadilha de teste manual.

Novas tentativas

A entrega é tentada uma vez imediatamente quando o batch é concluído. Se essa tentativa falhar (timeout, erro de conexão, ou resposta não-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 repetições). Se nenhuma delas tiver sucesso dentro de 24 horas da primeira tentativa, a entrega é marcada como falha e não há mais novas tentativas. Faça seu endpoint responder com status 2xx assim que você registrar o evento de forma durável — faça processamento lento (como buscar e parsear os resultados do batch) depois de responder, não antes, para que uma etapa downstream lenta não cause por si só uma nova tentativa.

Reprocessando um batch: uma notificação por onda

Uma notificação batch.completed dispara uma vez, no momento em que cada documento no batch atingiu um status terminal. Se você depois adicionar mais documentos ao mesmo batch_name e chamar process novamente — "uma nova onda" — você será notificado novamente quando essa onda terminar também, automaticamente. process_batch rearma o webhook por conta própria no momento em que encontra novos documentos elegíveis em um batch cuja onda anterior já notificou você; você não precisa re-PUT o webhook ou fazer mais nada entre as ondas.

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

Versões anteriores desta documentação descreviam o disparo único como uma limitação permanente que você precisava contornar re-PUT-ando o webhook antes de cada onda. Esse conselho nunca funcionou de verdade — o endpoint de registro era, e ainda é, deliberadamente não-intervencionista em relação a fired_at (veja a referência de Webhooks) — e o rearmamento agora é tratado automaticamente por process em vez disso. Se vocêre em uma integração escrita seguindo o conselho anterior, você pode parar de re-registrar antes de cada onda com segurança; nunca esteve fazendo nada.

📮 contact email: [email protected]