Documentação

API da LulaPay

Esta documentação é para quem vai plugar o PIX da LulaPay em alguma coisa: o checkout da sua loja, a sua plataforma, ou uma automação — um bot de Telegram que vende e entrega acesso VIP, por exemplo.

URL base http://localhost:3000 · todos os caminhos começam com /v1
Integrar com 1 clique usando IACopia um prompt pronto com toda a documentação. Cole no ChatGPT, Claude, Gemini, Cursor ou Lovable e diga onde quer plugar o PIX.

Visão geral

A LulaPay é um gateway de pagamentos PIX. O caminho é sempre o mesmo, independente de onde você esteja integrando:

  1. Seu sistema cria a cobrançaUma chamada POST /v1/charges feita pelo seu servidor. Volta o código copia e cola e o QR Code.
  2. Você mostra o PIX ao clienteNo checkout, numa mensagem do bot, num e-mail — onde fizer sentido.
  3. O cliente pagaA confirmação vem da instituição financeira, não de um webhook que qualquer um poderia forjar.
  4. A LulaPay te avisaPor webhook assinado (recomendado) ou pelo seu polling. O saldo entra no mesmo instante.
  5. Você entregaLibera o acesso, marca o pedido como pago, manda o link do grupo VIP.

O que muda entre um checkout e um bot? Só os passos 2 e 5. Os passos 1, 3 e 4 são idênticos — mesma API, mesmas chaves, mesmo webhook.

Comece em 5 minutos

  1. Crie a conta em /cadastro. Em ambiente local a conta já nasce liberada para produção.
  2. Pegue a chave em Painel → API. Chaves lp_test_… são sandbox; lp_live_… movimentam dinheiro de verdade.
  3. Configure um adquirente em Painel → Adquirentes. Sem isso, cobranças em produção falham com 503 no_provider — no modo teste não precisa.
  4. Crie a primeira cobrança e mostre o pix.copy_paste ao cliente.
  5. Cadastre um webhook em Painel → Integrações → Webhooks para ser avisado quando o PIX cair.
curl -X POST http://localhost:3000/v1/charges \
  -H "Authorization: Bearer lp_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-1042" \
  -d '{
    "amount_cents": 2990,
    "description": "VIP 30 dias",
    "external_id": "pedido-1042",
    "customer": { "name": "Maria Silva", "email": "[email protected]" },
    "metadata": { "telegram_user_id": "884412037", "plano": "vip_30d" }
  }'

Autenticação

Toda chamada leva a chave no header Authorization, no formato Bearer:

Authorization: Bearer lp_live_9f2c...
PrefixoAmbienteO que acontece
lp_test_TestePIX de mentira, dinheiro de mentira. Dá para marcar como paga pelo painel, sem pagar nada.
lp_live_ProduçãoPIX real, cobrado do cliente de verdade, entra no seu saldo de verdade.

Os dois ambientes são completamente separados: cobranças, clientes, saldo e webhooks de teste não enxergam nada de produção, e vice-versa. Dá para deixar os dois configurados ao mesmo tempo e trocar só a variável de ambiente.

A chave secreta só vive no seu servidor. Nunca coloque lp_live_ em JavaScript de página, em app de celular, em repositório público ou num arquivo de configuração que você compartilha. Quem tem a chave cria cobranças e enxerga todas as suas vendas.

Regras da API

Idempotência

Esse é o ponto que mais dá problema em integração de bot, então vale ler com atenção.

Um usuário no Telegram clica duas vezes no botão "Quero o VIP". Ou a rede cai no meio da chamada e o seu código tenta de novo. Nos dois casos, sem proteção, você cria duas cobranças para a mesma venda — e o cliente recebe dois PIX diferentes.

Para evitar, mande o header Idempotency-Key com um valor que identifique aquela intenção de compra:

Idempotency-Key: tg-884412037-vip30-1757630000
SituaçãoResposta
Primeira chamada com aquela chave201 com a cobrança criada
Repetiu a chave, mesmo corpoA mesma cobrança, com o header Idempotent-Replayed: true
Repetiu a chave, corpo diferente409 idempotency_conflict
Repetiu enquanto a primeira processa409 idempotency_in_progress — tente em 1s

