Alertas de Portadores
Os alertas gerados pela ferramenta antifraude são notificados por meio de Webhook. Para tanto, é necessário, por meio da equipe do suporte, configurar um endereço do endpoint por onde vamos notificar as notificações e também um secret_token que será utilizado para assinar a requisição.
Nesta notificação enviaremos informações dos alertas gerados, bem como de qual portador se trata, para que o cliente possa tomar alguma ação, por exemplo, enviar um push notification para o portador.
Requisição
Request Body
{
"alert_key": "123456",
"cardholder_id": "ef47bc3f-61ac-4b85-ad67-0cfa3a422201",
"company_name": "Cliente 1",
"irregularity_type" : "fraud",
"risk_level": "critical"
}
A requisição possui o formato acima e notifica a abertura de um novo alerta para um Portador - descrito pelo cardholder_id
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 — sempre POST nas notificações de alerta. |
payload | O corpo da requisição exatamente como recebido, byte a byte. |
signature_key | O secret_token que você combinou com o suporte. É 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.data, file_get_contents('php://input'), req.rawBody etc.
Nós serializamos o corpo 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 o company_name 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/alertas"
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, "POST", 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/alertas", methods=["POST"])
def receive_alert():
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
alert = request.get_json() # parse só depois de validar
print(alert["cardholder_id"], alert["risk_level"])
return "", 200
<?php
const SIGNATURE_KEY = 'YOUR_SECRET_TOKEN';
const WEBHOOK_URL = 'https://seu-dominio.com/webhooks/qitech/alertas';
function calculateSignature(string $endpoint, string $method, string $payload): string
{
return hash_hmac('sha1', $endpoint . $method . $payload, SIGNATURE_KEY);
}
function isValid(string $receivedSignature, string $rawBody): bool
{
$expected = calculateSignature(WEBHOOK_URL, 'POST', $rawBody);
// hash_equals evita ataques de temporização
return hash_equals($expected, $receivedSignature);
}
// Recebendo a notificação
$rawBody = file_get_contents('php://input'); // corpo bruto, sem parse
$received = $_SERVER['HTTP_SIGNATURE'] ?? '';
if (!isValid($received, $rawBody)) {
http_response_code(401);
exit;
}
$alert = json_decode($rawBody, true); // parse só depois de validar
error_log($alert['cardholder_id'] . ' - ' . $alert['risk_level']);
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/alertas";
function calculateSignature(endpoint, method, payload) {
return crypto
.createHmac("sha1", SIGNATURE_KEY)
.update(endpoint + method + payload, "utf8")
.digest("hex");
}
function isValid(receivedSignature, rawBody) {
const expected = calculateSignature(WEBHOOK_URL, "POST", 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.post(
"/webhooks/qitech/alertas",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const received = req.get("Signature") || "";
if (!isValid(received, rawBody)) {
return res.sendStatus(401);
}
const alert = JSON.parse(rawBody); // parse só depois de validar
console.log(alert.cardholder_id, alert.risk_level);
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/alertas";
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)
throws Exception {
String expected = calculateSignature(WEBHOOK_URL, "POST", 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:
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class AlertController {
@PostMapping("/webhooks/qitech/alertas")
public ResponseEntity<Void> receiveAlert(
@RequestBody String rawBody, // corpo bruto, sem parse
@RequestHeader(value = "Signature", required = false) String signature)
throws Exception {
if (signature == null || !WebhookSignature.isValid(signature, rawBody)) {
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";
private const string WebhookUrl =
"https://seu-dominio.com/webhooks/qitech/alertas";
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)
{
var expected = CalculateSignature(WebhookUrl, "POST", 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.MapPost("/webhooks/qitech/alertas", 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))
{
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 (
/alertasvs/alertas/) muda a assinatura. Use exatamente a URL configurada com o suporte. - O método está em maiúsculas? Deve ser
POST, nãopost. - A ordem da concatenação está certa? É
endpoint + method + payload, nessa ordem. - O digest está em hexadecimal minúsculo? Não é Base64.
Retentativas
A notificação é considerada realizada quando recebe como resposta um HTTP Status 200. Caso as notificações falhem, serão feitas 5 retentativas, com os seguintes intervalos, até que um 200 seja retornado ou as tentativas terminem:
- 30 segundos
- 60 segundos
- 120 segundos
- 240 segundos
- 360 segundos