Cadastrar o endereço#
No painel, em Webhooks, adicione a URL do seu servidor. Você pode ter até 20 endereços, cada um com:
- a própria URL, sempre em HTTPS;
- a lista de eventos que quer receber (vazia = todos);
- o próprio secret de assinatura, que você copia no painel e guarda no servidor.
O botão de teste envia um evento test.ping na hora, assinado como os outros:
{ "event_id": "...", "type": "test.ping", "version": "2026-05-01", "created_at": "2026-10-03T14:00:00Z",
"data": { "message": "Webhook de teste do Throne, seu endpoint está funcionando!", "sent_at": "2026-10-03T14:00:00Z" } }O histórico de entregas fica em Entregas de webhook.
Eventos#
| Evento | Quando |
|---|---|
transaction.paid |
Pagamento confirmado: Pix, cartão (Sterling, Imperium, NovaCorp, Eminence, Apex, ShadowBank, Vortex) e Internacional (Paycore) |
transaction.in_review |
Só na Vortex. O banco autorizou e a adquirente está analisando. O cartão já foi debitado, e a venda continua pending |
withdrawal.completed |
Saque pago |
withdrawal.failed |
Saque não pago (o valor volta para o saldo) |
test.ping |
Botão de teste do painel |
Só esses eventos existem. Recusa de cartão, expiração, estorno e contestação não geram webhook: acompanhe pelo GET /transactions/{id}.
O transaction.in_review vem desmarcado: marque-o no endpoint em
Webhooks para começar a
receber. Marcar ou desmarcar não muda nada nos outros eventos.
Recorrência não muda nada aqui: cada cobrança de ciclo chega como um transaction.paid normal, com o mesmo formato e com transaction_id próprio, como qualquer venda de cartão. Para ligar a cobrança à assinatura, consulte a recorrência, como em Recorrência.
transaction.in_review#
Só na Vortex. Nessa adquirente o banco autoriza na hora e a análise de
risco da adquirente libera depois. Nesse intervalo o cartão do comprador já
foi debitado e a venda continua pending para você.
O corpo tem os mesmos campos do transaction.paid, com os valores que uma
venda pendente tem: amount_cents, fee_cents e net_amount_cents só
nascem na liquidação, então chegam em zero, e status vem pending. Não
existe status novo: quem diz "em análise" é o nome do evento.
{
"type": "transaction.in_review",
"data": {
"transaction_id": "9f2c1d7e-4a8b-4c3d-8e9f-1a2b3c4d5e6f",
"status": "pending",
"payment_method": "credit_card",
"amount_cents": 0,
"fee_cents": 0,
"net_amount_cents": 0,
"card_brand": "visa",
"card_last4": "4242",
"currency": "BRL",
"native_amount_cents": 49900,
"customer": { "name": "Ana Lima", "email": "ana@exemplo.com" },
"metadata": { "order_id": "ORD-2026-00001" },
"created_at": "2026-10-29T18:04:11Z"
}
}Não libere acesso neste evento. Ele diz que o cartão foi debitado, não que
a venda foi aprovada: a análise pode recusar, e aí a venda vira failed e o
valor volta ao comprador. Quem libera é o transaction.paid.
Cada venda avisa uma vez. Reenvio da adquirente e releitura do seu lado não repetem o aviso.
Depois dele, três caminhos: a análise aprova e chega o transaction.paid; a
análise recusa e a venda vira failed (sem webhook, como as outras recusas);
ou a análise demora, e a venda segue pending até resolver.
Quando o webhook chega#
O transaction.paid sai quando o pagamento é confirmado. O tempo até ele chegar depende do meio de pagamento:
| Meio | Quando chega |
|---|---|
| Pix | Logo depois da confirmação do pagamento |
| Cartão na Sterling e na Imperium | Logo depois da aprovação, na maior parte das vendas. Uma venda em análise antifraude só gera o webhook quando a análise aprova |
| Cartão na NovaCorp | Logo depois da aprovação quando o comprador volta da tela do banco. Se ele sair antes, a confirmação vem pela conferência automática, alguns minutos depois |
| Cartão na Eminence | Logo depois da aprovação. Quando o banco pede verificação, depois que o comprador a conclui. Uma venda em análise antifraude só gera o webhook quando a análise aprova |
| Cartão na Apex | Logo depois da aprovação. Uma venda em análise antifraude só gera o webhook quando a análise aprova |
| Cartão na ShadowBank | Logo depois da aprovação. Uma venda em análise antifraude só gera o webhook quando a análise aprova |
| Cartão na Vortex | Logo depois da aprovação. Uma venda em análise antifraude só gera o webhook quando a análise aprova |
| Internacional (Paycore) | Logo depois da aprovação |
Não cancele um pedido por falta de webhook antes da validade da cobrança (24 horas no cartão). Na dúvida, consulte GET /transactions/{id}.
Conversão de campanha pelo servidor#
Se você mede vendas de anúncio (pixel de compra), envie a conversão pelo seu servidor, ao receber transaction.paid, pela API de conversões da plataforma de anúncios. Não dependa só do evento throne-elements:paid da página:
- na NovaCorp, quando o comprador vem de um navegador de app (Instagram, Facebook, Google, TikTok), a aba sai da sua página para pagar e nenhum evento de tela chega;
- o comprador pode fechar a página antes do evento;
- bloqueadores de anúncio derrubam pixels no navegador.
Para não contar a mesma compra duas vezes quando o pixel da página também disparar, use o transaction_id como identificador do evento nos dois lados. Assim a plataforma de anúncios junta os dois envios num só.
Formato#
Todo evento chega como POST com corpo JSON:
{
"event_id": "8c1f3c5e-2c1a-4d1e-9a2b-1f0c9d7e6b5a",
"type": "transaction.paid",
"version": "2026-05-01",
"created_at": "2026-10-03T14:21:07Z",
"data": { }
}transaction.paid#
Pix:
"data": {
"transaction_id": "72394240-db89-4ec5-b370-4a3e7b998409",
"status": "paid",
"payment_method": "pix",
"amount_cents": 4990,
"fee_cents": 250,
"net_amount_cents": 4740,
"paid_at": "2026-10-03T14:21:05Z",
"customer": { "name": "Maria Souza", "document": "12345678909", "email": "maria@exemplo.com" },
"shipping_address": null,
"items": null,
"metadata": { "loja_pedido_id": "PED-1042" }
}Cartão e Internacional trazem também card_brand, card_last4, currency, native_amount_cents e fx_rate:
"data": {
"transaction_id": "ae3bf1da-0c9b-4818-9140-c6432152c23e",
"status": "paid",
"payment_method": "credit_card",
"amount_cents": 31237,
"fee_cents": 4148,
"net_amount_cents": 27089,
"paid_at": "2026-10-03T14:30:58Z",
"card_brand": "visa",
"card_last4": "4242",
"currency": "CHF",
"native_amount_cents": 4990,
"fx_rate": "6.26000000",
"customer": { "name": "Luca Meier", "email": "luca@example.ch" },
"shipping_address": null,
"items": null,
"metadata": {}
}fx_rate chega como texto com 8 casas. Trate os campos que não usa (ou que vierem null) como opcionais: qualquer transaction.paid pode trazer o conjunto completo.
withdrawal.completed e withdrawal.failed#
"data": { "withdrawal_id": "0b8f3a52-6c1d-4e2f-9a7b-3c4d5e6f7a8b", "amount_cents": 50000, "completed_at": "2026-10-03T14:45:12Z" }"data": { "withdrawal_id": "0b8f3a52-6c1d-4e2f-9a7b-3c4d5e6f7a8b", "amount_cents": 50000, "reason": "Chave Pix inexistente" }O reason de withdrawal.failed pode vir técnico. Para mostrar ao seu cliente, use o failure_reason do GET /withdrawals/{id}, que já vem em linguagem amigável.
Validar a assinatura#
Cada entrega traz o header X-Throne-Signature: o HMAC-SHA256, em hexadecimal, do corpo cru da requisição, com o secret do endereço. Calcule sobre os bytes exatamente como chegaram, antes de interpretar o JSON (reformatar o JSON muda a assinatura).
$body = file_get_contents('php://input');
$recebida = $_SERVER['HTTP_X_THRONE_SIGNATURE'] ?? '';
$esperada = hash_hmac('sha256', $body, getenv('THRONE_WEBHOOK_SECRET'));
if (! hash_equals($esperada, $recebida)) {
http_response_code(401);
exit;
}
$evento = json_decode($body, true);
http_response_code(200); // responda rápido; processe em segundo planoimport crypto from 'node:crypto';
import express from 'express';
const app = express();
// corpo cru: não use express.json() nesta rota
app.post('/throne/webhook', express.raw({ type: '*/*' }), (req, res) => {
const recebida = req.get('X-Throne-Signature') || '';
const esperada = crypto
.createHmac('sha256', process.env.THRONE_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const ok = recebida.length === esperada.length
&& crypto.timingSafeEqual(Buffer.from(recebida), Buffer.from(esperada));
if (!ok) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // responda rápido; processe em segundo plano
});import hashlib, hmac, json, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/throne/webhook")
def throne_webhook():
body = request.get_data() # bytes crus
esperada = hmac.new(os.environ["THRONE_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(esperada, request.headers.get("X-Throne-Signature", "")):
abort(401)
evento = json.loads(body)
return "", 200 # responda rápido; processe em segundo planofunc throneWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("THRONE_WEBHOOK_SECRET")))
mac.Write(body)
esperada := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(esperada), []byte(r.Header.Get("X-Throne-Signature"))) {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK) // responda rápido; processe em segundo plano
}Entrega e retentativas#
- Responda 2xx em até 3 segundos. Grave o evento e processe depois; processamento pesado dentro da requisição faz a entrega falhar por tempo.
- Sem 2xx, o Throne tenta de novo: até 10 tentativas no total (a primeira e mais 9), com intervalos crescentes, de segundos até algumas horas.
- Responda 2xx também aos tipos que você não trata (como
test.ping), para não gerar retentativas. - Use a URL final. A entrega não segue redirecionamento como POST: uma URL que redireciona (por exemplo, de
httpparahttps, ou com e semwww) faz a entrega falhar. - Endereços internos (como
localhostou192.168.x.x) não recebem entregas.
Não processe duas vezes#
O mesmo pagamento pode chegar mais de uma vez (retentativas, ou mais de um endereço cadastrado). O event_id é diferente para cada endereço, então deduplique pelo conteúdo:
transaction.paid: pela duplatype+data.transaction_id;- saques: por
type+data.withdrawal_id.
Se o evento já foi processado, responda 200 de novo sem refazer nada.
Quando não chega webhook#
Recusa e expiração de cartão, expiração de Pix, estornos e contestações não geram evento, e alguns saques terminam sem webhook. Para esses casos, consulte:
GET /transactions/{id}para cobranças e checkouts;GET /withdrawals/{id}para saques.