Como montar uma boa chave: estável para a mesma intenção, diferente para intenções diferentes. Em bot, {plataforma}-{user_id}-{plano}-{janela_de_tempo} funciona bem; em loja, use o número do pedido. Evite usar só o user_id: assim a segunda compra do mesmo cliente seria bloqueada para sempre.

Criar cobrança

POST /v1/charges
CampoTipoDescrição
amount_cents obrigatóriointeiroValor em centavos. Entre 100 e 5000000. Opcional se você mandar product_id.
description opcionaltextoAté 140 caracteres. Aparece para o cliente e no seu painel.
external_id opcionaltextoAté 100 caracteres. O identificador do pedido no seu sistema. Use para casar a venda de volta.
expires_in opcionalinteiroSegundos até expirar. Entre 300 (5 min) e 604800 (7 dias). Padrão: 3600.
customer.name opcionaltextoNome do pagador.
customer.document opcionaltextoCPF ou CNPJ. Validado — se for inválido, a chamada falha.
customer.email opcionaltextoValidado como e-mail.
customer.phone opcionaltextoSó dígitos, até 15.
metadata opcionalobjetoAté 20 chaves, 4 KB. Volta inteiro no webhook. É o campo mais útil para bot.
tracking opcionalobjetoutm_source, utm_medium, utm_campaign, src, sck. Alimenta os relatórios de origem.
product_id opcionaltextoUm produto cadastrado no painel. Preenche valor e descrição sozinho.

Resposta 201

{
  "id": "ch_mvk64kniwDdt4CEw9WUyCjVtTYqO",
  "object": "charge",
  "livemode": false,
  "status": "pending",
  "amount_cents": 2990,
  "fee_cents": 338,
  "net_cents": 2652,
  "currency": "BRL",
  "description": "VIP 30 dias",
  "external_id": "pedido-1042",
  "customer": { "name": "Maria Silva", "document": null, "email": "[email protected]", "phone": null },
  "product": null,
  "origin": "api",
  "tracking": {},
  "metadata": { "telegram_user_id": "884412037", "plano": "vip_30d" },
  "pix": {
    "copy_paste": "00020126580014br.gov.bcb.pix...6304AB12",
    "qr_code_base64": "data:image/png;base64,iVBORw0KGgo..."
  },
  "payer_name": null,
  "end_to_end_id": null,
  "expires_at": "2026-10-22T23:14:03.117Z",
  "paid_at": null,
  "created_at": "2026-10-22T22:14:03.117Z"
}
CampoO que fazer com ele
idGuarde. É por ele que você consulta e que o webhook identifica a venda.
pix.copy_pasteO código copia e cola — é isso que o cliente cola no banco.
pix.qr_code_base64PNG pronto em data URL. Em bot, dá para mandar direto como foto.
fee_centsTaxa já calculada.
net_centsO que de fato entra no seu saldo.
expires_atDepois disso a cobrança vira expired sozinha e não pode mais ser paga.

O QR Code só vem em cobrança pendente. Depois de paga ou expirada, qr_code_base64 volta null; o copy_paste continua lá para histórico.

Estados de uma cobrança

statusSignificado
pendingPIX gerado, esperando o pagamento.
paidPago e confirmado na fonte. Só entregue neste estado.
expiredPassou do expires_at sem pagamento.
failedNão foi possível gerar o PIX no adquirente.
refundedEstornada.

Consultar cobrança

GET /v1/charges/{id}

Devolve o mesmo objeto acima, com o QR Code quando ainda estiver pendente. É o que o polling usa.

curl http://localhost:3000/v1/charges/ch_mvk64kniwDdt4CEw9WUyCjVtTYqO \
  -H "Authorization: Bearer lp_test_..."

Listar cobranças

GET /v1/charges
ParâmetroDescrição
statusFiltra por pending, paid, expired…
external_idBusca pelo seu identificador de pedido.
limitQuantas por página.
starting_afterID da última da página anterior, para paginar.
{ "object": "list", "data": [ { "id": "ch_...", "status": "paid", ... } ], "has_more": false }

Metadata: o campo que resolve a vida do bot

O metadata é um objeto livre (até 20 chaves, 4 KB) que a LulaPay guarda junto da cobrança e devolve inteiro no webhook. Ele existe para você não precisar manter uma tabela só para lembrar de quem era aquele PIX.

