Voltar pro blog
integracoes

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…

Equipe LimãoPay20 de outubro de 20188 min de leitura

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:

  1. Cliente paga no LimãoPay
  2. LimãoPay envia POST order.paid pro seu servidor
  3. 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

  1. Vai em Dashboard → Developers → Webhooks
  2. Clica em Novo endpoint
  3. 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
  4. Cola a URL HTTPS pública do seu servidor (ex: https://api.seusaas.com/webhooks/limaopay)
  5. Marca os eventos que quer receber (pelo menos order.paid)
  6. 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)

Estrutura do payload

Formato estilo Stripe Event Object — o campo type discrimina, e data.object muda de forma conforme o evento. Exemplo de um order.paid:

{
  "id": "evt_2k4m9x1abc",
  "type": "order.paid",
  "created": 1737686400,
  "livemode": true,
  "api_version": "2026-01-01",
  "data": {
    "object": {
      "id": "ord_8f3a1b2c",
      "object": "order",
      "tenant_id": "tnt_seller_xyz",
      "offer_id": "off_pro_plan",
      "product_id": "prod_pro",
      "status": "paid",
      "amount_total": 4900,
      "currency": "brl",
      "payment_method": "pix",
      "buyer_email": "comprador@example.com",
      "buyer_name": "Maria Silva",
      "external_customer_id": "user_42",
      "external_metadata": { "plan": "pro" },
      "product_subscription_id": null,
      "paid_at": "2026-05-21T14:30:00.000Z",
      "refunded_at": null
    }
  }
}

Onde achar o que você precisa:

Quero saber…Caminho no payload
Email do compradordata.object.buyer_email
Nome do compradordata.object.buyer_name
Seu ID interno do userdata.object.external_customer_id
Metadata que você passou na URLdata.object.external_metadata
Valor pago (em centavos)data.object.amount_total
Moedadata.object.currency (lowercase: brl, usd)
Método de pagamentodata.object.payment_method (pix, stripe, mp)
Quando foi pagodata.object.paid_at (ISO 8601)
ID único do evento (pra dedupe)header LimaoPay-Event-Id ou campo id

Eventos de assinatura (subscription.activated, subscription.renewed, etc.) têm data.object com formato diferente — incluem billing_cycle, current_period_start/end, price_at_creation e também buyer_email.

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:

  1. Escolhe o tipo de evento (ex: order.paid)
  2. Clica em Disparar evento mock
  3. O LimãoPay envia um POST com payload realista pro seu servidor
  4. 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:

ParteSignificado
limaopay.com.brDomínio fixo (ou limaopay.app em en-US)
sualojaSlug da loja do vendedor (escolhido no cadastro)
produtoSlug da página de produto
?external_customer_id=user_42ID do usuário no SEU sistema (volta em data.object.external_customer_id)
&metadata[chave]=valorMetadata 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.

Passo 6 — Redirect pós-pagamento (opcional)

Por padrão, depois que o comprador paga, o LimãoPay mostra um modal de "Pagamento confirmado" e fica nele. Pra SaaS externos que querem trazer o comprador de volta pro seu app (mostrar página de "obrigado", liberar acesso na hora, etc.), configure duas URLs no webhook:

  1. Vai em Dashboard → Developers → Webhooks → [seu endpoint]
  2. Preencha URL de sucesso (ex: https://seusaas.com/limaopay/success) e opcionalmente URL de cancelamento

Depois disso, todo pagamento confirmado pra esse vendedor exibe o modal de confirmação por 2,5 segundos com um spinner ("Redirecionando você de volta…") e em seguida joga o comprador na sua URL de sucesso.

Como casar a Order que voltou

A URL de sucesso é única (não muda por checkout). Pra saber qual Order o comprador acabou de fechar, use:

  • O webhook order.paid (que chega em paralelo com LimaoPay-Event-Id): tem todos os dados — order.id, buyer_email, external_customer_id, external_metadata
  • Se você precisar identificar no momento do redirect (antes do webhook chegar), passe o ID interno como external_customer_id na URL da página LimãoPay: ?external_customer_id=user_42. Você pode persistir esse ID no seu lado em "aguardando pagamento" e correlacionar quando o webhook chegar

Validação de segurança

  • HTTPS obrigatório (HTTP só em dev)
  • Hostnames localhost, .local, .internal e IPs privados são bloqueados (defesa anti-SSRF)
  • Limite de 2048 caracteres por URL
  • A URL fica snapshotada na Order no momento da criação. Mudanças posteriores no webhook config não afetam orders em andamento

Quando o redirect NÃO acontece

  • Webhook em modo test — só endpoints em modo live entram na resolução
  • Webhook pausado (status: paused ou disabled_by_failures)
  • Comprador fechou a aba durante o polling — não há browser pra redirecionar
  • Sua URL não passou na validação HTTPS — vendedor recebe erro ao criar/editar o webhook, redirect não é configurado

Política de retry

Se seu servidor responde algo diferente de 2xx, o LimãoPay tenta de novo:

TentativaDelay
1imediata
21 minuto
35 minutos
430 minutos
52 horas
68 horas
724 horas
848 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)

Comece a vender hoje

Crie sua página de vendas com IA, configure o pagamento e comece a faturar. Zero pra começar.