Webhooks
O SharkPay avisa o seu servidor quando uma cobrança ou saque muda de estado. Cada evento é um POST JSON assinado com HMAC-SHA256; verifique a assinatura antes de confiar no conteúdo.
Configurar
- Tenha um endereço https público (porta 443 ou 8443, sem usuário/senha na URL). URLs que resolvem para IP privado/reservado são recusadas.
- Chame PUT /api/webhook. Na primeira vez a resposta traz o segredo whsec_… — mostrado uma única vez.
- Guarde o segredo em variável de ambiente/cofre. Perdeu? POST /api/webhook/secret/rotate gera outro e invalida o anterior na hora.
- Teste com POST /api/webhook/test (envia um evento ping).
#!/usr/bin/env bash
# Configurar a URL do webhook
set -euo pipefail
: "${SHARKPAY_BASE_URL:?defina a URL base, ex.: https://pay.exemplo.com}"
: "${SHARKPAY_TOKEN:?obtenha o token com o exemplo "auth"}"
curl -sS --fail-with-body -X PUT "$SHARKPAY_BASE_URL/api/webhook" \
-H "Authorization: Bearer $SHARKPAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "'"$WEBHOOK_URL"'"
}'
echo// Configurar a URL do webhook — Node.js 18+ (fetch nativo, sem dependências)
import { randomUUID } from 'node:crypto';
const BASE = process.env.SHARKPAY_BASE_URL;
const TOKEN = process.env.SHARKPAY_TOKEN;
const body = {
url: process.env.WEBHOOK_URL,
};
const headers = { Authorization: `Bearer ${TOKEN}` };
headers['Content-Type'] = 'application/json';
const res = await fetch(`${BASE}/api/webhook`, {
method: 'PUT',
headers,
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000), // sempre defina um timeout
});
const data = await res.json().catch(() => ({}));
console.log(res.status, JSON.stringify(data, null, 2));
if (!res.ok) process.exit(1);# Configurar a URL do webhook — Python 3.9+ (pip install requests)
import os, sys, uuid
import requests
BASE = os.environ["SHARKPAY_BASE_URL"]
TOKEN = os.environ["SHARKPAY_TOKEN"]
body = {
"url": os.environ["WEBHOOK_URL"],
}
headers = {"Authorization": f"Bearer {TOKEN}"}
res = requests.put(BASE + "/api/webhook", headers=headers, json=body, timeout=30)
try:
data = res.json()
except ValueError:
data = {}
print(res.status_code, data)
sys.exit(0 if res.ok else 1)<?php
// Configurar a URL do webhook — PHP 8+ com ext-curl
$base = getenv("SHARKPAY_BASE_URL");
$token = getenv("SHARKPAY_TOKEN");
$body = [
'url' => getenv("WEBHOOK_URL"),
];
$ch = curl_init($base . '/api/webhook');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30, // sempre defina um timeout
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, " ", $response, PHP_EOL;
exit($status >= 200 && $status < 300 ? 0 : 1);#!/usr/bin/env bash
# Enviar evento de teste
set -euo pipefail
: "${SHARKPAY_BASE_URL:?defina a URL base, ex.: https://pay.exemplo.com}"
: "${SHARKPAY_TOKEN:?obtenha o token com o exemplo "auth"}"
curl -sS --fail-with-body -X POST "$SHARKPAY_BASE_URL/api/webhook/test" \
-H "Authorization: Bearer $SHARKPAY_TOKEN"
echo// Enviar evento de teste — Node.js 18+ (fetch nativo, sem dependências)
import { randomUUID } from 'node:crypto';
const BASE = process.env.SHARKPAY_BASE_URL;
const TOKEN = process.env.SHARKPAY_TOKEN;
const headers = { Authorization: `Bearer ${TOKEN}` };
const res = await fetch(`${BASE}/api/webhook/test`, {
method: 'POST',
headers,
signal: AbortSignal.timeout(30_000), // sempre defina um timeout
});
const data = await res.json().catch(() => ({}));
console.log(res.status, JSON.stringify(data, null, 2));
if (!res.ok) process.exit(1);# Enviar evento de teste — Python 3.9+ (pip install requests)
import os, sys, uuid
import requests
BASE = os.environ["SHARKPAY_BASE_URL"]
TOKEN = os.environ["SHARKPAY_TOKEN"]
headers = {"Authorization": f"Bearer {TOKEN}"}
res = requests.post(BASE + "/api/webhook/test", headers=headers, timeout=30)
try:
data = res.json()
except ValueError:
data = {}
print(res.status_code, data)
sys.exit(0 if res.ok else 1)<?php
// Enviar evento de teste — PHP 8+ com ext-curl
$base = getenv("SHARKPAY_BASE_URL");
$token = getenv("SHARKPAY_TOKEN");
$ch = curl_init($base . '/api/webhook/test');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30, // sempre defina um timeout
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, " ", $response, PHP_EOL;
exit($status >= 200 && $status < 300 ? 0 : 1);Eventos
| type | Quando | data |
|---|---|---|
| invoice.paid | Cobrança passou a PAID. | invoice: reference, invoice_no, total, status |
| invoice.canceled | Cobrança passou a CANCELED. | invoice: … |
| invoice.refunded | Cobrança passou a REFUNDED. | invoice: … |
| order.paid | Venda de produto confirmada (pagamento aprovado). | order: reference, status, total, quantity, product{reference,name}, customer{name,email}, paid_at |
| order.canceled | Venda de produto cancelada. | order: … |
| order.refunded | Venda reembolsada/estornada. | order: … |
| pixout.paid | Saque Pix concluído. | withdraw: reference, amount, status |
| pixout.canceled | Saque Pix cancelado (valor devolvido ao saldo). | withdraw: … |
| ping | Teste manual. | message |
{
"id": "evt_abcdefghijklmnopqrstuvwx",
"type": "invoice.paid",
"created": 1760000000,
"api_version": "2026-10-14",
"data": {
"invoice": { "reference": "INV-A1B2C3", "invoice_no": "PED-1042", "total": "190.00", "status": "PAID" }
}
}Cabeçalhos enviados
| Cabeçalho | Conteúdo |
|---|---|
| SharkPay-Signature | t=<unix>,v1=<hex> — HMAC-SHA256 de "t.corpo" com o segredo whsec_… |
| SharkPay-Event-Id | Id do evento (igual ao campo id); use para deduplicar. |
| Content-Type | application/json |
| User-Agent | SharkPay-Webhooks/1.0 |
Verificar a assinatura
- Leia o corpo bruto (bytes) — não o JSON já reserializado.
- Extraia t e v1 do cabeçalho.
- Calcule HMAC_SHA256(segredo, t + "." + corpo) em hexadecimal.
- Compare em tempo constante com v1.
- Rejeite se |agora − t| > 300 s (anti-replay).
#!/usr/bin/env bash
# Simula localmente uma entrega assinada para testar o SEU receptor (nenhuma chamada ao SharkPay).
set -euo pipefail
: "${SHARKPAY_WEBHOOK_SECRET:?}" "${RECEIVER_URL:?ex.: http://localhost:3000}"
BODY='{"id":"evt_local_1","type":"ping","created":'$(date +%s)',"api_version":"2026-10-14","data":{"message":"hello"}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SHARKPAY_WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -sS -X POST "$RECEIVER_URL" -H "Content-Type: application/json" \
-H "SharkPay-Signature: t=$T,v1=$SIG" -H "SharkPay-Event-Id: evt_local_1" -d "$BODY"
echo// Receptor de webhook SharkPay — Node.js 18+ (sem dependências)
import http from 'node:http';
import crypto from 'node:crypto';
const SECRET = process.env.SHARKPAY_WEBHOOK_SECRET; // whsec_... (mostrado uma única vez)
const TOLERANCE_S = 300; // rejeita eventos com mais de 5 min de diferença
export function verify(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(String(header ?? '').split(',').map((kv) => kv.trim().split('=')));
if (!/^\d+$/.test(parts.t ?? '') || !parts.v1) return false;
if (Math.abs(now - Number(parts.t)) > TOLERANCE_S) return false;
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b); // comparação em tempo constante
}
if (process.argv.includes('--selftest')) { // vetor de teste público
const sig = `t=1700000000,v1=e089fa65296cc0d43a37342cb8e9cb5f9b62cf38b25577d34d8aaa302b4d5725`;
const body = "{\"id\":\"evt_test\",\"type\":\"ping\",\"created\":1700000000,\"api_version\":\"2026-10-14\",\"data\":{\"message\":\"hello\"}}";
const ok = verify('whsec_test_secret', sig, body, 1700000100) && !verify('whsec_test_secret', sig, body + ' ', 1700000100);
console.log(ok ? 'selftest ok' : 'selftest FALHOU');
process.exit(ok ? 0 : 1);
}
const seen = new Set(); // em produção use banco/Redis (deduplicação por id)
http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8'); // use o corpo BRUTO, antes de qualquer parse
if (!verify(SECRET, req.headers['sharkpay-signature'], raw)) { res.writeHead(400).end('bad signature'); return; }
const event = JSON.parse(raw);
if (!seen.has(event.id)) { // pelo menos uma entrega: ignore duplicatas
seen.add(event.id);
console.log('evento', event.type, event.id); // enfileire o processamento; responda rápido
}
res.writeHead(200).end('ok'); // 2xx em até 10 s
});
}).listen(process.env.PORT ?? 3000);# Receptor de webhook SharkPay — Python 3.9+ (somente biblioteca padrão)
import hashlib, hmac, json, os, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ.get("SHARKPAY_WEBHOOK_SECRET", "") # whsec_... (mostrado uma única vez)
TOLERANCE_S = 300 # rejeita eventos com mais de 5 min de diferença
def verify(secret: str, header: str, raw_body: str, now: int | None = None) -> bool:
parts = dict(kv.strip().split("=", 1) for kv in (header or "").split(",") if "=" in kv)
if not parts.get("t", "").isdigit() or not parts.get("v1"):
return False
if abs((now or int(time.time())) - int(parts["t"])) > TOLERANCE_S:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"]) # tempo constante
if "--selftest" in sys.argv: # vetor de teste público
sig = "t=1700000000,v1=e089fa65296cc0d43a37342cb8e9cb5f9b62cf38b25577d34d8aaa302b4d5725"
body = '{"id":"evt_test","type":"ping","created":1700000000,"api_version":"2026-10-14","data":{"message":"hello"}}'
ok = verify("whsec_test_secret", sig, body, 1700000100) and not verify("whsec_test_secret", sig, body + " ", 1700000100)
print("selftest ok" if ok else "selftest FALHOU")
sys.exit(0 if ok else 1)
seen = set() # em produção use banco/Redis (deduplicação por id)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("Content-Length", 0))).decode() # corpo BRUTO
if not verify(SECRET, self.headers.get("SharkPay-Signature", ""), raw):
self.send_response(400); self.end_headers(); return
event = json.loads(raw)
if event["id"] not in seen: # pelo menos uma entrega: ignore duplicatas
seen.add(event["id"])
print("evento", event["type"], event["id"]) # enfileire o processamento; responda rápido
self.send_response(200); self.end_headers(); self.wfile.write(b"ok") # 2xx em até 10 s
HTTPServer(("", int(os.environ.get("PORT", 3000))), Handler).serve_forever()<?php
// Receptor de webhook SharkPay — PHP 8+ (sem dependências). Publique este arquivo atrás de HTTPS.
const TOLERANCE_S = 300; // rejeita eventos com mais de 5 min de diferença
function sharkpay_verify(string $secret, string $header, string $rawBody, ?int $now = null): bool
{
$parts = [];
foreach (explode(',', $header) as $kv) {
$p = explode('=', trim($kv), 2);
if (count($p) === 2) { $parts[$p[0]] = $p[1]; }
}
if (empty($parts['t']) || !ctype_digit($parts['t']) || empty($parts['v1'])) { return false; }
if (abs(($now ?? time()) - (int) $parts['t']) > TOLERANCE_S) { return false; }
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']); // tempo constante
}
if (PHP_SAPI === 'cli' && in_array('--selftest', $argv ?? [], true)) { // vetor de teste público
$sig = 't=1700000000,v1=e089fa65296cc0d43a37342cb8e9cb5f9b62cf38b25577d34d8aaa302b4d5725';
$body = '{"id":"evt_test","type":"ping","created":1700000000,"api_version":"2026-10-14","data":{"message":"hello"}}';
$ok = sharkpay_verify('whsec_test_secret', $sig, $body, 1700000100) && !sharkpay_verify('whsec_test_secret', $sig, $body . ' ', 1700000100);
echo $ok ? "selftest ok\n" : "selftest FALHOU\n";
exit($ok ? 0 : 1);
}
$raw = file_get_contents('php://input'); // corpo BRUTO, antes de qualquer parse
if (!sharkpay_verify(getenv('SHARKPAY_WEBHOOK_SECRET') ?: '', $_SERVER['HTTP_SHARKPAY_SIGNATURE'] ?? '', $raw)) {
http_response_code(400); exit('bad signature');
}
$event = json_decode($raw, true);
// Deduplique por $event['id'] (banco/Redis): a entrega é "pelo menos uma vez".
// Enfileire o processamento e responda rápido.
http_response_code(200); echo 'ok'; // 2xx em até 10 sEntrega e retentativas
- Sucesso = resposta 2xx em até 10 s (conexão em 5 s). Redirecionamentos não são seguidos.
- Qualquer outra coisa (4xx, 5xx, timeout) agenda nova tentativa, até 8 tentativas, com intervalos de 1, 5, 15, 60, 180, 360 e 720 minutos.
- A entrega é pelo menos uma vez: o mesmo evento pode chegar repetido. Deduplique por id.
- A ordem não é garantida. Em caso de dúvida, consulte a cobrança antes de agir.
- Os eventos são gravados na mesma transação que a mudança de status (padrão outbox): se o status mudou, o evento existe.
Para diagnosticar, use GET /api/webhook/deliveries (últimas 25 entregas, com status HTTP, tentativas e próxima tentativa).
#!/usr/bin/env bash
# Ver entregas recentes
set -euo pipefail
: "${SHARKPAY_BASE_URL:?defina a URL base, ex.: https://pay.exemplo.com}"
: "${SHARKPAY_TOKEN:?obtenha o token com o exemplo "auth"}"
curl -sS --fail-with-body -X GET "$SHARKPAY_BASE_URL/api/webhook/deliveries" \
-H "Authorization: Bearer $SHARKPAY_TOKEN"
echo// Ver entregas recentes — Node.js 18+ (fetch nativo, sem dependências)
import { randomUUID } from 'node:crypto';
const BASE = process.env.SHARKPAY_BASE_URL;
const TOKEN = process.env.SHARKPAY_TOKEN;
const headers = { Authorization: `Bearer ${TOKEN}` };
const res = await fetch(`${BASE}/api/webhook/deliveries`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30_000), // sempre defina um timeout
});
const data = await res.json().catch(() => ({}));
console.log(res.status, JSON.stringify(data, null, 2));
if (!res.ok) process.exit(1);# Ver entregas recentes — Python 3.9+ (pip install requests)
import os, sys, uuid
import requests
BASE = os.environ["SHARKPAY_BASE_URL"]
TOKEN = os.environ["SHARKPAY_TOKEN"]
headers = {"Authorization": f"Bearer {TOKEN}"}
res = requests.get(BASE + "/api/webhook/deliveries", headers=headers, timeout=30)
try:
data = res.json()
except ValueError:
data = {}
print(res.status_code, data)
sys.exit(0 if res.ok else 1)<?php
// Ver entregas recentes — PHP 8+ com ext-curl
$base = getenv("SHARKPAY_BASE_URL");
$token = getenv("SHARKPAY_TOKEN");
$ch = curl_init($base . '/api/webhook/deliveries');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30, // sempre defina um timeout
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, " ", $response, PHP_EOL;
exit($status >= 200 && $status < 300 ? 0 : 1);Boas práticas do receptor
- Responda 200 rápido; processe em fila. Não faça trabalho pesado antes de responder.
- Valide a assinatura antes de ler o corpo; responda 400 se inválida.
- Seja idempotente: guarde os id já processados.
- Não confie só no evento para ações críticas: confirme o estado atual pela API.
- Exponha o receptor apenas por HTTPS e registre erros sem gravar o segredo.