Numa venda de VIP por bot, o que você precisa saber na hora de entregar é: qual usuário do Telegram, em qual chat, qual plano. Guarde tudo ali:

"metadata": {
  "telegram_user_id": "884412037",
  "chat_id": "-1002145887733",
  "message_id": "4471",
  "plano": "vip_30d",
  "bot": "sharkbot",
  "workspace": "ws_8f2a"
}

Quando o webhook charge.paid chegar, você lê data.metadata.telegram_user_id e já sabe para quem mandar o link do grupo — sem consultar banco nenhum.

Não coloque segredo em metadata. Nada de token de bot, senha ou chave de API. É um campo de anotação, não um cofre.

Saldo e saques

GET /v1/balance
{ "object": "balance", "livemode": true, "available_cents": 1481410, "pending_cents": 0, "currency": "BRL" }
POST /v1/withdrawals
curl -X POST http://localhost:3000/v1/withdrawals \
  -H "Authorization: Bearer lp_live_..." \
  -H "Idempotency-Key: saque-2026-10-22-01" \
  -d '{ "amount_cents": 50000, "payout_account_id": "pa_..." }'

A conta de destino precisa estar cadastrada antes em Painel → Configurações → Contas de saque (o cadastro pede a sua senha). Se você configurar uma lista de IPs autorizados em Painel → API, saques pela API só saem desses IPs — recomendado.

GET /v1/withdrawals
GET /v1/withdrawals/{id}

Webhook ou polling?

Você tem duas formas de saber que o PIX caiu. Escolha pela sua infraestrutura, não por preferência:

CritérioWebhookPolling
Como funcionaA LulaPay chama a sua URLVocê pergunta de tempos em tempos
Precisa deUm endereço HTTPS públicoNada — só sair para a internet
LatênciaSegundosO intervalo que você escolher
Bom paraLoja, plataforma, API hospedadaBot em casa, VPS sem domínio, script local
CustoNenhumConsome do seu limite de 300 req/min

Dá para usar os dois. A configuração mais robusta é webhook como caminho principal e um polling lento (a cada 60s, só nas cobranças pendentes das últimas horas) como rede de segurança, para o caso de o webhook ter falhado.

Webhooks

Cadastre a URL em Painel → Integrações → Webhooks. Você recebe um segredo whsec_… — ele aparece uma vez só, guarde na hora.

Eventos

EventoQuando dispara
charge.createdCobrança criada (PIX gerado). Útil para marcar "aguardando pagamento".
charge.paidPagamento confirmado. É aqui que você entrega.
charge.expiredExpirou sem pagamento. Bom gancho para remarketing.
withdrawal.paidSaque concluído.
withdrawal.failedSaque recusado.

Formato

POST https://seu-sistema.com/webhooks/lulapay
Content-Type: application/json
User-Agent: LulaPay-Webhooks/1.0
LulaPay-Signature: t=1761172919,v1=c544a141d69d3f00...
LulaPay-Event-Id: evt_7Kd2...
LulaPay-Event-Type: charge.paid

{
  "id": "evt_7Kd2...",
  "type": "charge.paid",
  "livemode": true,
  "created_at": "2026-10-22T22:21:59.392Z",
  "data": {
    "id": "ch_mvk64kniwDdt4CEw9WUyCjVtTYqO",
    "status": "paid",
    "amount_cents": 2990,
    "fee_cents": 338,
    "net_cents": 2652,
    "external_id": "pedido-1042",
    "metadata": { "telegram_user_id": "884412037", "plano": "vip_30d" },
    "payer_name": "MARIA S SILVA",
    "end_to_end_id": "E00000000202610222221...",
    "paid_at": "2026-10-22T22:21:59.392Z"
  }
}

Regras de quem recebe

Validar a assinatura

Qualquer um pode mandar um POST para a sua URL dizendo "esse PIX foi pago". A assinatura é o que separa o webhook de verdade de uma tentativa de fraude. Valide sempre, antes de entregar qualquer coisa.

LulaPay-Signature: t=1761172919,v1=c544a141d69d3f00...

Onde v1 = HMAC_SHA256(segredo, "{t}.{corpo_bruto}"). O corpo bruto é o texto exato que chegou — se o framework fizer o parse do JSON e você serializar de novo, a assinatura não bate.

const crypto = require('node:crypto');
const express = require('express');
const app = express();

