Pular para o conteúdo principal

Webhook

Um Webhook é uma assinatura para um ou mais slugs de SystemEvent. Quando um evento que casa é emitido, a plataforma POSTa um payload assinado para sua URL.

Propósito​

  • Empurrar mudanças de estado (novos e-mails, resultados de agentes, rejeições) para os seus sistemas em tempo real.
  • Substituir polling. Combine com GET /system-events para backfill.
  • Encaminhar para ERPs, sistemas de ticket, filas internas, relays Slack, etc.

Schema​

CampoTipoDescrição
idUUIDID interno.
workspace_idUUIDWorkspace dono.
inbox_idUUID | nullSe setado, só eventos dessa inbox são entregues.
statusenumactive, inactive. Auto-flipa para inactive após N falhas consecutivas.
labelstringLabel humano.
urlstringURL alvo. https:// obrigatório fora de development.
signing_secretstringwhsec_…. Retornado somente na criação / rotação. Assina toda entrega — ver Segurança de webhooks.
signing_secret_rotated_atdatetime | nullÚltima rotação; null se nunca rotacionado.
signing_secret_previous_expires_atdatetime | nullAté quando o secret anterior ainda assina entregas depois de uma rotação.
authenumnone, basic, bearer, header.
auth_basic_userstring | nullQuando auth=basic.
auth_basic_passwordencryptedQuando auth=basic. Nunca retornado.
auth_bearer_tokenencryptedQuando auth=bearer. Nunca retornado.
auth_header_keystring | nullQuando auth=header — nome do header custom.
auth_header_valueencryptedQuando auth=header. Nunca retornado.
subscribed_eventsstring[]Array de valores de SystemEvent.slug.
consecutive_failuresintContador interno — flipa status para inactive depois do threshold.
created_at / updated_at / deleted_atdatetime

Respostas de GET substituem os campos de secret/credencial por auth_configured: true. Para rotacionar o secret de assinatura use POST /webhooks/{id}/regenerate-secret (o anterior segue assinando por uma janela de graça, 24 h por padrão); para trocar credenciais de auth use PATCH com novo payload.

Eventos assinados​

subscribed_events aceita qualquer SystemEvent.slug.

Eventos de linha — emitidos automaticamente em todo create/update da linha (updateMany/upsert em massa não emitem; dentro de uma transação são gravados após o commit):

  • workspace.db.created, workspace.db.updated
  • inbox.db.created, inbox.db.updated
  • agent.db.created, agent.db.updated, agent.version_updated — versão do prompt incrementada; data.version: { from, to }.
  • agent_run.db.created, agent_run.db.updated, agent_run.status_changed — data.status: { from, to }.
  • webhook.db.created, webhook.db.updated
  • email.db.created, email.db.updated, email.status_changed — data.status: { from, to }.
  • workflow_run.db.created, workflow_run.db.updated, workflow_run.status_changed — data.status: { from, to }.

Todo *.db.updated traz data.changed_fields (colunas alteradas, snake_case). Soft-delete aparece como *.db.updated com changed_fields: ["deleted_at"].

Eventos de domínio — emitidos explicitamente:

  • email.rejected — e-mail rejeitado; data.reason.
  • email.agent.processed — agente terminou; data.cost_limit_breached, e data.workflow_run_id quando o agente devolveu um workflow.
  • workflow.run.succeeded / workflow.run.failed — um workflow do agente terminou; a falha traz data.failed_step (o primeiro passo que falhou), data.failed_steps (todos os passos que falharam) e data.error (o erro da primeira falha).
  • workspace.cost_limit_breached — workspace passou do monthly_cost_limit.
  • webhook.delivery.failed — entrega esgotou as tentativas.
  • webhook.ping — evento sintético de /webhooks/{id}/test.

Novos slugs podem ser adicionados sem aviso. Slugs não assinados são ignorados silenciosamente.

Contrato de entrega​

Headers de POST de saída:

Content-Type: application/json
User-Agent: Kobana-Inbox-Webhook/1.0
X-Kobana-App: inbox
X-Kobana-Environment: production
X-Kobana-Webhook-Id: <Webhook.id>
X-Kobana-Event: email.db.created
X-Kobana-Event-Id: <SystemEvent.id>
X-Idempotency-Key: <SystemEvent.id>
X-Kobana-Delivery-Id: <WebhookDelivery.id>
X-Kobana-Timestamp: 1747490400
X-Kobana-Signature: v1,BASE64(HMAC_SHA256(key, "<delivery_id>.<timestamp>.<raw_body>"))
webhook-id: <WebhookDelivery.id>
webhook-timestamp: 1747490400
webhook-signature: v1,BASE64(HMAC_SHA256(key, "<delivery_id>.<timestamp>.<raw_body>"))

Mais qualquer header Authorization (ou custom) configurado. X-Kobana-App e X-Kobana-Environment (development, sandbox, production) identificam o emissor quando um mesmo endpoint recebe webhooks de vários produtos ou ambientes Kobana. Os headers webhook-* repetem id da entrega, timestamp e assinatura no layout Standard Webhooks, então qualquer verificador pronto funciona. X-Idempotency-Key carrega o id do evento no disparo original e em toda retentativa automática, para que um endpoint que honra chaves de idempotência o processe uma vez. Um replay manual carrega uma chave nova e aleatória — veja Replay.

