Depois da venda
Webhooks
A Yuupe avisa o seu servidor a cada mudança de status, com assinatura HMAC e reenvio por 24 horas.
Cadastro
- No painel da Yuupe, em Yuupe Pay › Integração › Webhooks.
- URL https, em endereço público. Até 5 por ambiente (por exemplo, a loja e o ERP), cada um com o próprio segredo e, se quiser, só os eventos que interessam.
- Sandbox e produção têm endereços e segredos separados.
Eventos
| Evento | Quando |
|---|---|
pedido.aprovado | Pix pago ou cartão aprovado |
pedido.recusado | Cartão ou Pix recusado, ou confirmação do banco não concluída |
pedido.expirado | Pix não pago no prazo, ou 3DS não concluído em 5 minutos |
pedido.estornado | Valor devolvido por completo |
pedido.estorno_parcial | Parte do valor devolvida |
pedido.contestado | Chargeback aberto pelo comprador |
Formato da entrega
POST https://loja.exemplo.com/webhooks/yuupe-pay
Content-Type: application/json
User-Agent: YuupePay-Webhook/1.0
X-Yuupe-Signature: t=1792170000,v1=5f2b…e9
X-Yuupe-Evento: pedido.aprovado
X-Yuupe-Entrega: 8c1d…
{
"id": "0b6f6d1e-6a51-4a5e-9c43-1f1d2a8b7c11",
"tipo": "pedido.aprovado",
"criado_em": "2026-10-16T16:35:04Z",
"ambiente": "producao",
"pedido": {
"id": "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
"numero_pedido": "NIC-2026-000123",
"status": "aprovado",
"metodo": "pix",
"valor_centavos": 15980,
"pago_em": "2026-10-16T16:35:03Z",
"…": "o mesmo formato do GET /pedidos/{id}"
}
}pedido tem o mesmo formato do objeto Pedido, inclusive motivo nos recusados.
Verificar a assinatura
v1 é o HMAC-SHA256, com o segredo daquele endereço, de `${t}.${corpo}`, onde corpo é o corpo cru da requisição. Recuse assinaturas com mais de 5 minutos.
import { createHmac, timingSafeEqual } from 'node:crypto'
// Use o corpo CRU (string/Buffer), antes de qualquer JSON.parse.
export function assinaturaValida(cabecalho, corpoCru, segredo) {
const partes = Object.fromEntries(String(cabecalho).split(',').map((p) => p.split('=')))
const t = Number(partes.t)
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false // mais de 5 min: recusa
const esperado = createHmac('sha256', segredo).update(`${t}.${corpoCru}`).digest('hex')
const a = Buffer.from(esperado), b = Buffer.from(partes.v1 ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}
// Express:
app.post('/webhooks/yuupe-pay', express.raw({ type: 'application/json' }), (req, res) => {
const cru = req.body.toString('utf8')
if (!assinaturaValida(req.get('X-Yuupe-Signature'), cru, process.env.YUUPE_PAY_WEBHOOK_SEGREDO)) {
return res.status(401).end()
}
const evento = JSON.parse(cru)
// descarte repetidos pelo evento.id; depois trate evento.tipo e evento.pedido
res.status(200).end()
})Corpo cru
Calcule a assinatura sobre o corpo exatamente como chegou. Se o framework já converteu para JSON, a assinatura não bate (espaços e ordem mudam).
Reenvios e repetidos
- Responda qualquer
2xxem até 10 segundos. Faça o trabalho pesado depois de responder (fila). - Sem
2xx, reenviamos em 1, 5, 15, 60, 180, 360 e 720 minutos (cerca de 24 horas), de forma independente para cada endereço. - O mesmo evento pode chegar mais de uma vez. Guarde o
iddo evento e descarte repetidos. - Os eventos podem chegar fora de ordem. Se precisar do estado atual, consulte
GET /pedidos/{id}.