Eventos de webhook
Receba, verifique a assinatura HMAC e processe os eventos enviados pela Paggpix.
Após criar um webhook, os eventos do objeto subscrito são enviados para sua URL de callback via POST. Todos os payloads são assinados com HMAC usando o verify_token como chave.
Requisição de evento
POST https://yourdomain.com/webhook
Content-Type: application/json
X-Paggpix-Signature: sha256=<payload-signature>
{
"event": "PAYMENT",
"timestamp": "2023-07-01T18:46:13Z",
"data": {
"pix_id": "ba5ce1d5cd58ff77d99b36cee2224e37",
"external_id": "3215158",
"status": "DONE",
"description": "Pagamento referente ao pedido #99999",
"value": "1000.00",
"paggpix_fee_percentage": "0.50",
"paggpix_fee_value": "5",
"partner_fee_percentage": "0.40",
"partner_fee_value": "4",
"created": "2023-07-01T18:45:06Z",
"paid": "2023-07-01T18:46:13Z"
}
}Verificando a assinatura
A assinatura no header X-Paggpix-Signature é gerada com HMAC-SHA256 usando o verify_token. Recalcule a assinatura e compare-as.
Calcule o HMAC sobre o corpo bruto da requisição, exatamente como ele chegou. Não use o JSON já parseado e re-serializado: a reserialização pode alterar espaçamento, ordem de chaves ou escape de caracteres e invalidar a assinatura.
import crypto from 'crypto';
import express from 'express';
const app = express();
const verifyToken = process.env.PAGGPIX_VERIFY_TOKEN; // Mesmo token usado na criação
// express.raw() entrega req.body como Buffer, preservando os bytes originais.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const received = req.headers['x-paggpix-signature'];
// Sem assinatura não há o que comparar. Recuse antes de tocar no payload.
if (typeof received !== 'string') {
return res.status(401).send('Missing signature');
}
const hmac = crypto.createHmac('sha256', verifyToken);
const expected = `sha256=${hmac.update(req.body).digest('hex')}`;
const receivedBuffer = Buffer.from(received);
const expectedBuffer = Buffer.from(expected);
// timingSafeEqual lança se os buffers tiverem tamanhos diferentes,
// então o tamanho precisa ser checado antes da comparação.
if (
receivedBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(receivedBuffer, expectedBuffer)
) {
return res.status(403).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log('Webhook event received:', event);
res.status(200).send('Event received');
});
app.listen(3000);Se sua aplicação usa express.json() globalmente, registre a rota do webhook com express.raw() antes desse middleware, ou o corpo bruto não estará mais disponível.
Evento PAYMENT
Disparado quando um pagamento é criado, efetuado, expirado ou cancelado.
Dados do evento (data)
Prop
Type
O campo paid é enviado apenas quando o pagamento foi concluído.