Webhooks

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:

HeaderDescrição
X-Redfox-SignatureAssinatura HMAC-SHA256 em hexadecimal
X-Redfox-TimestampUnix 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

CampoTipoSempre presenteDescrição
transactionIdstringSimID da transação PIX
transactionTypestringSimSempre PIX
transactionStatusstringSimStatus da transação
amountnumberSimValor bruto recebido
netAmountnumberNãoValor líquido após tarifas
requestNumberstringNãoReferência interna
payerNamestringNãoNome do pagador
payerTaxIdstringNãoCPF/CNPJ do pagador
paymentDatestringNãoData/hora do pagamento
paymentCodestringNãoCódigo Pix
endToEndIdstringNãoID do Banco Central
splitGatewayarrayNãoRegras de split
refundarrayNãoDevoluções

🔄 Status possíveis

ValorDescrição
PAID_OUTPagamento confirmado
CANCELEDTransação cancelada
CHARGEBACKEstorno solicitado
WAITING_FOR_APPROVALAguardando aprovação

📊 Objeto splitGateway

CampoTipoDescrição
usernamestringIdentificador do beneficiário
amountnumberValor repassado

💸 Objeto refund

CampoTipoDescrição
refundIdstringID da devolução
refundAmountnumberValor devolvido
refundDatestringData da devolução
refundStatusstringStatus 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

CampoTipoSempre presenteDescrição
transactionIdstringSimID externo ou UUID
transactionTypestringSimSempre PIX_CASHOUT
transactionStatusstringSimStatus da transação
amountnumberSimValor enviado
destinationNamestringNãoNome do destinatário
destinationTaxIdstringNãoCPF/CNPJ do destinatário
destinationBankstringNãoBanco do destinatário
endToEndIdstringNãoID do Banco Central
refundarrayNãoDevoluções

🔄 Status possíveis

ValorDescrição
PAID_OUTPagamento confirmado
CANCELEDCancelado antes do processamento
UNPAIDNão concluído
CHARGEBACKDevolução iniciada