Throne Gateway
Postman OpenAPI Gerar API key Voltar pro gateway

Webhooks Throne

Receba notificações em tempo real, 13 eventos, signature HMAC-SHA256, idempotência por event_id, retry exponential com DLQ.

Setup, 3 passos

Configure URL + secret no painel /painel/integracao/webhooks, Throne começa a enviar imediatamente.

1. Adicionar webhook

POST /painel/integracao/webhooks
{
  "url": "https://meusite.com/throne/webhook",
  "events": ["transaction.paid", "withdrawal.paid", "chargeback.created"],
  "active": true
}

2. Copie secret, NUNCA exponha no frontend

THRONE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

3. Receba + verifique signature em < 30s, responda 200

Throne aguarda 200 OK em < 30s, se não, retry exponential (60s, 5min, 30min, 2h, 12h, 24h), após 7 tentativas vai pra DLQ.

Eventos, 13 disponíveis

transaction.paid
Pagamento PIX confirmado pela adquirente, TX status=paid, saldo creditado no seller.
transaction.pending
Tx criada, aguardando pagamento PIX, expires_at em 1h default.
transaction.expired
PIX não pago dentro do prazo, tx auto-cancelled.
transaction.refunded
Estorno aprovado, partial ou total, saldo seller debitado.
withdrawal.paid
Saque PIX liquidado, enviado pra conta seller, acquirer_withdrawal_id no payload.
withdrawal.failed
Saque falhou, failure_reason no payload, saldo retornado pra seller.
withdrawal.pending
Saque solicitado, processando na adquirente.
chargeback.created
Chargeback aberto, dispute_deadline em 7 dias normalmente, prepare defesa.
chargeback.disputed
Defesa enviada, aguardando decisão.
chargeback.won
Defesa aceita, saldo retornou pra seller.
chargeback.lost
Defesa rejeitada, saldo permanente debitado.
seller.approved
Seller passou KYC, pode operar API, webhook útil pra dashboard interno.
seller.suspended
Seller suspended, operações bloqueadas temporariamente.
refund.created
Estorno PIX criado, aguardando processamento adquirente.

Payload, estrutura padrão

{
  "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" }
  }
}
⚠ Idempotência obrigatória

Use event_id pra deduplicate, Throne pode reenviar mesmo event se sua app retornar timeout/5xx. Salvar event_id em DB e ignorar duplicates.

Signature, HMAC-SHA256

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");
    }
}

Retry, exponential backoff

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)
💡 Performance crítico

Sua app DEVE retornar 200 em < 30 segundos. Processamento pesado, enfileire em queue (Sidekiq/Celery/SQS). Acknowledge fast, process async.

IP origin, whitelist

Throne envia webhooks dos seguintes IPs, adicione no firewall:

PRODUCTION:
 , 45.188.121.85
 , 45.188.121.86

SANDBOX:
 , 192.168.99.10 (interno)

Testando webhooks

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