Webhook
Atualizações nos tópicos de monitoramento serão notificadas por meio do envio de webhooks. Para tanto, é necessário, por meio da equipe do suporte, configurar um endereço do endpoint por onde vamos notificar as atualizações e também uma signature_key que será utilizada para assinar a requisição. Vale ressaltar que todos os envios de webhook serão feitos para um único endpoint.
Por questões de segurança, todas as requisições de Webhook serão somente realizadas em endpoints servidos por HTTPS.
Assinatura do Webhook
Para garantir que a requisição recebida no seu endpoint partiu dos nossos servidores, enviamos uma assinatura HMAC no header Signature. Você recalcula essa assinatura do seu lado e compara com a recebida — se forem iguais, a requisição é confiável.
Como a assinatura é calculada
Signature = HMAC-SHA1(signature_key, endpoint + method + payload) → hexadecimal
Os três componentes são concatenados nesta ordem, sem separador:
| Componente | O que é |
|---|---|
endpoint | A URL completa do seu webhook, exatamente como foi configurada com o suporte (incluindo https:// e eventual query string). |
method | O verbo HTTP em letras maiúsculas — PUT nestas notificações. |
payload | O corpo da requisição exatamente como recebido, byte a byte. |
signature_key | O segredo que você combinou com o suporte (também chamado de secret_token). É a chave do HMAC, não parte da mensagem. |
A assinatura é calculada sobre os bytes exatos do corpo. Se você desserializar o JSON e serializar de novo antes de validar, a ordem das chaves e o espaçamento mudam, e a assinatura nunca vai bater.
Leia o corpo como string/bytes brutos primeiro, valide a assinatura, e só depois faça o parse. Nos exemplos abaixo isso aparece como request.get_data(), file_get_contents('php://input'), express.raw() etc.
O corpo é serializado com ensure_ascii=False, ou seja, caracteres acentuados vão como UTF-8 literal ("João"), e não escapados ("João"). Trate o corpo como UTF-8 ao calcular o HMAC — é o comportamento padrão em todas as linguagens abaixo, mas é a causa mais comum de assinatura divergente quando algum campo tem acento.
Exemplos de validação
- Python
- PHP
- Node.js
- Java
- C#
import hashlib
import hmac
SIGNATURE_KEY = "YOUR_SECRET_TOKEN"
WEBHOOK_URL = "https://seu-dominio.com/webhooks/qitech"
HTTP_METHOD = "HTTP_VERB" # veja a tabela acima
def calculate_signature(endpoint: str, method: str, payload: str) -> str:
hmac_obj = hmac.new(
SIGNATURE_KEY.encode("utf-8"),
(endpoint + method + payload).encode("utf-8"),
hashlib.sha1,
)
return hmac_obj.hexdigest()
def is_valid(received_signature: str, raw_body: str) -> bool:
expected = calculate_signature(WEBHOOK_URL, HTTP_METHOD, raw_body)
# compare_digest evita ataques de temporização
return hmac.compare_digest(expected, received_signature)
# Exemplo com Flask
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/qitech", methods=[HTTP_METHOD])
def receive_webhook():
raw_body = request.get_data(as_text=True) # corpo bruto, sem parse
received = request.headers.get("Signature", "")
if not is_valid(received, raw_body):
return "", 401
event = request.get_json() # parse só depois de validar
print(event)
return "", 200
<?php
const SIGNATURE_KEY = 'YOUR_SECRET_TOKEN';
const WEBHOOK_URL = 'https://seu-dominio.com/webhooks/qitech';
const HTTP_METHOD = 'HTTP_VERB'; // veja a tabela acima
function calculateSignature(string $endpoint, string $method, string $payload): string
{
return hash_hmac('sha1', $endpoint . $method . $payload, SIGNATURE_KEY);
}
function isValid(string $receivedSignature, string $rawBody, string $url, string $method): bool
{
$expected = calculateSignature($url, $method, $rawBody);
// hash_equals evita ataques de temporização
return hash_equals($expected, $receivedSignature);
}
$rawBody = file_get_contents('php://input'); // corpo bruto, sem parse
$received = $_SERVER['HTTP_SIGNATURE'] ?? '';
if (!isValid($received, $rawBody, WEBHOOK_URL, HTTP_METHOD)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true); // parse só depois de validar
http_response_code(200);
const crypto = require("crypto");
const express = require("express");
const SIGNATURE_KEY = "YOUR_SECRET_TOKEN";
const WEBHOOK_URL = "https://seu-dominio.com/webhooks/qitech";
const HTTP_METHOD = "HTTP_VERB"; // veja a tabela acima
function calculateSignature(endpoint, method, payload) {
return crypto
.createHmac("sha1", SIGNATURE_KEY)
.update(endpoint + method + payload, "utf8")
.digest("hex");
}
function isValid(receivedSignature, rawBody, url, method) {
const expected = calculateSignature(url, method, rawBody);
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(receivedSignature, "utf8");
// timingSafeEqual exige buffers de mesmo tamanho
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// express.raw preserva o corpo bruto — NÃO use express.json() nesta rota
app.use(
"/webhooks/qitech",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const received = req.get("Signature") || "";
if (!isValid(received, rawBody, WEBHOOK_URL, HTTP_METHOD)) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody); // parse só depois de validar
console.log(event);
res.sendStatus(200);
},
);
app.listen(3000);
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WebhookSignature {
private static final String SIGNATURE_KEY = "YOUR_SECRET_TOKEN";
private static final String WEBHOOK_URL = "https://seu-dominio.com/webhooks/qitech";
private static final String HTTP_METHOD = "HTTP_VERB"; // veja a tabela acima
public static String calculateSignature(String endpoint, String method, String payload)
throws Exception {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(
SIGNATURE_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
byte[] digest = mac.doFinal(
(endpoint + method + payload).getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(digest.length * 2);
for (byte b : digest) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
public static boolean isValid(String receivedSignature, String rawBody,
String url, String method) throws Exception {
String expected = calculateSignature(url, method, rawBody);
// MessageDigest.isEqual evita ataques de temporização
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
receivedSignature.getBytes(StandardCharsets.UTF_8));
}
}
Em Spring Boot, receba o corpo como String para preservar os bytes originais:
@RestController
public class WebhookController {
@RequestMapping(value = "/webhooks/qitech")
public ResponseEntity<Void> receive(
@RequestBody String rawBody, // corpo bruto, sem parse
@RequestHeader(value = "Signature", required = false) String signature)
throws Exception {
if (signature == null
|| !WebhookSignature.isValid(signature, rawBody,
WebhookSignature.WEBHOOK_URL, WebhookSignature.HTTP_METHOD)) {
return ResponseEntity.status(401).build();
}
// parse só depois de validar (ex.: com Jackson)
return ResponseEntity.ok().build();
}
}
using System;
using System.Security.Cryptography;
using System.Text;
public static class WebhookSignature
{
private const string SignatureKey = "YOUR_SECRET_TOKEN";
public const string WebhookUrl = "https://seu-dominio.com/webhooks/qitech";
public const string HttpMethod = "HTTP_VERB"; // veja a tabela acima
public static string CalculateSignature(string endpoint, string method, string payload)
{
using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(SignatureKey));
var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(endpoint + method + payload));
return Convert.ToHexString(digest).ToLowerInvariant();
}
public static bool IsValid(string receivedSignature, string rawBody,
string url, string method)
{
var expected = CalculateSignature(url, method, rawBody);
// FixedTimeEquals evita ataques de temporização
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(receivedSignature));
}
}
Em ASP.NET Core, leia o corpo bruto antes de qualquer desserialização:
app.MapMethods("/webhooks/qitech", new[] { WebhookSignature.HttpMethod }, async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body, Encoding.UTF8);
var rawBody = await reader.ReadToEndAsync(); // corpo bruto, sem parse
var received = request.Headers["Signature"].ToString();
if (!WebhookSignature.IsValid(received, rawBody,
WebhookSignature.WebhookUrl, WebhookSignature.HttpMethod))
{
return Results.Unauthorized();
}
// parse só depois de validar
return Results.Ok();
});
- O corpo foi reserializado? É a causa mais frequente. Use o corpo bruto.
- A URL está idêntica? Uma barra final a mais ou a menos muda a assinatura. Use exatamente a URL configurada com o suporte.
- O método está correto e em maiúsculas? Nestas notificações é
PUT. - A ordem da concatenação está certa? É
endpoint + method + payload, nessa ordem. - O digest está em hexadecimal minúsculo? Não é Base64.
Webhook de Atualização de Evento
Request Body
{
"person_type" : "natural_person",
"account_type" : "natural_person_account",
"person_id" : "22f5d028-0ce7-46f7-9b63-e7e38171b485",
"account_id" : "e49ac344-f941-4668-9afb-a52ce4e5754a",
"monitoring_topic" : "OFAC",
"event" : "entered"
}
Abaixo está o significado de cada campo:
| Nome | Tipo | Descrição |
|---|---|---|
| person_type | string | Tipo de pessoa (natural_person ou legal_person). |
| account_type | string | Tipo de conta (natural_person_account ou legal_person_account). |
| person_id | strin | Identificador único da pessoa, passado na requisição de criação. |
| account_id | string | Identificador único da conta, passado na requisição de criação. |
| monitoring_topic | string | Tópico monitorado em que ocorreu a mudança. |
| event | string | Tipo de evento ocorrido, como "entered" (entrada) ou "exited" (saída) para os tópicos de listas restritivas. |
A requisição de atualização do tópico de monitoramento possui o formato acima e notifica a mudança no status de um dos tópico de monitoramento dentro da conta. O método utilizado é um PUT e o endereço do endpoint pode conter também o id do evento, de acordo com a necessidade do cliente. É importante ressaltar que o corpo da requisição é enviado como texto codificado em UTF-8.
Retentativas
A notificação é considerada realizada quando recebe como resposta um HTTP Status 200. Caso as notificações falhem, serão feitas 7 retentativas, com os seguintes intervalos, até que um 200 seja retornado ou as tentativas terminem:
- 10 segundos
- 40 segundos
- 160 segundos
- 640 segundos
- 2560 segundos
- 10240 segundos
- 40960 segundos