// IMPORTANTE: corpo bruto, sem parse
app.post('/webhooks/lulapay', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('LulaPay-Signature') || '';
  const t  = /t=(\d+)/.exec(header)?.[1];
  const v1 = /v1=([0-9a-f]+)/.exec(header)?.[1];
  if (!t || !v1) return res.sendStatus(400);

  // rejeita evento velho demais (proteção contra replay)
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);

  const esperado = crypto.createHmac('sha256', process.env.LULAPAY_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest('hex');

  // comparação em tempo constante
  const ok = esperado.length === v1.length &&
    crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(v1));
  if (!ok) return res.sendStatus(401);

  const evt = JSON.parse(req.body);
  res.sendStatus(200);                                // responde primeiro
  if (evt.type === 'charge.paid') processar(evt);     // processa depois
});

Três erros clássicos: (1) usar o JSON já parseado em vez do corpo bruto; (2) comparar as strings com == em vez de comparação em tempo constante; (3) não checar o t, deixando alguém reenviar um webhook antigo válido.

Polling

Se o seu bot roda num lugar sem endereço público — VPS sem domínio, máquina em casa, script local — você não consegue receber webhook. Nesse caso, pergunte:

import os, time, requests

BASE = "http://localhost:3000"
H = {"Authorization": "Bearer " + os.environ["LULAPAY_KEY"]}

def esperar_pagamento(charge_id, timeout=3600):
    """Consulta com intervalo crescente até pagar, expirar ou estourar o tempo."""
    inicio, intervalo = time.time(), 3
    while time.time() - inicio < timeout:
        r = requests.get(f"{BASE}/v1/charges/{charge_id}", headers=H, timeout=15)
        if r.status_code == 429:                       # respeite o limite
            time.sleep(30)
            continue
        r.raise_for_status()
        ch = r.json()
        if ch["status"] in ("paid", "expired", "failed", "refunded"):
            return ch
        time.sleep(intervalo)
        intervalo = min(intervalo * 1.5, 30)           # 3s, 4.5s, 6.7s… até 30s
    return None

Não consulte de 1 em 1 segundo em loop infinito. São 300 requisições por minuto no total da conta — com 20 clientes esperando ao mesmo tempo, um polling de 1s estoura o limite e derruba a integração inteira. Comece em 3s, aumente até 30s, e pare assim que a cobrança sair de pending.

Para conferir várias de uma vez, uma chamada resolve:

GET /v1/charges?status=paid&limit=50

Receita: bot de Telegram vendendo VIP

Esse é o caso mais comum de quem integra a LulaPay: um bot que vende acesso a um grupo ou canal e entrega sozinho quando o PIX cai. O desenho vale igual para SharkBot, NinjaBotX, SilverBot, RavenBot ou um bot próprio.

  1. O usuário clica em "Quero o VIP"Seu handler recebe o callback com o user_id e o chat_id.
  2. Seu servidor cria a cobrançaCom Idempotency-Key (contra clique duplo) e metadata guardando quem é.
  3. O bot manda o QR e o copia e colaQR como foto; copia e cola numa mensagem separada em <code>, para o usuário tocar e copiar.
  4. O pagamento confirmaWebhook charge.paid, ou polling se você não tem URL pública.
  5. O bot entregaGera um link de convite de uso único, manda ao usuário e registra a validade do acesso.

Passos 2 e 3 — criar a cobrança e mandar o PIX

import os, time, base64, httpx
from aiogram import types

BASE   = "http://localhost:3000"
KEY    = os.environ["LULAPAY_KEY"]
PLANOS = {"vip_30d": (2990, "VIP 30 dias"), "vip_vitalicio": (9700, "VIP vitalício")}

