Voltar pro gateway Throne Gateway

Webhooks Throne

Receba notificações em tempo real · 15+ 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 · 15+ disponíveis

transaction.paid
Pagamento PIX confirmado pela adquirente · TX status=paid · saldo creditado seller (exceto cautelar).
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.
transaction.held
TX marcada bloqueio cautelar · seller vê pending até liberação (manual ou automática).
transaction.released
Bloqueio cautelar liberado · saldo agora disponível pra saque.
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