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
-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.
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 })
})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}, 200Use 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:
if (await eventAlreadyProcessed(event.id)) {
return res.status(200).json({ received: true })
}
await processEvent(event)
await markEventProcessed(event.id)Listar e remover webhooks
GET /v1/webhooks
# Remover um webhook
DELETE /v1/webhooks/{id}