Webhooks da Nomba
Um webhook é como a Nomba informa ao seu servidor que o dinheiro realmente foi movido. Qualquer pessoa na internet pode fazer POST na sua URL de webhook, portanto, o manipulador precisa provar que cada requisição veio da Nomba antes de marcar um pedido como pago ou reembolsar alguém. Ele também precisa lidar com as tentativas de repetição (retry) da Nomba para o mesmo evento.
Use o verificador do SDK oficial; não implemente o HMAC manualmente. A assinatura da Nomba cobre campos específicos unidos por :, e os SDKs também bloqueiam um truque de replay que uma implementação ingênua deixaria passar.
| Linguagem | Verificação |
|---|---|
Node.js / TS (@nombaoss/nomba-node) | verifyWebhook(rawBody, headers, signingKey) |
Python (nomba) | from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key) |
PHP (nombaoss/nomba-php) | Nomba\Webhook::verify($rawBody, $headers, $signingKey) |
Os imports exatos e os tipos de argumento para cada linguagem estão em references/sdk-setup.md. Cada um retorna o evento analisado ou lança uma exceção (WebhookVerificationError / WebhookVerificationException). Nenhum deles precisa de credenciais de API, apenas da chave de assinatura do webhook obtida no painel da Nomba (Desenvolvedor → Configuração de Webhook). Leia-a de uma variável de ambiente como NOMBA_WEBHOOK_SIGNING_KEY. É um segredo diferente do client secret da API.
O manipulador, passo a passo
- Obtenha o corpo bruto. Verifique os bytes exatos que a Nomba enviou. Se um parser de JSON rodar primeiro e você re-serializar o objeto, a assinatura pode parar de corresponder, ou pior, você acaba verificando algo diferente do que está agindo. Cada framework tem uma maneira diferente de obter o corpo bruto; veja references/frameworks.md para a maneira correta, e exclua a rota da proteção CSRF onde o framework a tiver.
- Verifique, depois analise. Use o evento que o verificador retorna. Em caso de falha, responda
401(ou400) e pare. Não registre o corpo completo ou os cabeçalhos de requisições rejeitadas no nível de info. - Desduplique usando
requestId. A Nomba repete entregas, então o mesmo evento pode chegar mais de uma vez. Registre cadarequestIdprocessado com uma restrição de unicidade e ignore os que já foram vistos. Retorne200também para duplicatas, senão a Nomba continuará tentando. - Confirme rapidamente. Retorne
200prontamente e faça trabalhos lentos (e-mails, atendimento, atualizações de razão) em um job em segundo plano, indexado porrequestId. Manipuladores lentos causam timeouts e reentregas. - Aja no evento usando a tabela abaixo. Combine o evento com seu próprio registro pela referência e verifique se o valor e a moeda correspondem ao que você esperava antes de atender.
O que cada evento significa
event_type | Correspondência | Ação |
|---|---|---|
payment_success | checkout: data.order.orderReference; conta virtual / transfer in: data.transaction.aliasAccountReference ou transactionId | Marque como pago após verificar data.transaction.transactionAmount (e a moeda) em relação ao seu pedido. Para checkouts tokenizados, armazene data.tokenizedCardData.tokenKey (se presente e não "N/A") com o cliente, apenas no lado do servidor. |
payment_failed | mesmo que acima | Marque a tentativa como falha; deixe o cliente tentar novamente. Nada foi cobrado. |
payment_reversal | mesmo que acima | O dinheiro que você recebeu foi estornado. Desfaça o atendimento ou sinalize para revisão. |
payout_success | data.transaction.merchantTxRef | Marque a transferência ou o pagamento de conta como entregue. |
payout_failed | data.transaction.merchantTxRef | Não reembolse ainda. A Nomba pode tentar novamente por outra rota. Marque como falha e aguarde. |
payout_refund | data.transaction.merchantTxRef | O dinheiro está de volta na sua carteira Nomba. Agora é seguro reembolsar ou creditar seu cliente e marcar o pagamento como falho. |
Reembolsar em payout_failed (ou em um timeout) em vez de payout_refund é a maneira clássica de comerciantes pagarem clientes duas vezes. Para qualquer evento que você não reconheça, registre-o e retorne 200.
Confirme decisões financeiras com a API. A assinatura da Nomba cobre apenas event_type, requestId, data.merchant.userId, data.merchant.walletId, data.transaction.transactionId, type, time, responseCode e o timestamp. O valor, orderReference, merchantTxRef e o tokenKey do cartão não são assinados. Portanto, antes de atender, reembolsar ou armazenar um token de cartão, consulte novamente a API com o SDK e aja com base na resposta da API: transactions.get({ orderReference }) para pagamentos de checkout (também checkout.getTransaction em produção) e transactions.get({ merchantTxRef }) para pagamentos. A desduplicação usando o requestId assinado impede que uma cópia modificada de um evento que você já processou cause problemas, e a nova consulta cobre o resto.
Proteção contra replay
A desduplicação usando requestId é a principal defesa: uma entrega repetida ou tentada novamente de um evento que você já processou não altera nada.
Os verificadores também podem rejeitar timestamps antigos (toleranceSeconds / tolerance_seconds / o quarto argumento no PHP), mas deixe isso desligado por padrão. A Nomba documenta nomba-timestamp como a hora em que o payload foi enviado, sem dizer se as tentativas automáticas de repetição recebem um novo timestamp. Se elas mantiverem o original, uma tolerância mais curta que a janela de repetição da Nomba rejeitaria tentativas legítimas após uma falha, e pedidos pagos nunca seriam confirmados. Só habilite após confirmar no sandbox que as reentregas carregam um novo timestamp e, em seguida, mantenha-a generosa (por exemplo, uma hora) com os relógios do servidor sincronizados.
Exemplo (Node.js + Express)
import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
const app = express();
// Corpo bruto apenas para esta rota; registre antes de qualquer express.json() global.
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
} catch {
return res.sendStatus(401);
}
// Restrição de unicidade em request_id; retorna false se já foi visto.
const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
if (isNew) await jobs.enqueue("nomba-webhook", event); // processar em segundo plano
res.sendStatus(200);
});
Python (Flask/FastAPI/Django), PHP (Laravel/plain) e outros frameworks Node estão em references/frameworks.md.
Testes
- Teste unitário do manipulador com o vetor de teste documentado do SDK: corpo do
webhook-fixture.jsondo SDK, chaveSampleSignatureKey123, cabeçalhonomba-timestamp: 2025-09-29T10:51:44Z,nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=. Afirme que uma chave errada, um corpo adulterado e cabeçalhos ausentes são rejeitados, e que umrequestIdduplicado é processado uma vez. - Ponto a ponto: defina uma URL de webhook de teste e uma chave de assinatura no painel, exponha seu servidor local com um túnel e faça pagamentos no sandbox. O painel pode reenviar ou depurar entregas (https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks).
Checklist
- O corpo bruto é verificado com o SDK, antes de qualquer análise ou lógica de negócio.
- A chave de assinatura vem do ambiente, não do código.
- Assinaturas ruins recebem 401/400; nenhuma outra ação acontece.
-
requestIdé desduplicado com uma restrição de unicidade; duplicatas ainda recebem 200. - Trabalhos pesados rodam em segundo plano; o manipulador retorna 200 rapidamente.
- O valor e a moeda são verificados em relação ao pedido antes de atender.
- Reembolsos acontecem apenas em
payout_refund(ou uma nova consulta que retorneREFUND). - A rota está excluída da proteção CSRF e usa HTTPS.