CDX Pay para desenvolvedores

Webhook

A cada mudança de status de um pagamento, a CDX Pay envia o evento payment.status_changed ao endpoint que você cadastrar. O webhook é um aviso: a fonte da verdade continua sendo GET /v1/payments/{id}.

Cadastrar o endpoint: PUT /v1/webhook-endpoint

Permissão webhooks:write. A URL precisa ser https. Há um endpoint por estabelecimento.

curl -s -X PUT "$WP_API/v1/webhook-endpoint" \
  -H "Authorization: Bearer $WP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{ "url": "https://loja.example.com/hooks/pagamentos" }'
{ "url": "https://loja.example.com/hooks/pagamentos", "secret": "whsec_<segredo>",
  "signature_header": "X-WebPayments-Signature", "updated_at": "2026-10-03T12:00:00Z" }

O evento payment.status_changed

Cada evento chega como POST com corpo JSON e os cabeçalhos X-WebPayments-Event (o tipo do evento) e X-WebPayments-Signature (a assinatura).

{
  "event_id": "<uuid>",
  "event_type": "payment.status_changed",
  "payment_id": "<uuid>",
  "external_reference": "pedido-1234",
  "status": "captured",
  "amount_cents": 15000,
  "captured_cents": 15000,
  "refunded_cents": 0,
  "occurred_at": "2026-10-03T12:00:00Z"
}
Campo do evento O que é
event_id id único do evento; use-o para descartar repetições
event_type sempre payment.status_changed
payment_id o id do pagamento
external_reference o id do pedido que você enviou
status o novo status (Status do pagamento)
amount_cents, captured_cents, refunded_cents valores em centavos depois da mudança
occurred_at quando a mudança aconteceu (UTC, ISO 8601)

Verificar a assinatura

O cabeçalho tem a forma X-WebPayments-Signature: t=<unix>,v1=<hex>, em que t é o instante do envio em segundos e v1 = HMAC-SHA256(secret, "<t>.<corpo cru>"), em hexadecimal.

  1. Leia o corpo cru, antes de qualquer conversão de JSON.
  2. Calcule o HMAC-SHA256 de <t>.<corpo cru> com o secret.
  3. Compare com v1 em tempo constante.
  4. Recuse eventos com t a mais de 5 minutos do seu relógio.

PHP:

$t = null;
$v1 = null;
foreach (explode(',', $request->header('X-WebPayments-Signature', '')) as $parte) {
    [$k, $v] = array_pad(explode('=', $parte, 2), 2, null);
    if ($k === 't') { $t = $v; }
    if ($k === 'v1') { $v1 = $v; }
}
$esperado = hash_hmac('sha256', $t.'.'.$request->getContent(), $segredo);
$valido = $t !== null && $v1 !== null
    && hash_equals($esperado, $v1)
    && abs(time() - (int) $t) <= 300;

Node.js:

import { createHmac, timingSafeEqual } from 'node:crypto'

function assinaturaValida(cabecalho, corpoCru, segredo) {
  const partes = Object.fromEntries(cabecalho.split(',').map((p) => p.split('=', 2)))
  if (!partes.t || !partes.v1) return false
  if (Math.abs(Date.now() / 1000 - Number(partes.t)) > 300) return false
  const esperado = createHmac('sha256', segredo).update(`${partes.t}.${corpoCru}`).digest('hex')
  return esperado.length === partes.v1.length
    && timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1))
}

Responder e reentrega