Webhooks
Receba notificações em tempo real quando pagamentos são confirmados, saques são completados e mais.
Webhooks
O Moneyfly te notifica em tempo real sobre o status das suas transações via HTTP POST.
Como configurar
- Vá em app.moneyflybr.com/integrations
- Em Webhooks, clique em "+ Novo webhook"
- Informe a URL do seu endpoint (HTTPS obrigatório em produção)
- Selecione quais eventos quer receber
- Copie o webhook secret que aparece uma única vez — você vai usar pra validar assinaturas
Você também pode sobrescrever o webhook por transação usando o campo webhook_url na request de criação.
Eventos disponíveis
| Evento | Quando dispara |
|---|---|
deposit.pending | Depósito criado |
deposit.paid | Cliente final pagou o PIX — dinheiro caiu |
deposit.cancelled | Depósito cancelado (expirou, ou merchant cancelou) |
deposit.refunded | Depósito estornado |
payout.pending_approval | Saque criado, aguardando aprovação manual do Moneyfly (controle anti-fraude) |
payout.pending | Saque aprovado pelo admin Moneyfly e em processamento na provedora |
payout.paid | Saque efetivado — beneficiário recebeu |
payout.cancelled | Saque cancelado (saldo devolvido) |
payout.failed | Saque falhou (saldo devolvido) |
Anatomia do payload
{
"event": "deposit.paid",
"event_id": "evt_xxx...",
"internal_id": "cl_abc123...",
"external_id": "pedido-12345",
"status": "paid",
"amount_cents": 9990,
"utm": "instagram-ads",
"timestamp": "2026-04-24T18:01:32Z",
"attempt": 1
}Headers que enviamos
Todo POST inclui:
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Moneyfly-Webhooks/1.0 |
X-Moneyfly-Signature | t=<unix_seconds>,v1=<hex> — timestamp + assinatura HMAC |
X-Moneyfly-Event-Id | ID único do envio |
X-Moneyfly-Event-Type | Tipo do evento |
X-Moneyfly-Attempt | Número da tentativa |
Validando a assinatura
OBRIGATÓRIO. Sem isso, qualquer um que descobrir seu endpoint pode mandar deposit.paid falso e forçar liberação de produto.
A assinatura é HMAC-SHA256("<timestamp>.<body_cru>", webhook_secret) em hex.
O timestamp (em Unix seconds) vai no próprio header X-Moneyfly-Signature no formato t=<ts>,v1=<hex>. O v1= é versionado pra que mudanças futuras no esquema possam coexistir.
Recomendação adicional: rejeite payloads cujo timestamp esteja fora de uma janela de tolerância (ex: 5 min). Sem essa checagem, atacante que captura uma entrega válida pode replay infinito — esse é o ponto principal do timestamp.
Node.js
import crypto from 'node:crypto';
const TOLERANCE_SECONDS = 300; // 5 minutos
function parseSignatureHeader(header) {
const parts = Object.fromEntries(
header.split(',').map((p) => {
const [k, ...v] = p.split('=');
return [k.trim(), v.join('=').trim()];
}),
);
return { t: parts.t, v1: parts.v1 };
}
export function verifySignature(rawBody, signatureHeader, webhookSecret) {
const { t, v1 } = parseSignatureHeader(signatureHeader);
if (!t || !v1) return false;
// 1. Anti-replay: timestamp dentro de 5 min
const ageSec = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
if (ageSec > TOLERANCE_SECONDS) return false;
// 2. HMAC do `${timestamp}.${body}`
const signedPayload = `${t}.${rawBody}`;
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
const a = Buffer.from(v1, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/moneyfly', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-moneyfly-signature'];
if (!verifySignature(req.body.toString('utf8'), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).end();
});Python
import hmac, hashlib, time
TOLERANCE_SECONDS = 300
def parse_signature(header: str) -> dict:
return dict(p.split('=', 1) for p in header.split(','))
def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = parse_signature(signature_header)
t = parts.get('t')
v1 = parts.get('v1')
if not t or not v1:
return False
if abs(int(time.time()) - int(t)) > TOLERANCE_SECONDS:
return False
signed = f"{t}.{raw_body.decode('utf-8')}".encode()
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)PHP
const TOLERANCE_SECONDS = 300;
function parse_signature(string $header): array {
$out = [];
foreach (explode(',', $header) as $p) {
[$k, $v] = explode('=', $p, 2);
$out[trim($k)] = trim($v);
}
return $out;
}
function verify_signature(string $rawBody, string $signatureHeader, string $secret): bool {
$parts = parse_signature($signatureHeader);
if (empty($parts['t']) || empty($parts['v1'])) return false;
if (abs(time() - intval($parts['t'])) > TOLERANCE_SECONDS) return false;
$signed = $parts['t'] . '.' . $rawBody;
$expected = hash_hmac('sha256', $signed, $secret);
return hash_equals($expected, $parts['v1']);
}Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
)
const ToleranceSeconds = 300
func parseSignature(header string) (string, string) {
var t, v1 string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 {
continue
}
switch strings.TrimSpace(kv[0]) {
case "t":
t = strings.TrimSpace(kv[1])
case "v1":
v1 = strings.TrimSpace(kv[1])
}
}
return t, v1
}
func verifySignature(rawBody []byte, signatureHeader, secret string) bool {
t, v1 := parseSignature(signatureHeader)
if t == "" || v1 == "" {
return false
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return false
}
if abs64(time.Now().Unix()-ts) > ToleranceSeconds {
return false
}
signed := fmt.Sprintf("%s.%s", t, rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signed))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(v1))
}
func abs64(n int64) int64 { if n < 0 { return -n }; return n }Política de retry
Se seu endpoint não retornar 2xx em até 10 segundos, a gente tenta de novo com backoff exponencial:
| Tentativa | Delay |
|---|---|
| 1 | imediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 15 minutos |
| 5 | 1 hora |
| 6 | 6 horas |
| 7 | 24 horas |
Total: 7 tentativas ao longo de ~31h30. Depois disso, desistimos e o webhook vai pra exhausted. Você pode replay manualmente pelo painel.
Boas práticas
Responda 200 rápido. Idealmente em menos de 1s. Processamento pesado deve ir pra uma fila interna sua.
Seja idempotente. A gente pode mandar o mesmo evento 2x. Use event_id pra dedupe.
Valide a assinatura SEMPRE. Não pula em hipótese alguma.
Use HTTPS. Em produção, URLs HTTP são rejeitadas.
Loga tudo. Guarde os payloads recebidos por pelo menos 90 dias.
Histórico no painel
Vá em app.moneyflybr.com/integrations/webhook-deliveries pra ver as últimas 100 entregas com request, response, latência e resultado. Suporta replay manual em falhas.