neo-pays

Webhooks

A fonte de verdade das suas integrações: cada mudança de estado é-lhe enviada, assinada, com reenvio automático.

Criar um endpoint

POST /v1/webhooks
{
  "url": "https://oseusite.com/webhook/neopays",
  "description": "produção",
  "subscribed_events": ["payment.success", "refund.success"]
}

A resposta traz o segredo de assinatura. subscribed_events vazio = todos os eventos; um evento fora do catálogo é recusado em vez de guardado, porque esperaria por uma entrega que nada emite.

Esta chamada garante, nem sempre cria. Um URL já registado volta com 200 em vez de ser duplicado — um script de aprovisionamento executado duas vezes deixa UM endpoint, não dois a receber cada evento em duplicado. O segredo é devolvido em ambos os casos, porque a razão legítima para voltar a chamar é tê-lo perdido.

GET    /v1/webhooks              — os seus endpoints e os seus contadores
GET    /v1/webhooks/{id}         — um endpoint
PATCH  /v1/webhooks/{id}         — modificação parcial
DELETE /v1/webhooks/{id}         — eliminação definitiva
GET    /v1/webhooks/{id}/secret  — reler o segredo
POST   /v1/webhooks/test         — entrega de teste a todos os seus endpoints activos

O PATCH só muda aquilo que envia: omitir is_active não desactiva o endpoint. Para parar as entregas sem perder o endpoint nem o seu segredo, envie is_active: false em vez de eliminar.

Dez endpoints por conta no máximo. O painel faz tudo isto também, e só ele sabe reenviar uma entrega falhada e rodar um segredo.

Os eventos

Evento Quando
payment.initiated Cobrança criada
payment.pending À espera da confirmação do pagador
payment.success Cobrada — o evento que conta
payment.failed Falha
payout.success / payout.failed / payout.cancelled Ciclo de vida de um pagamento
payout.refunded Pagamento devolvido pelo rail à posteriori
refund.success / refund.failed Ciclo de vida de um reembolso

O envelope é sempre o mesmo:

{
  "event": "payment.success",
  "created": "2026-08-24T12:00:00Z",
  "data": {
    "id": "op_01J8Z9K2QW",
    "reference": "NP-...",
    "merchant_reference": "encomenda-1042",
    "amount": "5000",
    "currency": "XOF",
    "status": "success"
  }
}

O corpo do webhook não tem a mesma forma que a resposta da API. Traz amount e currency onde a API traz amount_minor e asset_id, e merchant_reference onde ela traz client_ref. É um contrato distinto, escrito antes do outro e inalterado desde então: não presuma que um objecto de um se descodifica como um objecto do outro.

Verificar a assinatura

Cada entrega traz o cabeçalho:

X-NeoPays-Signature: t=<marca temporal unix>,v1=<hmac>

onde hmac = HMAC-SHA256(segredo, "<t>.<corpo em bruto>"), em hexadecimal. Verifique em três tempos:

const crypto = require('node:crypto')

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

O corpo EM BRUTO, não o reserializado. Calcule o HMAC sobre os bytes recebidos, antes de qualquer análise JSON. Um JSON.stringify de volta pode reordenar as chaves e invalidar a assinatura.

Durante uma rotação de segredo o cabeçalho traz DUAS entradas v1=, o segredo actual primeiro. Verifique-as todas, não apenas a primeira: no dia em que mudar a sua configuração para o segredo novo antes de terminar a mudança do lado da plataforma, a primeira assinatura é a do segredo antigo, e um verificador que pare na primeira recusa então 100 % das entregas.

Reenvio

Responda 2xx em menos de alguns segundos (acuse a recepção, processe depois). Qualquer outra resposta dispara a escada de tentativas: 5 tentativas no total, após 60 s, 5 min, 30 min e 2 h.

Um endpoint que falha 20 entregas consecutivas fica suspenso uma hora (as entregas retomam sozinhas); um único sucesso — incluindo uma entrega de teste a partir do painel — reabilita-o de imediato.

Idempotência do lado do receptor

As entregas são at-least-once: o mesmo evento pode chegar duas vezes. Deduplique pelo par (event, data.id) ou registe os identificadores de eventos já processados.