Como receber webhooks do LimãoPay
O LimãoPay envia um POST HTTPS assinado com HMAC SHA256 pra cada evento (pagamento confirmado, assinatura ativada, reembolso). Você cria o endpoint no dashboard…
Resposta curta
O LimãoPay envia um POST HTTPS assinado com HMAC SHA-256 pra cada evento (pagamento confirmado, assinatura ativada, reembolso). Você cria o endpoint no dashboard, recebe um signing secret, valida a assinatura no seu servidor.
Pra que serve
Imagina que você tem um SaaS com plano Free e Pro, e usa o LimãoPay pra cobrar o Pro. Quando alguém paga, você precisa liberar o Pro pro usuário automaticamente. É exatamente pra isso que webhooks existem:
- Cliente paga no LimãoPay
- LimãoPay envia POST
order.paidpro seu servidor - Seu servidor recebe, valida a assinatura, libera o Pro
Sem polling, sem latência, sem precisar consultar a API toda hora.
Passo 1 — Criar o endpoint
- Vai em Dashboard → Developers → Webhooks
- Clica em Novo endpoint
- Escolhe o modo:
- Test: pra desenvolvimento. Não recebe eventos reais — só os que você dispara manualmente pelo botão "Sandbox"
- Live: pra produção. Recebe eventos de pagamentos reais
- Cola a URL HTTPS pública do seu servidor (ex:
https://api.seusaas.com/webhooks/limaopay) - Marca os eventos que quer receber (pelo menos
order.paid) - Clica em Criar webhook
O LimãoPay mostra o signing secret (whsec_...). Copie agora — não dá pra ver de novo. Se perder, tem que rotacionar.
Passo 2 — Validar a assinatura
Cada POST chega com esse header:
LimaoPay-Signature: t=1737686400,v1=<hmac_sha256_hex>
A assinatura é o HMAC SHA-256 da string <timestamp>.<rawBody> usando seu signing secret.
Exemplo em Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.LIMAOPAY_WEBHOOK_SECRET!;
// IMPORTANTE: usar raw body — JSON.parse antes invalida a assinatura
app.post(
"/webhooks/limaopay",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const sig = req.headers["limaopay-signature"] as string;
if (!verify(rawBody, sig)) return res.status(401).send("invalid");
const event = JSON.parse(rawBody);
if (event.type === "order.paid") {
// Liberar acesso pro buyer_email ou external_customer_id
console.log("Pago:", event.data.object.buyer_email);
}
res.status(200).send("ok");
},
);
function verify(body: string, sigHeader: string): boolean {
const parts = Object.fromEntries(
sigHeader.split(",").map((p) => p.split("=")),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || !v1) return false;
// Anti-replay: rejeita timestamps fora de 5min
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", SECRET)
.update(`${t}.${body}`)
.digest("hex");
if (v1.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
}
Por que validar assim?
- HMAC: garante que o POST veio do LimãoPay (só quem tem o secret consegue gerar a assinatura)
- Timing-safe compare: impede que um atacante descubra o secret medindo o tempo de comparação
- Timestamp tolerance: impede ataque de replay (alguém pegar um POST antigo e reenviar)
Passo 3 — Idempotência
O LimãoPay pode enviar o mesmo evento duas vezes (em caso de retry). Use o header LimaoPay-Event-Id pra dedupe:
const eventId = req.headers["limaopay-event-id"] as string;
if (await alreadyProcessed(eventId)) {
return res.status(200).send("ok (duplicate)");
}
// ... processa
await markProcessed(eventId);
Passo 4 — Testar antes de ir pra produção
No dashboard, dentro do endpoint test mode, tem o botão Sandbox:
- Escolhe o tipo de evento (ex:
order.paid) - Clica em Disparar evento mock
- O LimãoPay envia um POST com payload realista pro seu servidor
- Você vê o resultado em "Últimas entregas" — incluindo response status e tempo
Use isso pra validar a integração ponta-a-ponta antes de criar o endpoint live.
Passo 5 — Casar o pagamento com o usuário do seu SaaS
Quando o cliente do seu SaaS for pagar, gere o link da página LimãoPay anexando o ID dele:
https://limaopay.com.br/sualoja/produto?external_customer_id=user_42&metadata[plan]=pro
Estrutura da URL:
| Parte | Significado |
|---|---|
limaopay.com.br | Domínio fixo (ou limaopay.app em en-US) |
sualoja | Slug da loja do vendedor (escolhido no cadastro) |
produto | Slug da página de produto |
?external_customer_id=user_42 | ID do usuário no SEU sistema (volta em data.object.external_customer_id) |
&metadata[chave]=valor | Metadata livre — múltiplas keys aceitas (volta em data.object.external_metadata) |
Como funciona internamente: a página guarda esses valores em sessionStorage e envia ao backend quando o cliente clica em pagar. O backend persiste em Order.externalCustomerId + Order.externalMetadata. O webhook entrega ambos no payload.
Limites (alinhados com Stripe Metadata):
- Máx 50 chaves de metadata
- Chave: até 40 chars, valor: até 500 chars
external_customer_id: até 255 chars
Funciona em todos os métodos de pagamento — Stripe, PIX (V1 e V2 inline) e Mercado Pago.
Política de retry
Se seu servidor responde algo diferente de 2xx, o LimãoPay tenta de novo:
| Tentativa | Delay |
|---|---|
| 1 | imediata |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 8 horas |
| 7 | 24 horas |
| 8 | 48 horas |
Depois disso, o evento é marcado como exhausted (esgotou). Após 30 falhas consecutivas, o endpoint é auto-pausado e você recebe e-mail.
Rotacionar o secret
Se suspeitar de vazamento, rotacione no dashboard. O secret antigo continua válido por 24h (janela pra atualizar seu receptor). Durante a janela, o LimãoPay envia 2 assinaturas: v1 (novo) e v1_prev (antigo).
Checklist
- Endpoint criado com URL HTTPS pública
- Signing secret copiado e salvo em variável de ambiente
- Validação HMAC com timing-safe compare
- Validação de timestamp (anti-replay, tolerância 5min)
- Idempotência usando
LimaoPay-Event-Id - Testado no modo Sandbox antes de criar endpoint live
- Endpoint responde 2xx em até 10 segundos
- Não segue redirects (responda 2xx final, não 3xx)