Receba notificações em tempo real · 15+ eventos · signature HMAC-SHA256 · idempotência por event_id · retry exponential com DLQ.
Configure URL + secret no painel /painel/integracao/webhooks · Throne começa a enviar imediatamente.
POST /painel/integracao/webhooks
{
"url": "https://meusite.com/throne/webhook",
"events": ["transaction.paid", "withdrawal.paid", "chargeback.created"],
"active": true
}
THRONE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Throne aguarda 200 OK em < 30s · se não · retry exponential (60s · 5min · 30min · 2h · 12h · 24h) · após 7 tentativas vai pra DLQ.
{
"event_id": "evt_a3b6e3f8-2f1d-4f6b-9e7c-9b4f5d1c3a2e",
"type": "transaction.paid",
"created_at": "2026-05-14T22:15:33Z",
"version": "2026-05-14",
"data": {
"id": "72394240-db89-4ec5-b370-4a3e7b998409",
"status": "paid",
"amount_cents": 9900,
"fee_cents": 495,
"net_amount_cents": 9405,
"paid_at": "2026-05-14T22:15:30Z",
"payment_method": "pix",
"customer": { "email": "joao@example.com" },
"metadata": { "sku": "premium-001", "order_id": "order-v6-001" }
}
}
Use event_id pra deduplicate · Throne pode reenviar mesmo event se sua app retornar timeout/5xx. Salvar event_id em DB e ignorar duplicates.
Cada request inclui header X-Throne-Signature com HMAC-SHA256 do body cru usando seu webhook secret.
Route::post('/throne/webhook', function (Request $req) { $payload = $req->getContent(); // body cru, não JSON parsed $signature = $req->header('X-Throne-Signature'); $expected = hash_hmac('sha256', $payload, env('THRONE_WEBHOOK_SECRET')); if (! hash_equals($expected, $signature)) { abort(401, 'Invalid signature'); } $event = json_decode($payload, true); // Idempotência · evita processar mesmo event 2x if (DB::table('webhook_processed')->where('event_id', $event['event_id'])->exists()) { return response('already_processed', 200); } // Process event · async preferido ProcessThroneEvent::dispatch($event); DB::table('webhook_processed')->insert([ 'event_id' => $event['event_id'], 'received_at' => now(), ]); return response('ok', 200); // IMPORTANTE: 200 em < 30s });
const crypto = require('crypto'); app.post('/throne/webhook', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); const signature = req.headers['x-throne-signature']; const expected = crypto.createHmac('sha256', process.env.THRONE_WEBHOOK_SECRET) .update(payload).digest('hex'); if (! crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(payload); // Idempotência via Redis redis.setnx(`webhook:${event.event_id}`, 1).then(set => { if (! set) return res.send('already_processed'); queue.add('throne-event', event); res.send('ok'); }); });
import hmac, hashlib @app.route('/throne/webhook', methods=['POST']) def webhook(): payload = request.get_data() signature = request.headers.get('X-Throne-Signature') expected = hmac.new( THRONE_WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, signature): return 'invalid signature', 401 event = json.loads(payload) # Idempotência via Redis if not redis.setnx(f'webhook:{event["event_id"]}', 1): return 'already_processed', 200 process_throne_event.delay(event) # Celery async return 'ok', 200
require 'openssl' class ThroneWebhooksController < ApplicationController skip_before_action :verify_authenticity_token def receive payload = request.raw_post signature = request.headers['X-Throne-Signature'] expected = OpenSSL::HMAC.hexdigest('SHA256', ENV['THRONE_WEBHOOK_SECRET'], payload) return head(:unauthorized) unless ActiveSupport::SecurityUtils.secure_compare(signature, expected) event = JSON.parse(payload) return head(:ok) if WebhookProcessed.exists?(event_id: event['event_id']) ThroneEventJob.perform_later(event) WebhookProcessed.create!(event_id: event['event_id']) head :ok end end
import ("crypto/hmac"; "crypto/sha256"; "encoding/hex") func webhookHandler(w http.ResponseWriter, r *http.Request) { payload, _ := io.ReadAll(r.Body) signature := r.Header.Get("X-Throne-Signature") mac := hmac.New(sha256.New, []byte(os.Getenv("THRONE_WEBHOOK_SECRET"))) mac.Write(payload) expected := hex.EncodeToString(mac.Sum(nil)) if ! hmac.Equal([]byte(signature), []byte(expected)) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } var event ThroneEvent json.Unmarshal(payload, &event) // Idempotência via Redis SETNX set, _ := redisClient.SetNX(ctx, "webhook:"+event.EventID, 1, 0).Result() if ! set { fmt.Fprint(w, "already_processed"); return } go processEvent(event) fmt.Fprint(w, "ok") }
@RestController public class ThroneWebhookController { @PostMapping("/throne/webhook") public ResponseEntity<String> handle(@RequestBody byte[] payload, @RequestHeader("X-Throne-Signature") String sig) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(System.getenv("THRONE_WEBHOOK_SECRET").getBytes(), "HmacSHA256")); String expected = HexFormat.of().formatHex(mac.doFinal(payload)); if (! MessageDigest.isEqual(sig.getBytes(), expected.getBytes())) { return ResponseEntity.status(401).body("invalid"); } ThroneEvent event = objectMapper.readValue(payload, ThroneEvent.class); eventService.processAsync(event); return ResponseEntity.ok("ok"); } }
Throne tenta entregar até 7 vezes · após falha vai pra Dead Letter Queue · admin pode replay manualmente em /admin/webhooks-dlq.
Tentativa 1 · imediato Tentativa 2 · +1 minuto Tentativa 3 · +5 minutos Tentativa 4 · +30 minutos Tentativa 5 · +2 horas Tentativa 6 · +12 horas Tentativa 7 · +24 horas Após · DLQ (admin replay manual)
Sua app DEVE retornar 200 em < 30 segundos. Processamento pesado · enfileire em queue (Sidekiq/Celery/SQS). Acknowledge fast · process async.
Throne envia webhooks dos seguintes IPs · adicione no firewall:
PRODUCTION: · 45.188.121.85 · 45.188.121.86 SANDBOX: · 192.168.99.10 (interno)
Use o Webhook Debugger no admin pra enviar test events · ou ngrok pra túnel local:
# Instalar ngrok brew install ngrok # Túnel localhost:8000 → URL público ngrok http 8000 # Configure URL public no painel Throne # Cole no admin: https://abc123.ngrok.io/throne/webhook