Body:

{
"event": { "slug": "email.agent.processed", "id": "uuid" },
"delivered_at": "2026-05-17T12:00:00Z",
"workspace": { "id": "uuid", "external_id": "string | null", "slug": "acme" },
"data": {
"organization": { "public_id": "uuid", "external_id": "string | null", "slug": "acme-org" },
"agent": { "id": "uuid", "external_id": "string | null", "version": 3 },
"inbox": { "id": "uuid", "external_id": "string | null", "slug": "support" },
"email": {
"id": "uuid",
"message_id": "<...@mail>",
"status": "processed",
"rejection_reason": null,
"metadata": null,
"agent_metadata": {},
"custom_metadata": null,
"aws_ses_metadata": {},
"download_url": "https://app.inbox.kobana.com.br/eml/uuid/eyJhbGciOi..."
},
"agent_run": {
"id": "uuid",
"status": "success",
"llm_model": "anthropic/claude-sonnet-5",
"agent_version": 3,
"input_tokens": 1200,
"output_tokens": 300,
"cache_read": 0,
"cache_write": 0,
"total_cost": 0.0123,
"duration_ms": 4200
},
"cost_limit_breached": false
}
}

data traz snapshots dos recursos no momento da entrega (organization, agent, inbox, email, agent_run) mais as chaves específicas do evento, tudo em snake_case. Eventos de e-mail trazem as cinco seções, null quando o evento não está ligado àquele recurso; os demais eventos trazem o subconjunto listado abaixo. agent.version é a versão do prompt que de fato rodou quando existe agent_run, senão a versão atual do agente. Registros com soft-delete mantêm o id e têm os demais campos como null.

email.download_url é um link pronto pra uso para o .eml original, válido por 30 minutos a partir da entrega — ver Baixando o .eml a partir de um webhook pra entender como difere do GET /v1/emails/{id}/eml.

Seções e chaves específicas por evento:

EventoSeções em dataChaves extras
email.db.createdworkspace, organization, agent, inbox, email, agent_run—
email.db.updatedidemchanged_fields
email.status_changedidemstatus: { from, to }
email.rejectedidemreason
email.agent.processedidemcost_limit_breached, workflow_run_id
workflow.run.succeededworkspace, organization, agent, inbox, email, agent_run (resolvido pela execução de agente que produziu o workflow)workflow_run_id, summary (total, succeeded, error), duration_ms
workflow.run.failedidemidem, mais failed_step, failed_steps e error
agent_run.db.created / agent_run.db.updatedworkspace, organization, agent, inbox, email, agent_runchanged_fields (updated)
agent_run.status_changedworkspace, organization, agent, inbox, email, agent_runstatus: { from, to }
workspace.cost_limit_breachedworkspace, organization, agent, inbox, email, agent_run (inbox resolvida pelo run)—
workspace.db.created / workspace.db.updatedworkspace, organizationchanged_fields (updated)
inbox.db.created / inbox.db.updatedworkspace, organization, inbox, agentchanged_fields (updated)
agent.db.created / agent.db.updatedworkspace, organization, agentchanged_fields (updated)
agent.version_updatedworkspace, organization, agentversion: { from, to }
webhook.db.created / webhook.db.updatedworkspace, organization, inbox, webhook: { id, label }changed_fields (updated)
webhook.delivery.failedworkspace, organization, agent, inbox, email, agent_run, webhook, event: { id, slug }—
webhook.pingworkspace, organization, webhook: { id, label }—

Verificando a assinatura​

Recompute HMAC_SHA256(key, "{delivery_id}.{timestamp}.{raw_request_body}"), onde key é a parte do secret após whsec_ decodificada de base64, codifique em base64 e compare em tempo constante com o(s) token(s) v1,… de X-Kobana-Signature (ou webhook-signature). Durante uma rotação o header traz um token por secret válido. Rejeite tudo onde |now - timestamp| > 300s e deduplique por X-Kobana-Delivery-Id. Guia passo a passo e exemplos de código: Segurança de webhooks.

Política de retry​

  • Sucesso: HTTP 2xx em até 10 s.
  • Falha: timeout / não-2xx.
  • Backoff: 1m, 5m, 30m, 2h, 6h, 24h — 6 tentativas em ~33 h.
  • Após esgotar: WebhookDelivery.status=failed e consecutive_failures++.
  • Passando o threshold (configurável, default 10): Webhook.status flipa para inactive; o dono do workspace é notificado.

Exemplo​

{
"id": "f6a7b8c9-0d1e-4f2a-3b4c-5d6e7f8a9b0c",
"workspace_id": "a8b2c1d4-9e7f-4a3b-8c5d-1f2e3a4b5c6d",
"inbox_id": "b3c4d5e6-7a89-4b0c-9d1e-2f3a4b5c6d7e",
"status": "active",
"label": "Triagem → ERP",
"url": "https://erp.acme.com/inbox/hook",
"auth": "bearer",
"auth_configured": true,
"subscribed_events": ["email.db.created", "email.agent.processed"],
"consecutive_failures": 0,
"created_at": "2026-01-04T12:00:00.000Z",
"updated_at": "2026-05-17T08:12:00.000Z",
"deleted_at": null
}