Pular para o conteúdo
GedFlow

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:

  1. O escopo Receber avisos (webhooks) (webhooks).
  2. O Endereço (HTTPS) do seu sistema que vai receber os avisos.
  3. 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 de t, um ponto e o corpo bruto da requisição, exatamente como chegou.

Para conferir:

  1. Leia o corpo bruto (os bytes), antes de qualquer conversão de JSON.
  2. Separe t e v1 do cabeçalho.
  3. Recuse se t estiver longe do seu relógio (recomendamos tolerância de 5 minutos). Isso impede que alguém reaproveite um aviso antigo capturado.
  4. Calcule HMAC-SHA256(segredo, t + "." + corpo) em hexadecimal.
  5. Compare com v1 usando comparação em tempo constante.
  6. 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, 202 ou 204) 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 301 ou 302 conta como falha. Cadastre o endereço final.
  • Faça o trabalho pesado depois: grave o aviso em uma fila sua, responda 2xx e 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:

  1. Guarde o id de cada aviso processado (ou o X-GedFlow-Delivery) e ignore os repetidos.
  2. 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.
  3. Se quiser comparar, use data.document.updated_at para 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, localhost e 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.