Enviamos um POST JSON para a URL cadastrada em Webhooks no painel (uma de produção e, opcionalmente, outra de sandbox). Sem URL de sandbox, os eventos de teste vão para a de produção com "livemode": false.
Formato
{
"id": "evt_3f0c9a...",
"object": "event",
"type": "payment.paid",
"api_version": "2026-09-28",
"created": 1759413600,
"livemode": true,
"data": {
"object": {
"object": "payment",
"id": 1842,
"order_id": "pedido-2001",
"status": "paid",
"method": "europix",
"amount": "49.90",
"fee_amount": "1.50",
"net_amount": "48.40",
"currency": "EUR",
"description": "Pedido 2001",
"customer": {
"name": "Ana Souza",
"email": "ana@exemplo.com"
},
"refunded": false,
"refund_amount": null,
"paid_at": "2026-10-02T14:03:11+00:00",
"expired_at": null,
"refunded_at": null,
"created_at": "2026-10-02T14:01:52+00:00",
"metadata": {},
"livemode": false,
"public_token": "pt_8bZ2mQ..."
}
}
}
| Header | Conteúdo |
|---|---|
X-Uneway-Event | Tipo do evento |
X-Uneway-Event-Id | Id do evento (use para idempotência) |
X-Uneway-Delivery-Id | Id da entrega (cada tentativa/reenvio) |
X-Uneway-Timestamp | Unix timestamp da assinatura |
X-Uneway-Signature | t=<unix>,v1=<hex> |
X-Uneway-Livemode | true ou false |
Os mesmos headers também são enviados com o prefixo X-Uniway-* por compatibilidade.
Verificar a assinatura (HMAC-SHA256)
A assinatura é HMAC-SHA256(segredo, "<t>.<corpo cru>") em hexadecimal. O segredo (whsec_...) fica em Webhooks no painel. Recuse timestamps com mais de 5 minutos de diferença.
Node
import crypto from 'node:crypto';
// Use o corpo CRU (Buffer/string), antes de qualquer JSON.parse
export function verifyUneway(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const got = Buffer.from(parts.v1 || '', 'hex');
const exp = Buffer.from(expected, 'hex');
return got.length === exp.length && crypto.timingSafeEqual(got, exp);
}
// Express
app.post('/webhooks/uneway', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyUneway(req.body.toString('utf8'), req.get('X-Uneway-Signature'), process.env.UNEWAY_WEBHOOK_SECRET)) {
return res.status(400).send('assinatura inválida');
}
const event = JSON.parse(req.body);
// trate event.type de forma idempotente (event.id)
res.sendStatus(200);
});PHP
<?php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_UNEWAY_SIGNATURE'] ?? '';
$secret = getenv('UNEWAY_WEBHOOK_SECRET');
$parts = [];
foreach (explode(',', $header) as $piece) {
[$k, $v] = array_pad(explode('=', trim($piece), 2), 2, '');
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);
if (!$t || abs(time() - $t) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit('assinatura inválida');
}
$event = json_decode($raw, true);
// trate $event['type'] de forma idempotente ($event['id'])
http_response_code(200);curl / openssl
# Recalcule a assinatura de um corpo salvo (body.json) para depurar T=1759413600 SECRET=whsec_... printf '%s.%s' "$T" "$(cat body.json)" | openssl dgst -sha256 -hmac "$SECRET" # compare com o v1= do header X-Uneway-Signature: t=$T,v1=<hex>
Entrega, retentativas e reenvio
- Responda 2xx em até 10 segundos. Processe em segundo plano se precisar.
- Falhas são reenviadas após 1 min, 5 min, 30 min, 2 h e 12 h (até 6 tentativas).
- O mesmo evento pode chegar mais de uma vez: trate pelo
iddo evento. - No painel você vê cada entrega (status HTTP, tempo, corpo da resposta) e pode reenviar qualquer uma.
- Enviar evento de teste manda um
webhook.testassinado para conferir a URL e a verificação.
Catálogo de eventos
| Evento | Quando |
|---|---|
payment.created | Cobrança criada (aguardando pagamento). |
payment.paid | Pagamento confirmado pelo provedor. Libere o pedido aqui. |
payment.failed | Pagamento recusado ou com erro. |
payment.expired | Prazo de pagamento esgotado. |
payment.cancelled | Cobrança cancelada. |
payment.refunded | Pagamento estornado (total ou o último estorno parcial). |
refund.created | Um estorno (total ou parcial) foi registrado; um evento por estorno. |
withdrawal.created | Repasse solicitado. |
withdrawal.approved | Repasse aprovado. |
withdrawal.processing | Repasse enviado ao banco. |
withdrawal.cancelled | Repasse cancelado. |
withdrawal.completed | Repasse concluído (dinheiro na conta). |
withdrawal.failed | Repasse falhou; o valor volta ao saldo. |
withdrawal.rejected | Repasse recusado na análise. |
dispute.created | Disputa aberta (chargeback, MED do Pix...). O valor fica retido. |
dispute.updated | Disputa atualizada (prazo, evidências, status). |
dispute.closed | Disputa encerrada (ganha ou perdida). |
subscription.created | Assinatura criada. |
subscription.renewed | Ciclo renovado: a cobrança do novo período foi paga. |
subscription.past_due | Cobrança do ciclo não foi paga no prazo (inadimplente). |
subscription.canceled | Assinatura cancelada. |
subscription.paused | Assinatura pausada. |
subscription.resumed | Assinatura retomada. |
webhook.test | Evento de teste enviado pelo painel (Webhooks → Enviar evento de teste). |
GET /developer/events
Catálogo de eventos
Lista os tipos de evento que podem chegar no seu webhook.
curl -X GET https://api.uneway.pro/api/v1/developer/events \ -H "Authorization: Bearer uw_test_SUA_CHAVE"
Resposta (exemplo)
{
"success": true,
"data": [
{
"type": "payment.paid"
},
{
"type": "payment.refunded"
}
]
}