CyrusDocs

Webhooks

Receba notificações em tempo real sobre eventos de pagamentos e cobranças.

O que são webhooks?

Webhooks são requisições HTTP POST que a Cyrus envia para a URL que você cadastrar quando eventos ocorrem na plataforma — por exemplo, quando uma cobrança é paga ou um pagamento falha.

Isso elimina a necessidade de polling periódico na API.

Registrar um webhook

POST /v1/webhooks
 
curl -X POST https://api.cyrus.com/v1/webhooks
  -H "X-API-Key: $CYRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://seusite.com/webhooks/cyrus",
    "events": ["charge.paid", "payment.sent", "payment.failed"]
  }'

Resposta

{
  "success": true,
  "data": {
    "id": "wh_01hw5n...",
    "url": "https://seusite.com/webhooks/cyrus",
    "events": ["charge.paid", "payment.sent", "payment.failed"],
    "secret": "whsec_4a8f2c1d9e3b7a5f...",
    "createdAt": "2024-01-15T10:00:00.000Z"
  }
}

O campo secret é exibido somente uma vez. Armazene-o em local seguro — você precisará dele para verificar as assinaturas.

Eventos disponíveis

| Evento | Descrição | |---|---| | charge.paid | Cobrança PIX recebida e confirmada | | payment.sent | Pagamento PIX enviado com sucesso | | payment.failed | Pagamento PIX falhou | | refund.created | MED aberto pelo pagador (estorno iniciado) | | refund.completed | MED encerrado — estorno concluído ou disputa resolvida |

Formato do payload

Todos os eventos seguem a mesma estrutura:

{
  "id": "evt_01hw6p...",
  "event": "charge.paid",
  "createdAt": "2024-01-15T12:04:22.000Z",
  "data": {
    "id": "txn_01hw3k9xyz",
    "txid": "a1b2c3d4e5f60718...",
    "amount": 49.90,
    "externalId": "sub_jan2024_user123",
    "paidAt": "2024-01-15T12:04:22.000Z"
  }
}

Verificar assinatura

Cada requisição de webhook inclui o header X-Cyrus-Signature com uma assinatura HMAC-SHA256 do payload.

Sempre verifique a assinatura antes de processar o evento.

Node.js
import crypto from 'crypto'
import express from 'express'
 
const router = express.Router()
 
router.post('/webhooks/cyrus', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-cyrus-signature'] as string
  const secret = process.env.CYRUS_WEBHOOK_SECRET!
 
  const expected = `sha256=${crypto
    .createHmac('sha256', secret)
    .update(req.body) // raw body, não parsed
    .digest('hex')}`
 
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).json({ error: 'Assinatura inválida' })
  }
 
  const event = JSON.parse(req.body.toString())
 
  switch (event.event) {
    case 'charge.paid':
      await handleChargePaid(event.data)
      break
    case 'payment.failed':
      await handlePaymentFailed(event.data)
      break
  }
 
  res.status(200).json({ received: true })
})
Python
import hmac
import hashlib
from flask import Flask, request, abort
 
app = Flask(__name__)
 
@app.route('/webhooks/cyrus', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Cyrus-Signature', '')
    secret = os.environ['CYRUS_WEBHOOK_SECRET']
    
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        request.get_data(),
        hashlib.sha256
    ).hexdigest()
    
    if not hmac.compare_digest(signature, expected):
        abort(401)
    
    event = request.json
    
    if event['event'] == 'charge.paid':
        handle_charge_paid(event['data'])
    
    return {'received': True}, 200
💡

Use crypto.timingSafeEqual (Node.js) ou hmac.compare_digest (Python) para comparar assinaturas — isso previne ataques de timing.

Resposta esperada

Seu endpoint deve responder com HTTP 200 em até 10 segundos. A Cyrus considera qualquer outra resposta (4xx, 5xx, timeout) como falha e tentará novamente.

Política de retentativa

| Tentativa | Aguarda antes | |---|---| | 1ª (original) | Imediata | | 2ª | 1 segundo | | 3ª | 5 segundos | | 4ª | 30 segundos |

Após 4 tentativas sem sucesso, o evento é marcado como failed no log de webhooks.

Idempotência

O mesmo evento pode ser entregue mais de uma vez (em caso de retentativa após timeout, por exemplo). Use o campo id do evento para deduplicar:

// Armazene IDs processados (Redis, banco, etc.)
if (await eventAlreadyProcessed(event.id)) {
  return res.status(200).json({ received: true })
}
 
await processEvent(event)
await markEventProcessed(event.id)

Listar e remover webhooks

# Listar todos os webhooks cadastrados
GET /v1/webhooks
 
# Remover um webhook
DELETE /v1/webhooks/{id}