Webhooks

Webhooks

Recebe notificações automáticas quando um documento chega a um estado final — evita ter de fazer polling constante.

A URL do webhook é configurada no dashboard. A activação, porém, depende do teu endpoint responder correctamente ao desafio abaixo — é isso que esta página cobre, junto com o formato dos eventos reais.

Um webhook por conta, não por empresa

Se a tua conta gere várias empresas, todas partilham a mesma URL de webhook — não é preciso (nem possível) configurar uma diferente por empresa.

Isso significa que o teu endpoint vai receber eventos de todas as empresas da conta misturados no mesmo lugar. Todo evento traz tenant_id no payload (topo, junto com event) — usa esse campo para saberes de qual empresa veio e encaminhares/filtrares no teu lado (ex.: por tenant_id correspondente a cada empresa registada no teu ERP).

⚠️

Se geres várias empresas, valida sempre tenant_id antes de processar o evento — nunca assumas que o payload pertence a uma empresa só porque é a única que configuraste até agora.

Activação (challenge-echo)

Depois de configurares a URL no dashboard, o webhook começa inactivo — eventos reais só são entregues depois de activares. Ao pedir a activação (no dashboard), o Gateway envia um pedido de teste ao teu endpoint:

POST <a tua URL>
Content-Type: application/json
{ "event": "webhook.verify", "challenge": "3f8a1c9b2d4e5f6a" }

O teu endpoint tem de responder, em até 5 segundos, com HTTP 200 e um corpo JSON que ecoe exactamente o mesmo challenge:

{ "challenge": "3f8a1c9b2d4e5f6a" }

Se o valor coincidir, o webhook passa a active e começa a receber eventos reais. Caso contrário (status diferente de 200, corpo sem o challenge correcto, ou timeout), a activação falha e tens de tentar novamente depois de corrigir o endpoint.

from fastapi import FastAPI, Request
 
app = FastAPI()
 
@app.post("/webhooks/agt")
async def receber_webhook(request: Request):
    body = await request.json()
    if body.get("event") == "webhook.verify":
        return {"challenge": body["challenge"]}
    # ... tratar eventos reais (ver abaixo)
    return {"ok": True}

Eventos reais

POST <a tua URL>
Content-Type: application/json
X-Webhook-Signature: sha256=<hmac hex>
{
  "event": "request.completed",
  "service_type": "registar_factura",
  "tenant_id": "b3f1c2a4-1234-4a5b-9c6d-7e8f9a0b1c2d",
  "timestamp": "2026-07-27T10:00:00Z",
  "data": {
    "request_id": "uuid",
    "agt_request_id": "202500000000118",
    "status": "VALID",
    "response": { "gateway_status": "VALID", "agt_status_code": 1, "agt_data_response": {} },
    "error_list": []
  }
}

data.status segue os mesmos valores finais descritos em Consultar Estado.

Séries: series.created / series.failed

Pedidos de criação de série (POST /serie ou POST /serie/all) disparam um evento dedicado quando a AGT responde, em vez do genérico request.completed:

{
  "event": "series.created",
  "service_type": "solicitar_serie",
  "tenant_id": "b3f1c2a4-1234-4a5b-9c6d-7e8f9a0b1c2d",
  "timestamp": "2026-07-27T10:00:00Z",
  "data": {
    "request_id": "uuid",
    "agt_request_id": "202500000000118",
    "status": "SUCCESS",
    "response": { "gateway_status": "SUCCESS", "agt_status_code": 1, "agt_data_response": { "seriesCode": "FT SEDE 2026", "authorizedQuantity": 1000 } },
    "error_list": []
  }
}

Se a AGT rejeitar o pedido, o evento é series.failed (mesma estrutura, status e error_list reflectem o motivo). Usa POST /serie/all para pedir todas as séries em falta de uma vez — a resposta HTTP é imediata (QUEUED para cada tipo), o resultado real chega por este webhook.

Validar a assinatura

O segredo do teu webhook está disponível no dashboard. Cada entrega é assinada com HMAC-SHA256 sobre o corpo bruto do pedido.

⚠️

Valida sempre a assinatura antes de confiar no payload.

import hmac, hashlib
 
def verificar_assinatura(corpo_bruto: bytes, header_assinatura: str, secret: str) -> bool:
    esperado = "sha256=" + hmac.new(secret.encode(), corpo_bruto, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, header_assinatura or "")

Boas práticas

  • Responde 200 de imediato e processa o payload de forma assíncrona no teu lado — o Gateway espera até 10s pela resposta.
  • Não dependas só do webhook: usa GET /v1/client/status/{requestId} como rede de segurança para reconciliar estados, caso alguma entrega falhe.
  • Se o teu endpoint falhar 5 vezes consecutivas, o webhook é desactivado automaticamente — reactiva-o no dashboard depois de corrigir o problema. Como o webhook é por conta, isto pára a entrega de eventos de todas as empresas da conta, não só da que gerou a falha.
  • Com mais de uma empresa, usa sempre tenant_id para rotear o evento internamente — nunca dependas da URL ou de qualquer outro campo para identificar a origem.