Webhook
Atualizações no status de fraude (Para RentalAgreements que sejam derivados para análise manual ou que sejam respondidos como Pendente), 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 atualizações e também um secret_token que será utilizado para assinar a requisição.
O cliente pode, apesar de não recomendável, também utilizar a técnica de polling. Neste caso, basta não configurar o endpoint de webhook e utilizar os endpoints de recuperação de RentalAgreement para proceder com o polling.
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 — POST 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 é
POST. - A ordem da concatenação está certa? É
endpoint + method + payload, nessa ordem. - O digest está em hexadecimal minúsculo? Não é Base64.
Requisição
Exemplo de requisição:
{
"rental_agreement_id": "123456",
"fraud_status": "automatically_approved",
"upgrade_status": "automatically_approved",
"event_date": "2019-10-01T10:37:25-03:00"
}
A requisição possui o formato acima e notifica a mudança no status de fraude.
Retentativas
A notificação é considerada realizada quando recebe como resposta um HTTP Status 200. Caso as notificações falhem, serão feitas retentativas, com os seguintes intervalos, até que um 200 seja retornado ou as tentativas terminem:
- 10 segundos
- 30 segundos
- 60 segundos
- 120 segundos
- 120 segundos
- 3600 segundos
- 7200 segundos
- 36000 segundos