Webhooks
Receba um aviso assinado quando um documento for cadastrado, alterado ou autenticado, e confira a assinatura antes de confiar nele.
Para: Desenvolvedores dos sistemas da prefeitura
Em vez de o seu sistema perguntar à API a cada cinco minutos "mudou alguma coisa?", o GedFlow avisa: assim que um documento do recorte da integração é cadastrado, alterado ou autenticado, uma chamada POST chega ao endereço que você informou.
O aviso é mínimo de propósito: leva só o evento e os identificadores do documento. Os detalhes você busca pela API, com o token e os escopos da integração. Assim, nenhum dado sensível viaja em um aviso.
Como ligar#
O administrador da prefeitura configura, na integração:
- O escopo Receber avisos (webhooks) (
webhooks). - O Endereço (HTTPS) do seu sistema que vai receber os avisos.
- Os Eventos desejados.
Ao salvar, o painel mostra o segredo do webhook (começa com whsec_) uma única vez. É com ele que você confere a assinatura. Guarde junto com o token, em cofre de segredos ou variável de ambiente.
Para buscar os detalhes do documento avisado, a integração precisa também do escopo documents:read.
Eventos#
| Evento | Quando é enviado |
|---|---|
document.created |
Um documento foi cadastrado (pela operação de digitalização, pelo painel ou pela API). |
document.updated |
A ficha ou a situação de um documento mudou (por exemplo, passou pelo controle de qualidade, foi entregue ou validado). |
document.authenticated |
O documento foi autenticado pela prefeitura com certificado ICP-Brasil. A partir daqui, o download entrega o PDF autenticado. |
Só chegam avisos de documentos que a integração enxerga (dentro do recorte), e só dos eventos marcados.
A requisição#
POST /gedflow/webhook HTTP/1.1
Host: sistema.exemplo.sp.gov.br
Content-Type: application/json
User-Agent: GedFlow-Webhooks/1
X-GedFlow-Event: document.authenticated
X-GedFlow-Delivery: 01JB8M2K4N6P8R0T2V4X6Z8B0D
X-GedFlow-Signature: t=1790000000,v1=5f0c7a1e9b3d2c4f6a8e0b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a
{"id":"01JB8M2K4N6P8R0T2V4X6Z8B0D","event":"document.authenticated","occurred_at":"2026-09-30T17:42:08+00:00","data":{"document":{"id":"01J9ZD3H7K2M5N8P1Q4R6S9T0V","code":"000123","status":"authenticated","document_type":"01J9ZC4A1B2C3D4E5F6G7H8J9K","updated_at":"2026-09-30T17:42:07+00:00"}}}
Cabeçalhos#
| Cabeçalho | Conteúdo |
|---|---|
X-GedFlow-Event |
O nome do evento. |
X-GedFlow-Delivery |
Identificador único deste aviso. É o mesmo em todas as tentativas de entrega do mesmo aviso. |
X-GedFlow-Signature |
t=<carimbo>,v1=<assinatura>. Veja abaixo. |
Corpo#
O mesmo corpo, formatado para leitura:
{
"id": "01JB8M2K4N6P8R0T2V4X6Z8B0D",
"event": "document.authenticated",
"occurred_at": "2026-09-30T17:42:08+00:00",
"data": {
"document": {
"id": "01J9ZD3H7K2M5N8P1Q4R6S9T0V",
"code": "000123",
"status": "authenticated",
"document_type": "01J9ZC4A1B2C3D4E5F6G7H8J9K",
"updated_at": "2026-09-30T17:42:07+00:00"
}
}
}
| Campo | Descrição |
|---|---|
id |
Identificador do aviso (igual ao X-GedFlow-Delivery). Use para não processar o mesmo aviso duas vezes. |
event |
O evento (igual ao X-GedFlow-Event). |
occurred_at |
Quando o evento foi registrado. |
data.document.id |
Identificador do documento. Use em GET /documents/{id}. |
data.document.code |
Código do documento na prefeitura (o número da folha de rosto, ex.: 000123). Pode vir null. |
data.document.status |
Situação do documento no momento do evento. |
data.document.document_type |
Identificador do tipo documental (pode vir null). |
data.document.updated_at |
Última alteração do documento. |
A assinatura#
Cada aviso é assinado com HMAC-SHA256, usando o segredo da integração como chave. O cabeçalho tem este formato:
X-GedFlow-Signature: t=1790000000,v1=5f0c7a1e9b3d2c4f...
té o momento do envio, em segundos desde 01/01/1970 (Unix, UTC).v1é o HMAC-SHA256, em hexadecimal minúsculo (64 caracteres), da mensagem"{t}.{corpo}": o valor det, um ponto e o corpo bruto da requisição, exatamente como chegou.
Para conferir:
- Leia o corpo bruto (os bytes), antes de qualquer conversão de JSON.
- Separe
tev1do cabeçalho. - Recuse se
testiver longe do seu relógio (recomendamos tolerância de 5 minutos). Isso impede que alguém reaproveite um aviso antigo capturado. - Calcule
HMAC-SHA256(segredo, t + "." + corpo)em hexadecimal. - Compare com
v1usando comparação em tempo constante. - Só então interprete o JSON.
PHP#
<?php
const TOLERANCE_SECONDS = 300;
function gedVerifySignature(string $body, string $header, string $secret): bool
{
$parts = [];
foreach (explode(',', $header) as $pair) {
[$key, $value] = array_pad(explode('=', trim($pair), 2), 2, '');
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? '';
$signature = $parts['v1'] ?? '';
if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {
return false;
}
$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
return hash_equals($expected, $signature);
}
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_GEDFLOW_SIGNATURE'] ?? '';
if (! gedVerifySignature($body, $header, getenv('GEDFLOW_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
// Enfileire o processamento e responda logo (veja "Responda rápido").
http_response_code(204);
Em Laravel, use $request->getContent() para o corpo bruto e $request->header('X-GedFlow-Signature') para o cabeçalho.
Python (Flask)#
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["GEDFLOW_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300
def verify_signature(body: bytes, header: str) -> bool:
parts = {}
for pair in header.split(","):
key, _, value = pair.strip().partition("=")
parts[key] = value
timestamp = parts.get("t", "")
signature = parts.get("v1", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
expected = hmac.new(SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/gedflow/webhook")
def gedflow_webhook():
body = request.get_data() # bytes, exatamente como chegaram
if not verify_signature(body, request.headers.get("X-GedFlow-Signature", "")):
abort(401)
event = request.get_json()
# Enfileire o processamento e responda logo.
return "", 204
JavaScript (Node com Express)#
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.GEDFLOW_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
function verifySignature(rawBody, header) {
const parts = {};
for (const pair of header.split(',')) {
const index = pair.indexOf('=');
if (index > 0) parts[pair.slice(0, index).trim()] = pair.slice(index + 1).trim();
}
const timestamp = parts.t ?? '';
const signature = parts.v1 ?? '';
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
return false;
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(signature, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw mantém o corpo como Buffer, sem converter o JSON.
app.post('/gedflow/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySignature(req.body, req.get('X-GedFlow-Signature') ?? '')) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Enfileire o processamento e responda logo.
return res.sendStatus(204);
});
app.listen(3000);
Responda rápido#
- Responda com qualquer código 2xx (
200,202ou204) para confirmar o recebimento. - O GedFlow espera a resposta por até 10 segundos. Mais do que isso conta como falha.
- Redirecionamentos não são seguidos: um
301ou302conta como falha. Cadastre o endereço final. - Faça o trabalho pesado depois: grave o aviso em uma fila sua, responda
2xxe processe em seguida.
Novas tentativas#
Se o seu sistema responder fora da faixa 2xx, demorar mais de 10 segundos ou estiver fora do ar, o GedFlow tenta de novo, com espera crescente:
| Tentativa | Espera depois da falha anterior |
|---|---|
| 1ª | (envio imediato) |
| 2ª | 1 minuto |
| 3ª | 5 minutos |
| 4ª | 30 minutos |
| 5ª | 2 horas |
| 6ª | 6 horas |
Depois da 6ª tentativa sem sucesso, o aviso fica como falho. O administrador da prefeitura vê cada aviso, as tentativas, o último código de resposta e o último erro na página da integração, e pode clicar em Reenviar para começar de novo.
Cada tentativa é assinada na hora em que sai, com um t novo. Por isso, a tolerância de 5 minutos não atrapalha as novas tentativas.
Para não sobrecarregar o seu sistema, os avisos de uma integração saem em ritmo limitado (padrão: 60 por minuto). Em uma carga grande, eles podem chegar com algum atraso.
Idempotência e ordem#
O mesmo aviso pode chegar mais de uma vez: por exemplo, quando o seu sistema processou mas a resposta se perdeu no caminho, ou quando o administrador clicou em Reenviar. E avisos diferentes podem chegar fora de ordem.
Para ficar à prova disso:
- Guarde o
idde cada aviso processado (ou oX-GedFlow-Delivery) e ignore os repetidos. - Não confie na ordem de chegada. Use o aviso como um "vá olhar este documento": busque
GET /documents/{id}e trabalhe com o estado atual que a API devolver. - Se quiser comparar, use
data.document.updated_atpara descartar um aviso mais antigo do que o que você já tem.
<?php
// Exemplo de processamento idempotente (depois de conferir a assinatura).
if ($repository->deliveryAlreadyProcessed($event['id'])) {
http_response_code(204);
exit;
}
$queue->push('sincronizar-documento', ['document' => $event['data']['document']['id']]);
$repository->markDeliveryProcessed($event['id']);
http_response_code(204);
Endereço seguro: HTTPS obrigatório#
- O endereço precisa ser HTTPS, com certificado válido.
- Ele não pode apontar para a rede interna (endereços privados,
localhoste semelhantes). O endereço é conferido ao salvar e de novo a cada envio. - Prefira um caminho difícil de adivinhar e confira sempre a assinatura. A assinatura, e não o segredo do caminho, é o que garante que o aviso veio do GedFlow.
Trocar o segredo#
O administrador clica em Gerar novo segredo do webhook. O novo segredo vale imediatamente para todos os envios seguintes, inclusive para as novas tentativas de avisos antigos.
Até você instalar o novo segredo, os avisos que chegarem vão falhar na sua conferência. Tudo bem: responda 401 e eles serão reenviados pelas novas tentativas. Para encurtar a janela, combine o horário da troca com o administrador e instale o novo segredo assim que recebê-lo. Durante alguns minutos, você pode aceitar os dois (conferir com o novo e, se falhar, com o antigo) e depois remover o antigo.