async def vender(cb: types.CallbackQuery, plano: str):
    valor, nome = PLANOS[plano]
    uid = cb.from_user.id

    # janela de 10 min: clique duplo reaproveita o mesmo PIX,
    # mas uma compra nova daqui a meia hora gera outro
    janela = int(time.time()) // 600
    chave  = f"tg-{uid}-{plano}-{janela}"

    async with httpx.AsyncClient(timeout=20) as http:
        r = await http.post(f"{BASE}/v1/charges",
            headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": chave},
            json={
                "amount_cents": valor,
                "description": nome,
                "external_id": chave,
                "expires_in": 1800,                       # 30 min
                "customer": {"name": cb.from_user.full_name},
                "metadata": {
                    "telegram_user_id": str(uid),
                    "chat_id": str(cb.message.chat.id),
                    "plano": plano,
                },
            })
    r.raise_for_status()
    ch = r.json()

    # QR como foto
    png = base64.b64decode(ch["pix"]["qr_code_base64"].split(",", 1)[1])
    await cb.message.answer_photo(
        types.BufferedInputFile(png, "pix.png"),
        caption=f"<b>{nome}</b>\nR$ {valor/100:.2f}".replace(".", ","))

    # copia e cola em mensagem separada: o usuário toca e copia
    await cb.message.answer(
        "Toque no código para copiar:\n\n"
        f"<code>{ch['pix']['copy_paste']}</code>\n\n"
        "Assim que o PIX cair eu te mando o acesso aqui mesmo. ⚡")

    return ch["id"]

Passos 4 e 5 — receber e entregar

async def liberar_acesso(data):
    """Chamado pelo webhook charge.paid, com a assinatura já validada."""
    uid   = int(data["metadata"]["telegram_user_id"])
    plano = data["metadata"]["plano"]

    # 1) proteção contra entrega duplicada — o mesmo evento pode chegar 2x
    if ja_entregue(data["id"]):
        return
    marcar_entregue(data["id"])

    # 2) convite de uso único e com validade
    link = await bot.create_chat_invite_link(
        chat_id=GRUPO_VIP, member_limit=1,
        expire_date=int(time.time()) + 3600)

    # 3) registra a assinatura ANTES de mandar o link
    salvar_assinatura(uid, plano, dias=30 if plano == "vip_30d" else None)

    await bot.send_message(uid,
        "✅ <b>Pagamento confirmado!</b>\n\n"
        f"Seu acesso está liberado: {link.invite_link}\n"
        "O convite vale por 1 hora e só pode ser usado uma vez.")

Por que marcar como entregue antes de mandar? Se o processo cair no meio, é melhor o cliente reclamar que não recebeu (e você reenviar manualmente) do que dois eventos gerarem dois convites e um deles vazar para fora do grupo.

Se o seu bot não tem URL pública

Troque o passo 4 pelo polling: depois de mandar o PIX, dispare uma tarefa que acompanha aquela cobrança e chama o mesmo liberar_acesso() quando o status virar paid.

import asyncio

async def acompanhar(charge_id):
    intervalo = 3
    for _ in range(600):
        async with httpx.AsyncClient(timeout=15) as http:
            r = await http.get(f"{BASE}/v1/charges/{charge_id}",
                               headers={"Authorization": f"Bearer {KEY}"})
        if r.status_code == 429:
            await asyncio.sleep(30); continue
        ch = r.json()
        if ch["status"] == "paid":
            return await liberar_acesso(ch)
        if ch["status"] in ("expired", "failed", "refunded"):
            return
        await asyncio.sleep(intervalo)
        intervalo = min(intervalo * 1.5, 30)

# logo depois de mandar o PIX:
asyncio.create_task(acompanhar(charge_id))

Receita: checkout externo

Para uma loja, plataforma ou checkout próprio o desenho é o mesmo — muda só que a tela é uma página web.

  1. O cliente finaliza o pedidoSeu backend cria a cobrança com o número do pedido em external_id.
  2. A página mostra o PIXQR Code, botão de copiar e um contador até o expires_at.
  3. A página pergunta ao SEU backend se já pagouNunca direto à LulaPay — a chave não pode ir para o navegador.
  4. O webhook confirma no backendMarque o pedido como pago e dispare o que tiver que disparar (nota, e-mail, separação).
  5. A página redirecionaPara a tela de obrigado assim que o seu backend responder "pago".

Backend — criar a cobrança

