Quando uma transação muda de status, a RedFox Pay envia uma requisição HTTP POST para a callbackUrl informada no momento da criação da transação.
🔐 Autenticação e Verificação de Assinatura
Os webhooks são enviados exclusivamente do IP 18.222.67.195, sendo recomendado permitir este endereço em sua lista de IPs confiáveis (whitelist).
Todos os webhooks são enviados com os seguintes headers:
| Header | Descrição |
|---|---|
| X-Redfox-Signature | Assinatura HMAC-SHA256 em hexadecimal |
| X-Redfox-Timestamp | Unix timestamp em segundos usado na assinatura |
🧠 Como verificar a assinatura
A assinatura é gerada da seguinte forma:
assinado = timestamp + "." + rawBody
signature = HMAC-SHA256(assinado, clientSecret)
O clientSecret é a chave secreta da sua aplicação
O rawBody deve ser o corpo da requisição exatamente como recebido (sem re-serialização)
💻 Exemplos
<?php
function isValidSignature(string $rawBody, string $timestamp, string $receivedSignature, string $clientSecret): bool
{
$signedPayload = $timestamp . '.' . $rawBody;
$expectedSignature = hash_hmac('sha256', $signedPayload, $clientSecret);
return hash_equals($expectedSignature, $receivedSignature);
}
// Exemplo de uso
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_REDFOX_TIMESTAMP'] ?? '';
$receivedSignature = $_SERVER['HTTP_X_REDFOX_SIGNATURE'] ?? '';
$clientSecret = 'seu_client_secret';
if (!isValidSignature($rawBody, $timestamp, $receivedSignature, $clientSecret)) {
http_response_code(401);
echo json_encode(['message' => 'Assinatura inválida']);
exit;
}
http_response_code(200);
echo json_encode(['message' => 'Webhook recebido com sucesso']);const crypto = require('crypto');
function isValidSignature(rawBody, timestamp, receivedSignature, clientSecret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(signedPayload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'utf8'),
Buffer.from(receivedSignature, 'utf8')
);
}
// Exemplo de uso com Express
// Importante: capturar o body bruto (raw body)
const express = require('express');
const app = express();
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
})
);
app.post('/webhook', (req, res) => {
const timestamp = req.header('X-Redfox-Timestamp') || '';
const receivedSignature = req.header('X-Redfox-Signature') || '';
const clientSecret = 'seu_client_secret';
const rawBody = req.rawBody || '';
if (!isValidSignature(rawBody, timestamp, receivedSignature, clientSecret)) {
return res.status(401).json({ message: 'Assinatura inválida' });
}
return res.status(200).json({ message: 'Webhook recebido com sucesso' });
});
app.listen(3000, () => {
console.log('Servidor rodando na porta 3000');
});import hmac
import hashlib
def is_valid_signature(raw_body: str, timestamp: str, received_signature: str, client_secret: str) -> bool:
signed_payload = f"{timestamp}.{raw_body}"
expected_signature = hmac.new(
client_secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, received_signature)
# Exemplo simples
if __name__ == "__main__":
raw_body = '{"transactionId":"847","transactionType":"PIX"}'
timestamp = "1715600000"
received_signature = "assinatura_recebida_no_header"
client_secret = "seu_client_secret"
if is_valid_signature(raw_body, timestamp, received_signature, client_secret):
print("Assinatura válida")
else:
print("Assinatura inválida")public static boolean isValid(String payload, String timestamp, String signature, String clientSecret) {
String expectedSignature = generateHmac(timestamp + "." + payload, clientSecret);
return MessageDigest.isEqual(
expectedSignature.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8)
);
}
private static String generateHmac(String message, String secret) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
String h = Integer.toHexString(0xff & b);
if (h.length() == 1) hex.append('0');
hex.append(h);
}
return hex.toString();
}✅ Resposta esperada
Retorne HTTP 200 ou 201 para confirmar o recebimento.
Qualquer outro código será tratado como falha e o webhook será reenviado automaticamente (até 3 tentativas).
💰 Webhook — PIX Recebido
Disparado quando um QR Code PIX é pago pelo pagador.
transactionType: PIX
📦 Exemplo de payload
{
"transactionId": "847",
"transactionType": "PIX",
"transactionStatus": "PAID_OUT",
"amount": 150.00,
"netAmount": 148.50,
"requestNumber": "REQ-20240101-001",
"payerName": "João da Silva",
"payerTaxId": "123.456.789-00",
"paymentDate": "2024-01-15T14:32:00",
"paymentCode": "00020126580014br.gov.bcb.pix...",
"endToEndId": "E35713491202604092031205962c9eff",
"splitGateway": [
{ "username": "parceiro01", "amount": 10.00 }
],
"refund": [
{
"refundId": "REF-001",
"refundAmount": 50.00,
"refundDate": "2024-01-16T10:00:00",
"refundStatus": "PAID_OUT"
}
]
}📋 Campos
| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| transactionId | string | Sim | ID da transação PIX |
| transactionType | string | Sim | Sempre PIX |
| transactionStatus | string | Sim | Status da transação |
| amount | number | Sim | Valor bruto recebido |
| netAmount | number | Não | Valor líquido após tarifas |
| requestNumber | string | Não | Referência interna |
| payerName | string | Não | Nome do pagador |
| payerTaxId | string | Não | CPF/CNPJ do pagador |
| paymentDate | string | Não | Data/hora do pagamento |
| paymentCode | string | Não | Código Pix |
| endToEndId | string | Não | ID do Banco Central |
| splitGateway | array | Não | Regras de split |
| refund | array | Não | Devoluções |
🔄 Status possíveis
| Valor | Descrição |
|---|---|
| PAID_OUT | Pagamento confirmado |
| CANCELED | Transação cancelada |
| CHARGEBACK | Estorno solicitado |
| WAITING_FOR_APPROVAL | Aguardando aprovação |
📊 Objeto splitGateway
| Campo | Tipo | Descrição |
|---|---|---|
| username | string | Identificador do beneficiário |
| amount | number | Valor repassado |
💸 Objeto refund
| Campo | Tipo | Descrição |
|---|---|---|
| refundId | string | ID da devolução |
| refundAmount | number | Valor devolvido |
| refundDate | string | Data da devolução |
| refundStatus | string | Status da devolução |
💸 Webhook — PIX Enviado
Disparado quando um pagamento PIX via gateway tem seu status atualizado.
transactionType: PIX_CASHOUT
📦 Exemplo de payload
{
"transactionId": "C9CBEE8E-B1C4-4E37-BDE4-7373CADD34A5",
"transactionType": "PIX_CASHOUT",
"transactionStatus": "PAID_OUT",
"amount": 200.00,
"destinationName": "Maria Oliveira",
"destinationTaxId": "987.654.321-00",
"destinationBank": "Itaú Unibanco",
"endToEndId": "E35713491202604092031205962c9eff",
"refund": [
{
"refundId": "REF-002",
"refundAmount": 200.00,
"refundDate": "2024-01-17T09:00:00",
"refundStatus": "PAID_OUT"
}
]
}📋 Campos
| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| transactionId | string | Sim | ID externo ou UUID |
| transactionType | string | Sim | Sempre PIX_CASHOUT |
| transactionStatus | string | Sim | Status da transação |
| amount | number | Sim | Valor enviado |
| destinationName | string | Não | Nome do destinatário |
| destinationTaxId | string | Não | CPF/CNPJ do destinatário |
| destinationBank | string | Não | Banco do destinatário |
| endToEndId | string | Não | ID do Banco Central |
| refund | array | Não | Devoluções |
🔄 Status possíveis
| Valor | Descrição |
|---|---|
| PAID_OUT | Pagamento confirmado |
| CANCELED | Cancelado antes do processamento |
| UNPAID | Não concluído |
| CHARGEBACK | Devolução iniciada |

