A BSPAY envia eventos via POST para a postback_url configurada na credencial (default) ou na própria transação (override). Você usa esses eventos pra liberar acesso, atualizar status e disparar notificações.
Content-Type: application/json
X-Webhook-Event: <nome do evento>
X-Webhook-Signature: hex(hmac_sha256(rawBody, webhook_secret))
X-Webhook-Id: evt_<random>
X-Webhook-Timestamp: <unix_seconds>
User-Agent: BSPay-Webhook/2.0
Quando a credencial é sandbox, o header X-Sandbox: 1 é adicionado. Trate o ambiente sandbox como não-financeiro no seu backend.
Toda requisição vem assinada com o seu webhook_secret (definido no Dashboard ao habilitar webhooks). Recalcule o HMAC sobre o body raw e compare com X-Webhook-Signature antes de processar — se não bater, rejeite com 401.
PHP
Node.js / Express
Python / Flask
Go
<?php
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$ts = (int) ($_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? 0);
// 1) Janela ±5min anti-replay
if (abs(time() - $ts) > 300) {
http_response_code(401);
exit;
}
// 2) HMAC SHA256 timing-safe
$expected = hash_hmac('sha256', $rawBody, $WEBHOOK_SECRET);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$payload = json_decode($rawBody, true);
// processar...
http_response_code(200);
echo json_encode(['received' => true]);
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// IMPORTANTE: capturar raw body antes do parse JSON
app.post('/webhook/bspay',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-webhook-signature'];
const ts = parseInt(req.headers['x-webhook-timestamp'], 10);
const raw = req.body; // Buffer
// 1) anti-replay
if (Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(401).end();
}
// 2) HMAC timing-safe
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(raw)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).end();
}
const payload = JSON.parse(raw.toString('utf8'));
// processar...
res.status(200).json({ received: true });
}
);
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "..."
@app.route('/webhook/bspay', methods=['POST'])
def webhook():
raw = request.get_data() # bytes — antes do JSON parse
sig = request.headers.get('X-Webhook-Signature', '')
ts = int(request.headers.get('X-Webhook-Timestamp', '0'))
# 1) anti-replay
if abs(int(time.time()) - ts) > 300:
abort(401)
# 2) HMAC timing-safe
expected = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
payload = request.get_json()
# processar...
return {'received': True}, 200
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"time"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Webhook-Signature")
ts, _ := strconv.ParseInt(r.Header.Get("X-Webhook-Timestamp"), 10, 64)
// 1) anti-replay
if abs(time.Now().Unix()-ts) > 300 {
w.WriteHeader(401); return
}
// 2) HMAC
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write(raw)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(sig)) {
w.WriteHeader(401); return
}
// processar...
w.WriteHeader(200)
}
Use o body raw (bytes), não o JSON parseado. O HMAC é calculado sobre os bytes exatos enviados — qualquer re-serialização (espaços, ordenação de chaves, escape de unicode) quebra a assinatura.
Todo payload chega no formato:
{
"event": "cashin.confirmed",
"timestamp": "2026-05-01T12:34:56-03:00",
"transaction_id": "abc123def456",
"data": {
"transaction_id": "abc123def456",
"amount": "...",
"..."
}
}
| Campo | Descrição |
|---|
event | Nome canônico do evento em dot notation |
timestamp | ISO 8601 com timezone — momento que o webhook foi gerado |
transaction_id | Espelho do data.transaction_id no nível raiz (idempotência mais simples) |
data | Payload específico do evento (varia conforme o tipo) |
Se seu endpoint não responder com HTTP 2xx (timeout, 5xx, DNS fail), reentregamos com backoff exponencial:
| Tentativa | Delay após anterior |
|---|
| 1 (síncrono no momento do evento) | — |
| 2 | +1s |
| 3 | +5s |
| 4 | +30s |
| 5 | +120s |
| 6 (final) | +600s |
Após 5 retries falhos, paramos de tentar. Solicite reentrega manual via Dashboard ou suporte.
Implementação no seu lado:
- Responda HTTP 200 o quanto antes (idealmente <2s). Processamento pesado deve ser assíncrono.
- Trate idempotência via
transaction_id ou X-Webhook-Id — duplicate delivery pode ocorrer. - URL deve ser HTTPS com certificado válido.
http:// ou self-signed são rejeitadas. - Limite: 10s timeout total + 5s connect timeout.
Pra evitar bursts no seu endpoint em picos de transação:
- 100 webhooks/minuto por integration (sliding window 60s)
- Quando excede, webhooks adicionais são enfileirados e reentregues após +120s, mantendo a ordem aproximada
- Você não perde eventos — apenas alguns chegam atrasados em picos extremos
Throughput maior? Fale com suporte.
Sobre estornos:
cashin.refunded — cashin revertido por qualquer causa. Sempre disparado quando saldo é debitado de volta.chargeback.confirmed — disputa MED resultou em devolução. Disparado junto com cashin.refunded quando a origem é chargeback.cashout.refunded — saque estornado pelo provider (ex: PIX devolvido pela instituição destino).conversion.refunded — conversão estornada via suporte (raro, casos de erro/fraude).
Pagador completou a cobrança (PIX dinâmico/estático, cripto temporário/wallet fixa, SPEI). Saldo já creditado.
PIX BRL
Cripto USDT TRC20
Wallet fixa (source: static)
SPEI MXN
{
"event": "cashin.confirmed",
"timestamp": "2026-05-01T13:00:04-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "order_xyz",
"type": "cashin",
"currency": "BRL",
"currency_type": "fiat",
"amount": "100.00",
"fee": "1.50",
"amount_net": "98.50",
"status": "confirmed",
"source": "dynamic_qr",
"network": "PIX",
"e2e_id": "E17028875202604301400ABC123",
"payer": { "name": "João Silva", "document": "12345678901" },
"old_balance": "500.00",
"new_balance": "598.50",
"confirmed_at": "2026-05-01T13:00:04Z"
}
}
{
"event": "cashin.confirmed",
"timestamp": "2026-05-01T13:00:30-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "crypto_001",
"type": "cashin",
"currency": "USDT",
"currency_type": "crypto",
"chain": "tron",
"network": "TRC20",
"amount": "50.00",
"fee": "1.00",
"amount_net": "49.00",
"status": "confirmed",
"source": "dynamic_qr",
"tx_hash": "1c71b23e0fac...",
"old_balance": "1000.00",
"new_balance": "1049.00",
"confirmed_at": "2026-05-01T13:00:30Z"
}
}
{
"event": "cashin.confirmed",
"timestamp": "2026-05-01T13:00:00-03:00",
"transaction_id": "wdep_abc123",
"data": {
"transaction_id": "wdep_abc123",
"wallet_id": 41,
"type": "cashin",
"currency": "USDT",
"currency_type": "crypto",
"chain": "tron",
"network": "TRC20",
"amount": "5.403",
"fee": "1.000",
"amount_net": "4.403",
"status": "confirmed",
"source": "static",
"address": "TWMrdKSXa...",
"tx_hash": "24669f333...",
"from_address": "TKxSenderAddress...",
"old_balance": "100.00",
"new_balance": "104.403",
"confirmed_at": "2026-05-01T13:00:00Z"
}
}
{
"event": "cashin.confirmed",
"timestamp": "2026-05-01T13:01:00-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "spei_001",
"type": "cashin",
"currency": "MXN",
"currency_type": "fiat",
"network": "SPEI",
"amount": "200.00",
"fee": "5.00",
"amount_net": "195.00",
"status": "confirmed",
"source": "dynamic_qr",
"clabe": "012345678901234567",
"payer": { "name": "Maria Lopez", "bank": "BANORTE" },
"old_balance": "100.00",
"new_balance": "295.00",
"confirmed_at": "2026-05-01T13:01:00Z"
}
}
source — dynamic_qr para cobranças geradas via cashin, static para depósitos em carteira fixa.
Cashin revertido por qualquer causa (refund manual, falha do provider, ou chargeback perdido). Saldo debitado.
{
"event": "cashin.refunded",
"timestamp": "2026-05-01T14:00:00-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"original_transaction_id": "abc123",
"external_id": "order_xyz",
"currency": "BRL",
"amount": "100.00",
"amount_refunded": "100.00",
"status": "refunded",
"reason": "PROVIDER_REVERSAL",
"old_balance": "598.50",
"new_balance": "498.50",
"refunded_at": "2026-05-01T14:00:00Z"
}
}
QR PIX expirou sem pagamento (não houve crédito). Bom pra limpar pedidos pendentes na sua DB.
{
"event": "cashin.expired",
"timestamp": "2026-05-01T14:30:00-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "order_xyz",
"currency": "BRL",
"amount": "100.00",
"status": "expired",
"expired_at": "2026-05-01T14:30:00Z"
}
}
Saque processado pelo provider (PIX liquidado, tx broadcast on-chain, SPEI confirmado).
PIX BRL
Cripto USDT TRC20
SPEI MXN
{
"event": "cashout.confirmed",
"timestamp": "2026-05-01T13:00:04-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "withdraw_001",
"type": "cashout",
"currency": "BRL",
"currency_type": "fiat",
"network": "PIX",
"amount": "50.00",
"fee": "1.00",
"status": "confirmed",
"wallet": "12345678901",
"key_type": "cpf",
"hash": "E17028875202604301400ABC123",
"receiver": {
"name": "João Silva",
"document": "12345678901",
"bank": "BCO DO BRASIL S.A."
},
"confirmed_at": "2026-05-01T13:00:04Z"
}
}
{
"event": "cashout.confirmed",
"timestamp": "2026-05-01T13:00:30-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "withdraw_002",
"type": "cashout",
"currency": "USDT",
"currency_type": "crypto",
"chain": "tron",
"network": "TRC20",
"amount": "50.00",
"fee": "1.50",
"status": "confirmed",
"wallet": "TRwLhXvqNqTqbGgHNPCEfLeEfDwfWTpuxi",
"tx_hash": "1c71b23e0fac4e...",
"confirmed_at": "2026-05-01T13:00:30Z"
}
}
{
"event": "cashout.confirmed",
"timestamp": "2026-05-01T13:01:00-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "withdraw_003",
"type": "cashout",
"currency": "MXN",
"currency_type": "fiat",
"network": "SPEI",
"amount": "200.00",
"fee": "10.00",
"status": "confirmed",
"wallet": "012345678901234567",
"bank_code": "BANORTE",
"receiver": { "name": "Maria Lopez" },
"confirmed_at": "2026-05-01T13:01:00Z"
}
}
Saque rejeitado pelo gateway/provider. Saldo refundado automaticamente — você não precisa estornar manualmente.
{
"event": "cashout.failed",
"timestamp": "2026-05-01T13:05:00-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"external_id": "withdraw_001",
"type": "cashout",
"currency": "USDT",
"network": "TRC20",
"amount": "50.00",
"status": "failed",
"wallet": "TRwLh...",
"error_code": "TRON_ENERGY_FAIL",
"error_message": "Insufficient energy on hot wallet to broadcast.",
"refunded_amount": "51.50",
"failed_at": "2026-05-01T13:05:00Z"
}
}
error_code | Significado |
|---|
TRON_ENERGY_FAIL | Hot wallet sem energia/bandwidth para broadcast |
INSUFFICIENT_BALANCE | Hot wallet sem saldo on-chain |
NO_HOT_WALLET | Nenhuma wallet quente disponível para esta chain |
INVALID_ADDRESS | Endereço destino inválido (checksum/formato) |
BLACKLISTED | Endereço destino na blacklist (sanção/AML) |
BLOCKCHAIN_REVERT | Tx revertida no bloco |
BROADCAST_FAIL | Falha ao enviar tx para a rede |
PIX_KEY_NOT_FOUND | Chave PIX não encontrada no DICT |
PROVIDER_TIMEOUT | Provider PIX/SPEI timeout |
UNKNOWN | Erro não categorizado — contate o suporte |
Saque estornado pelo provider após já ter sido confirmado (caso raro — geralmente devolução pela instituição destino). Saldo creditado de volta.
{
"event": "cashout.refunded",
"timestamp": "2026-05-01T14:00:00-03:00",
"transaction_id": "abc123",
"data": {
"transaction_id": "abc123",
"original_transaction_id": "abc123",
"external_id": "withdraw_001",
"currency": "BRL",
"network": "PIX",
"amount": "50.00",
"amount_refunded": "50.00",
"status": "refunded",
"reason": "PROVIDER_REVERSAL",
"refunded_at": "2026-05-01T14:00:00Z"
}
}
Transferência interna concluída. Ambas as contas (remetente e destinatário) recebem este evento.
Recebida (no destinatário)
Enviada (no remetente)
{
"event": "transfer.confirmed",
"timestamp": "2026-05-01T13:00:00-03:00",
"transaction_id": "tx_abc123",
"data": {
"transaction_id": "tx_abc123",
"direction": "received",
"amount": "25.00",
"currency": "BRL",
"counterparty": "sender_username",
"description": "Pagamento parceiro",
"old_balance": "100.00",
"new_balance": "125.00",
"confirmed_at": "2026-05-01T13:00:00Z"
}
}
{
"event": "transfer.confirmed",
"timestamp": "2026-05-01T13:00:00-03:00",
"transaction_id": "tx_abc123",
"data": {
"transaction_id": "tx_abc123",
"external_id": "transfer_001",
"direction": "sent",
"amount": "25.00",
"currency": "BRL",
"counterparty": "receiver_username",
"description": "Pagamento parceiro",
"old_balance": "150.00",
"new_balance": "125.00",
"confirmed_at": "2026-05-01T13:00:00Z"
}
}
Conversão FX executada — saldo de origem debitado e destino creditado.
{
"event": "conversion.confirmed",
"timestamp": "2026-05-01T13:00:00-03:00",
"transaction_id": "conv_abc",
"data": {
"conversion_id": "conv_abc",
"external_id": "conv_001",
"amount_from": "80.00",
"currency_from": "USDT",
"amount_to": "432.00",
"currency_to": "BRL",
"rate": "5.4000",
"fee": "1.50",
"status": "completed",
"completed_at": "2026-05-01T13:00:00Z"
}
}
Conversão estornada via operação manual do suporte (raro — casos de erro ou fraude).
{
"event": "conversion.refunded",
"timestamp": "2026-05-01T15:00:00-03:00",
"transaction_id": "conv_abc",
"data": {
"conversion_id": "conv_abc",
"original_conversion_id": "conv_abc",
"amount_reverted_from": "432.00",
"currency_reverted_from": "BRL",
"amount_returned_to": "80.00",
"currency_returned_to": "USDT",
"reason": "SUPPORT_MANUAL_REVERSAL",
"refunded_at": "2026-05-01T15:00:00Z"
}
}
Carteira fixa provisionada (chave PIX estática, CLABE permanente ou endereço cripto on-chain).
{
"event": "wallet.created",
"timestamp": "2026-05-01T13:00:00-03:00",
"transaction_id": "wallet_41",
"data": {
"wallet_id": 41,
"currency": "USDT",
"currency_type": "crypto",
"chain": "tron",
"network": "TRC20",
"address": "TWMrdKSXaTbpcjBAT5PEzL4LtkC9fL4HQk",
"min_confirmations": 20,
"created_at": "2026-05-01T13:00:00Z"
}
}
Disputa MED PIX aberta pelo pagador (ver Disputas). Saldo bloqueado automaticamente. Você tem 7 dias pra responder via /v2/account/infractions/reply.
{
"event": "chargeback.opened",
"timestamp": "2026-05-01T13:00:00-03:00",
"transaction_id": "tx_xyz789",
"data": {
"infraction_id": "abc-uuid-1234",
"transaction_id": "tx_xyz789",
"type": "REFUND_REQUEST",
"status": "open",
"amount": "25.00",
"currency": "BRL",
"e2e_id": "E17028875202604301400ABC123",
"deadline_at": "2026-05-08T13:00:00Z",
"reason": "Customer disputed — alleged unauthorized payment",
"created_at": "2026-05-01T13:00:00Z"
}
}
Sua resposta foi registrada via /v2/account/infractions/reply. Aguarde decisão do banco/PSP.
{
"event": "chargeback.responded",
"timestamp": "2026-05-01T15:00:00-03:00",
"transaction_id": "tx_xyz789",
"data": {
"infraction_id": "abc-uuid-1234",
"transaction_id": "tx_xyz789",
"reply_id": 60,
"status": "responded",
"responded_at": "2026-05-01T15:00:00Z"
}
}
Chargeback finalizado — dinheiro devolvido ao pagador. Disparado junto com cashin.refunded quando a origem é uma disputa MED.
{
"event": "chargeback.confirmed",
"timestamp": "2026-05-05T10:00:00-03:00",
"transaction_id": "tx_xyz789",
"data": {
"infraction_id": "abc-uuid-1234",
"transaction_id": "tx_xyz789",
"amount": "25.00",
"currency": "BRL",
"confirmed_at": "2026-05-05T10:00:00Z"
}
}
Defesa aceita pelo PSP. Saldo desbloqueado e mantido na sua conta.
{
"event": "chargeback.won",
"timestamp": "2026-05-05T10:00:00-03:00",
"transaction_id": "tx_xyz789",
"data": {
"infraction_id": "abc-uuid-1234",
"transaction_id": "tx_xyz789",
"amount": "25.00",
"currency": "BRL",
"resolved_at": "2026-05-05T10:00:00Z"
}
}
Defesa rejeitada. Saldo é debitado e devolvido ao pagador. cashin.refunded também dispara.
{
"event": "chargeback.lost",
"timestamp": "2026-05-05T10:00:00-03:00",
"transaction_id": "tx_xyz789",
"data": {
"infraction_id": "abc-uuid-1234",
"transaction_id": "tx_xyz789",
"amount": "25.00",
"currency": "BRL",
"amount_refunded": "25.00",
"resolved_at": "2026-05-05T10:00:00Z"
}
}
Disputa cancelada pelo próprio pagador antes da decisão. Saldo desbloqueado.
{
"event": "chargeback.canceled",
"timestamp": "2026-05-03T10:00:00-03:00",
"transaction_id": "tx_xyz789",
"data": {
"infraction_id": "abc-uuid-1234",
"transaction_id": "tx_xyz789",
"amount": "25.00",
"currency": "BRL",
"canceled_by": "payer",
"canceled_at": "2026-05-03T10:00:00Z"
}
}
- Valide a assinatura HMAC sempre — sem isso, qualquer um pode forjar webhooks
- Responda HTTP 200 imediatamente — processe em background (queue, async task)
- Idempotência via
X-Webhook-Id ou transaction_id — o mesmo evento pode chegar 2+ vezes após retry - Logue
request_id quando reportar bugs ao suporte - Reconcile periodicamente via
POST /v2/account/transactions/list — webhook não é fonte única de verdade - Sandbox — rejeite explicitamente eventos com
X-Sandbox: 1 no seu sistema de produção