app.post('/pedidos/:id/pix', async (req, res) => {
  const pedido = await db.pedido(req.params.id);

  const r = await fetch(`${process.env.LULAPAY_BASE}/v1/charges`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.LULAPAY_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `pedido-${pedido.id}`,          // o número do pedido já é único
    },
    body: JSON.stringify({
      amount_cents: pedido.total_cents,
      description: `Pedido #${pedido.numero}`,
      external_id: String(pedido.id),
      expires_in: 1800,
      customer: {
        name: pedido.cliente_nome,
        document: pedido.cliente_cpf,
        email: pedido.cliente_email,
      },
      tracking: { utm_source: pedido.utm_source, utm_campaign: pedido.utm_campaign },
    }),
  });
  const ch = await r.json();
  await db.salvarCobranca(pedido.id, ch.id);

  // só o necessário vai para o navegador — nunca a chave
  res.json({ copy_paste: ch.pix.copy_paste, qr: ch.pix.qr_code_base64, expires_at: ch.expires_at });
});

Frontend — perguntar ao seu próprio backend

let intervalo = 3000;
async function conferir() {
  const r = await fetch(`/pedidos/${pedidoId}/status`);   // SUA rota, não a da LulaPay
  const { pago } = await r.json();
  if (pago) return location.assign(`/obrigado/${pedidoId}`);
  intervalo = Math.min(intervalo * 1.4, 15000);
  setTimeout(conferir, intervalo);
}
setTimeout(conferir, intervalo);

A chave nunca vai para o navegador. Se o seu frontend chamar /v1/charges direto, qualquer visitante abre o DevTools e leva a sua chave — e com ela consulta todas as suas vendas e cria cobranças em seu nome. O navegador fala com o seu servidor; só o seu servidor fala com a LulaPay.

Remarketing e expiração

O evento charge.expired é o gancho para recuperar quem gerou o PIX e não pagou. Com o metadata você sabe exatamente quem foi:

if evt["type"] == "charge.expired":
    uid   = int(evt["data"]["metadata"]["telegram_user_id"])
    plano = evt["data"]["metadata"]["plano"]
    # uma oferta de recuperação, normalmente mais barata que a original
    await oferecer_desconto(uid, plano)

Ajuste o expires_in ao seu funil: 30 minutos costuma bastar para venda por impulso em bot, e deixa o remarketing começar rápido. Para compra de valor alto, algo entre 24h e 7 dias faz mais sentido.

Testar antes de produção

Com uma chave lp_test_ as cobranças são geradas normalmente, mas nenhum dinheiro se move. Para simular um pagamento, abra a cobrança em Painel → Vendas (com o seletor no modo Teste) e clique em Simular pagamento. O webhook charge.paid dispara igualzinho ao de produção.

O que vale exercitar antes de virar a chave:

Erros

Todo erro volta com o mesmo formato:

{ "error": { "code": "validation_error", "message": "Dados inválidos.",
  "details": { "amount_cents": "Valor mínimo: R$ 1,00" } } }
HTTPcodeO que fazer
400invalid_requestCorrija o corpo. O details diz qual campo.
401unauthorizedChave ausente, errada ou revogada.
403ip_not_allowedSaque pedido de um IP fora da lista autorizada.
404not_foundO recurso não existe nesse modo. Cobrança de teste não aparece com chave de produção.
409idempotency_conflictMesma Idempotency-Key com corpo diferente. Use outra chave.
409idempotency_in_progressA primeira chamada ainda processa. Tente em 1s.
422insufficient_fundsSaque maior que o saldo disponível.
429rate_limitedPassou de 300/min. Espere e reduza a frequência do polling.
502provider_unavailableTodos os adquirentes falharam. Tente de novo em instantes.
503no_providerNenhum adquirente configurado para produção. Configure em Painel → Adquirentes.

Na sua integração, trate 429, 502 e 503 como temporários (vale repetir, com espera crescente) e 400, 401, 404 e 409 como definitivos — repetir não muda nada.

Segurança

Checklist de go-live

  • Chave lp_live_ em variável de ambiente, fora do git
  • Adquirente configurado em Painel → Adquirentes
  • Idempotency-Key em toda criação de cobrança
  • Webhook cadastrado, com assinatura validada e checagem de replay
  • Entrega protegida contra evento duplicado (guarda o LulaPay-Event-Id)
  • Polling, se usado, com intervalo crescente e teto de 30s
  • 429, 502 e 503 tratados com repetição e espera
  • external_id apontando para o pedido no seu sistema
  • metadata com o que você precisa na hora de entregar
  • Conta de saque cadastrada e lista de IPs autorizados ativa
  • Fluxo inteiro exercitado no modo teste, inclusive os caminhos ruins

Faltou alguma coisa aqui? Escreva para [email protected] que a gente documenta.