v1.3.0 OAS 3.0.3

LiquiBR v1

API REST para gerar cobranças PIX e receber USDT na sua carteira automaticamente. Construída sobre HTTPS com auth Bearer e webhooks assinados via HMAC-SHA256.

Base URL: https://liquibr.com/v1 JSON UTF-8

Introdução

O LiquiBR é uma plataforma de pagamentos que recebe PIX em BRL e envia USDT (TRON / Polygon / BSC) automaticamente para a carteira configurada do lojista. Esta API permite integrar o LiquiBR ao seu checkout, sistema de gestão ou painel administrativo.

Fluxo típico: seu sistema chama POST /v1/charges com o valor → LiquiBR retorna QR code PIX e copia-e-cola → o pagador paga → LiquiBR recebe webhook do PSP, confirma o pagamento, e envia POST assinado para a sua URL configurada → seu sistema marca o pedido como pago.

Autenticação

Todas as requisições à API /v1/* exigem autenticação via API Key. Gere a sua em Dashboard → API Keys & Webhooks. As chaves começam com pk_live_ e devem ser tratadas como senha.

Header padrão (recomendado)

httpAuthorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Alternativa

httpX-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
⚠ Nunca exponha a API key no front-end. Sempre faça as chamadas a partir do seu backend. Chaves comprometidas devem ser revogadas imediatamente no dashboard.

Health check

Endpoint público pra monitoramento (UptimeRobot, statuspage etc):

GET/health
json{
  "status": "ok",
  "checks": {
    "database": "ok",
    "psp_mode": "pushinpay"
  },
  "uptime_seconds": 3600,
  "response_time_ms": 2,
  "version": "1.1.0",
  "timestamp": "2026-05-06T12:00:00Z"
}

Retorna HTTP 200 quando tudo OK, 503 se algum serviço crítico está degradado.

Carteira & Rede USDT

O LiquiBR é não-custodial: nós convertemos PIX em USDT e enviamos diretamente para a carteira que você cadastra no painel. Não custodiamos cripto — assim que o PIX é confirmado, o operador da plataforma envia USDT direto pro seu wallet, on-chain, com TX hash registrado e auditável.

Redes suportadas

RedeTokenTaxa de rede aprox.Tempo de confirmação
TRONUSDT (TRC-20)~$1 USD~30 segundos
POLYGONUSDT (Polygon PoS)~$0.05 USD~5 segundos
BSCUSDT (BEP-20)~$0.30 USD~3 segundos

Cadastre seu endereço e a rede em Dashboard → Meu Perfil → Carteira USDT. O endereço é validado automaticamente conforme o formato da rede escolhida. A consulta da rede atual está disponível em GET /v1/account:

json{
  "id": 42,
  "name": "Loja Exemplo",
  "email": "[email protected]",
  "role": "admin",
  "usdt_wallet": {
    "address": "TXyz...AbCd",
    "network": "TRON",
    "configured": true
  }
}
⚠ Verifique antes de operar: chame GET /v1/account no setup da sua integração e confirme que usdt_wallet.configured é true. Se for false, novas cobranças funcionam mas o envio de USDT fica pendente até você cadastrar uma carteira.

Erros

Todas as respostas de erro retornam JSON com a estrutura abaixo. O HTTP status indica a categoria do erro.

json{
  "error": "validation_error",
  "message": "Dados inválidos",
  "details": { ... }
}
StatusCodeSignificado
400validation_errorBody inválido ou campo faltando
400amount_above_limitValor da cobrança ultrapassa o max_charge_brl da sua conta. Response inclui max_charge_brl com seu limite atual.
401unauthorizedAPI key ausente, inválida ou revogada
403profile_incompletePerfil do lojista não tem nome legal + WhatsApp cadastrados. Complete em Dashboard → Meu Perfil → Dados de Contato.
403account_blocked_medConta bloqueada por débito de MED (devolução PIX) acima do limite. Anexe documentação em Dashboard → Compliance.
403account_suspendedConta suspensa pelo operador. Contate o suporte.
404not_foundCobrança ou recurso não existe
409idempotency_conflictexternal_id (ou header Idempotency-Key) já foi usado nas últimas 24h com valor diferente. Use external_id novo ou aguarde a janela expirar.
409charge_not_pendingTentativa de cancelar (DELETE /v1/charges/:id) charge que já está paid, expired ou refunded.
429rate_limit_exceededExcedeu 100 requisições/minuto por IP. Header Retry-After indica em quantos segundos pode tentar de novo.
501not_implementedEndpoint reservado mas ainda não disponível (ex: POST /v1/charges/:id/refund). Response inclui contato de suporte pra operação manual.
502psp_errorErro no PSP que processa o PIX
500internal_errorErro interno do servidor

Criar cobrança PIX

POST/v1/charges

Gera um QR code PIX que o pagador escaneia / copia-e-cola pra pagar. O valor já vem com taxa da plataforma calculada: 3% sobre o valor do PIX + R$ 0,55 fixo (a taxa fixa é aplicada em todas as cobranças, independente do valor).

Request body

CampoTipoDescrição
amount obrigatórionumberValor em reais. Mín R$ 1, máx limitado pelo seu max_charge_brl per-conta (default R$ 500, operator eleva caso a caso até R$ 100.000). Se ultrapassar, retorna 400 amount_above_limit com o seu limite atual.
external_idstringID do seu sistema (ex: ID do pedido). Funciona como chave de idempotência (janela de 24h — veja seção "Idempotência" abaixo).
descriptionstringDescrição livre (até 255 chars)
expires_in_minutesnumberTempo de expiração. Default 30, máx 1440. Piso silencioso de 30min — valores menores são automaticamente elevados, porque o PSP pode confirmar pagamento depois do QR "expirar" e disparar pix.expired antes do pix.received quebra integrações que travam a cobrança no expired.
webhook_urlstring (URL)URL de callback específica desta cobrança. Recebe pix.received, pix.expired, pix.med e usdt.sent assinados. Alternativa ao cadastro no painel.
customer.namestringNome do pagador
customer.emailstringEmail do pagador
customer.documentstringCPF/CNPJ do pagador

Headers opcionais

HeaderDescrição
Idempotency-KeyAlternativa ao external_id no body, padrão Stripe-like. Janela de 24h. Se ambos forem enviados, external_id do body tem prioridade.

Idempotência

Tanto external_id (body) quanto Idempotency-Key (header) servem como chave de idempotência. Comportamento:

Exemplo cURL

bashcurl -X POST https://liquibr.com/v1/charges \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.90,
    "external_id": "pedido-12345",
    "description": "Plano Premium mensal",
    "webhook_url": "https://seu-sistema.com/webhook/liquibr",
    "customer": {
      "name": "João Silva",
      "email": "[email protected]",
      "document": "12345678901"
    }
  }'
💡 webhook_url inline: ao informar webhook_url no corpo da cobrança, o LiquiBR envia pix.received, pix.expired, pix.med e usdt.sent diretamente para essa URL — sem precisar cadastrar nada no painel. Útil quando seu sistema já sabe pra onde quer receber o callback antes de criar a cobrança (integrações tipo CRM, gestor de pedidos, plataforma de afiliados). O payload e a assinatura HMAC-SHA256 são idênticos aos webhooks do painel.
📷 QR Code — três formatos:
  • pix.qr_code_base64 e pix.qr_code_image_url: strings prontas pra <img src=> (data URI data:image/png;base64,… ou URL externa). Use direto: <img src={pix.qr_code_image_url} />.
  • pix.qr_code_base64_raw: base64 puro (sem prefixo). Só use se você quiser montar o data URI manualmente: <img src={`data:image/png;base64,${pix.qr_code_base64_raw}`} />.
❌ NÃO faça: <img src={`data:image/png;base64,${pix.qr_code_base64}`} /> — isso duplica o prefixo e quebra a imagem (use qr_code_base64_raw nesse caso).

Response 201

json{
  "id": "1842",
  "external_id": "pedido-12345",
  "status": "pending",
  "amount": 49.90,
  "amount_cents": 4990,
  "fees": {
    "platform_fee_pct": 3.0,
    "percentage_fee_brl": 1.50,
    "fixed_fee_brl": 0.55,
    "total_fee_brl": 2.05,
    "net_brl": 47.85
  },
  "pix": {
    "qr_code": "00020126580014BR.GOV.BCB.PIX...",
    "qr_code_base64": "data:image/png;base64,iVBORw0...",
    "qr_code_image_url": "data:image/png;base64,iVBORw0...",
    "qr_code_base64_raw": "iVBORw0KGgoAAAANSUhEUgAAA...",
    "copy_paste": "00020126580014BR.GOV.BCB.PIX..."
  },
  "expires_at": "2026-05-05T18:30:00.000Z",
  "created_at": "2026-05-05T18:00:00",
  "psp_id": "a1b5323c-9e68-4afb-8927-94cdcef7a156"
}

Consultar cobrança

GET/v1/charges/{id}

Retorna o status atual de uma cobrança. O {id} pode ser tanto o id retornado pelo LiquiBR quanto o external_id que você passou no momento da criação.

bashcurl https://liquibr.com/v1/charges/pedido-12345 \
  -H "Authorization: Bearer pk_live_xxx"

Status possíveis

StatusSignificado
pendingAguardando pagamento
paidPago e confirmado pelo PSP
expiredQR code expirou sem pagamento
failedErro no processamento
refundedEstornado

Listar cobranças

GET/v1/charges?status=paid&limit=50&offset=0
bashcurl "https://liquibr.com/v1/charges?status=paid&limit=50" \
  -H "Authorization: Bearer pk_live_xxx"

Filtros disponíveis

Query paramTipoDescrição
statusstringpending | paid | expired | failed | refunded
date_fromdateYYYY-MM-DD. Filtra created_at >= esta data (inclusive). Use pra reconciliar dia/período.
date_todateYYYY-MM-DD. Filtra created_at <= fim deste dia (inclusive).
payer_documentstringCPF/CNPJ do pagador. Aceita formatado (123.456.789-01) ou só dígitos.
customer_emailstringEmail do cliente cadastrado (match exato, case-insensitive)
external_idstringID do seu sistema (match exato)
limitnumberDefault 50, máx 200
offsetnumberPaginação

Cancelar cobrança

DELETE/v1/charges/{id}

Cancela explicitamente uma charge ainda em pending antes que ela expire sozinha. Útil quando o pedido foi cancelado no seu sistema. A charge passa pra status refunded.

bashcurl -X DELETE https://liquibr.com/v1/charges/pedido-12345 \
  -H "Authorization: Bearer pk_live_xxx"
json{
  "id": "1842",
  "status": "refunded",
  "cancelled_at": "2026-05-30T18:23:14Z"
}
⚠ Só funciona em status pending. Charge já paga, expirada ou cancelada retorna 409 charge_not_pending. Pra estornar charge paga, veja a seção abaixo.

Estorno de cobrança paga (não implementado)

POST/v1/charges/{id}/refund

Endpoint reservado pra estorno de charges com status paid. Atualmente retorna 501 not_implemented — o endpoint existe pra você não precisar refatorar o cliente quando for liberado.

json{
  "error": "not_implemented",
  "message": "Estornos via API ainda não estão disponíveis. Solicite via suporte.",
  "support": {
    "email": "[email protected]",
    "ticket_url": "https://liquibr.com/support"
  },
  "charge_reference": "1842",
  "estimated_response_time": "2 horas úteis"
}

Pra estornar uma charge paga hoje, abra ticket em /support com o charge_reference. Atendimento médio de 2 horas úteis.

Webhook de teste

POST/v1/webhooks/test

Dispara um payload de teste pra sua webhook_url, com assinatura HMAC válida. Use durante setup pra validar sua integração antes de criar charges reais.

bashcurl -X POST https://liquibr.com/v1/webhooks/test \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://seu-sistema.com/webhook/liquibr",
    "event_type": "pix.received"
  }'
json{
  "ok": true,
  "status_code": 200,
  "duration_ms": 142,
  "error": null,
  "signature_sent": "4e8c5f3a2b1c9d8e7f6a...",
  "timestamp_sent": "1717092194",
  "payload_sent": { ... }
}

O payload simulado tem a mesma estrutura do evento real (com dados fictícios). Sua integração processa exatamente como faria com um evento de produção — perfeito pra validar HMAC, timeout e idempotência.

Info da conta

GET/v1/account

Retorna informações do dono da API key. Útil pra validar a chave durante setup da integração.

Saldo

GET/v1/balance
json{
  "gross_received_brl": 12483.50,
  "pending_brl": 347.00,
  "sent_usdt_brl": 11250.00,
  "total_fees_brl": 374.50,
  "net_brl": 12109.00,
  "med": {
    "debt_brl": 0,
    "block_threshold_brl": 200,
    "account_blocked": false
  },
  "account_status": "active"
}

Campos

CampoDescrição
gross_received_brlTotal bruto recebido em PIX (todo histórico)
pending_brlCobranças pending aguardando pagamento
sent_usdt_brlEquivalente BRL do USDT já enviado pra sua carteira
total_fees_brlSoma das taxas da plataforma cobradas até hoje
net_brlSaldo líquido (gross − fees)
med.debt_brlDébito acumulado por MEDs (devoluções PIX) abertos
med.block_threshold_brlLimite a partir do qual a conta é bloqueada por MED
med.account_blockedtrue se conta bloqueada por MED — POST /v1/charges vai retornar 403 account_blocked_med
account_statusactive | suspended | blocked_med

Conversões USDT

Histórico de envios USDT confirmados pelo operador. Cada PIX pago gera uma transaction quando o USDT é enviado on-chain.

GET/v1/transactions?status=confirmed&network=BSC&limit=50
bashcurl "https://liquibr.com/v1/transactions?status=confirmed" \
  -H "Authorization: Bearer pk_live_xxx"

Filtros disponíveis

Query paramTipoDescrição
statusstringpending | sent | confirmed
charge_idstringID interno LiquiBR da cobrança
external_idstringID que você passou na criação da cobrança
networkstringTRON | POLYGON | BSC
limitnumberDefault 50, máx 200
offsetnumberPaginação

Response

json{
  "data": [
    {
      "id": "42",
      "charge_id": "1842",
      "external_id": "pedido-12345",
      "amount_brl": 49.90,
      "usdt_amount": 9.18,
      "rate_brl": 5.22,
      "network": "BSC",
      "to_address": "0x95c4d336087276c291bade26b556562be4d52c7b",
      "tx_hash": "0x482cc3...0137",
      "explorer_url": "https://bscscan.com/tx/0x482cc3...0137",
      "network_fee_usd": 0.30,
      "status": "confirmed",
      "sent_at": "2026-05-06T18:23:10Z",
      "confirmed_at": "2026-05-06T18:23:42Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Pra consultar uma conversão específica:

GET/v1/transactions/{id}

Eventos de Webhook

O LiquiBR envia POST para a URL configurada no dashboard sempre que um evento da sua conta acontece. Cadastre suas URLs em Dashboard → API Keys & Webhooks → Webhooks Configurados.

EventoQuando dispara
pix.receivedCobrança PIX confirmada como paga pelo PSP. Sua integração marca o pedido como pago aqui.
pix.expiredCobrança expira sem pagamento.
pix.medOperador marcou MED (Mecanismo Especial de Devolução PIX — pagador acionou estorno no banco). Payload inclui campo med com motivo. Sua integração deve marcar o pedido como cancelado/estornado e devolver o produto/acesso ao estoque.
usdt.sentUSDT enviado pra carteira do lojista. Payload inclui transaction com tx_hash e network on-chain. Sinal de que o ciclo PIX→USDT foi concluído.
webhook.testDisparo manual via POST /v1/webhooks/test. Use durante setup pra validar sua assinatura HMAC antes de criar cobranças reais.
⚠ Importante: trate pix.med. Se você apenas processa pix.received, vai liberar o produto mas pode receber um MED depois (o pagador contesta no banco). Quando o MED chega, o saldo retorna pro pagador e a venda é cancelada do nosso lado — sua integração precisa reverter o pedido (cancelar acesso, devolver crédito, etc), senão o lojista entrega o produto sem ter recebido o dinheiro.

Exemplo de payload usdt.sent

json{
  "event": "usdt.sent",
  "timestamp": "2026-05-06T18:23:42.521Z",
  "data": {
    "charge": {
      "id": 1842,
      "external_id": "pedido-12345",
      "amount_brl": 49.90,
      "psp_id": "a1b5323c-9e68-4afb-8927-94cdcef7a156"
    },
    "transaction": {
      "id": 42,
      "usdt_amount": 9.18,
      "rate_brl": 5.22,
      "network": "BSC",
      "to_address": "0x95c4d336087276c291bade26b556562be4d52c7b",
      "tx_hash": "0x482cc3...0137",
      "network_fee": 0.30,
      "explorer_url": "https://bscscan.com/tx/0x482cc3...0137",
      "batch": false,
      "status": "sent"
    }
  }
}
Envio em lote: quando o operador envia USDT pra vários PIX de uma vez (1 TX hash cobre N cobranças), cada charge recebe um webhook usdt.sent separado, com transaction.batch: true e transaction.batch_size indicando quantas cobranças compartilham o mesmo tx_hash.

Payload do webhook

Todos os webhooks chegam como POST application/json com a estrutura:

json{
  "event": "pix.received",
  "timestamp": "2026-05-05T18:23:14.521Z",
  "data": {
    "charge": {
      "id": 1842,
      "amount_brl": 49.90,
      "status": "paid",
      "paid_at": "2026-05-05 18:23:10",
      "created_at": "2026-05-05 18:00:00",
      "psp_id": "a1b5323c-9e68-4afb-8927-94cdcef7a156",
      "payer_name": "João Silva",
      "payer_document": "123.456.789-01"
    },
    "client": null
  }
}

Headers enviados

httpContent-Type: application/json
X-Webhook-Signature: 4e8c5f3a2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e
X-Webhook-Timestamp: 1714000000
X-Webhook-Id: 9f2d4e8a-1b3c-4d5e-8f9a-1b2c3d4e5f6a
X-Webhook-Source: pixusdt

Validação HMAC-SHA256 + Timestamp (anti-replay)

O LiquiBR assina o webhook em 2 etapas pra prevenir ataques de replay:

Sua validação deve fazer 2 checks:

  1. O timestamp está dentro de uma janela de tolerância (recomendado: ±5 minutos) — se for fora, rejeita (suspeita de replay)
  2. Recalcula HMAC-SHA256 sobre "{timestamp}.{body}" e compara em tempo constante (use hmac.compare_digest, crypto.timingSafeEqual, etc — nunca ===)

Node.js (Express)

javascriptimport crypto from 'node:crypto';
import express from 'express';

const SECRET = process.env.LIQUIBR_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SEC = 300; // 5 minutos

const app = express();
app.post('/webhook/liquibr',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ts  = req.header('X-Webhook-Timestamp');
    const sig = req.header('X-Webhook-Signature');
    if (!ts || !sig) return res.status(401).send('missing headers');

    // 1) Janela temporal
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(ts, 10)) > TOLERANCE_SEC) {
      return res.status(401).send('timestamp out of tolerance');
    }

    // 2) Recalcula HMAC sobre "timestamp.body"
    const expected = crypto.createHmac('sha256', SECRET)
      .update(`${ts}.${req.body.toString()}`)
      .digest('hex');

    // 3) Compara em tempo constante
    const ok = sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.status(401).send('invalid signature');

    const event = JSON.parse(req.body.toString());
    if (event.event === 'pix.received') {
      console.log('PIX confirmado:', event.data.charge.external_id);
    }
    res.status(200).json({ ok: true });
  }
);

PHP (Laravel)

php$secret    = env('LIQUIBR_WEBHOOK_SECRET');
$tolerance = 300; // 5 minutos

$body = $request->getContent();
$ts   = $request->header('X-Webhook-Timestamp');
$sig  = $request->header('X-Webhook-Signature');

if (!$ts || !$sig) abort(401, 'missing headers');

// 1) Janela temporal
if (abs(time() - intval($ts)) > $tolerance) {
    abort(401, 'timestamp out of tolerance');
}

// 2) Recalcula HMAC sobre "timestamp.body"
$expected = hash_hmac('sha256', $ts . '.' . $body, $secret);

// 3) Compara em tempo constante
if (!hash_equals($expected, $sig)) {
    abort(401, 'invalid signature');
}

$event = json_decode($body, true);
if ($event['event'] === 'pix.received') {
    Order::where('external_id', $event['data']['charge']['external_id'])
         ->update(['status' => 'paid']);
}

Python (FastAPI)

pythonimport hmac, hashlib, os, json, time
from fastapi import FastAPI, Request, HTTPException

SECRET = os.environ['LIQUIBR_WEBHOOK_SECRET'].encode()
TOLERANCE = 300  # 5 minutos
app = FastAPI()

@app.post('/webhook/liquibr')
async def webhook(request: Request):
    body = await request.body()
    ts   = request.headers.get('x-webhook-timestamp', '')
    sig  = request.headers.get('x-webhook-signature', '')
    if not ts or not sig:
        raise HTTPException(401, 'missing headers')

    # 1) Janela temporal
    if abs(int(time.time()) - int(ts)) > TOLERANCE:
        raise HTTPException(401, 'timestamp out of tolerance')

    # 2) Recalcula HMAC sobre "timestamp.body"
    payload  = f"{ts}.{body.decode()}".encode()
    expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()

    # 3) Compara em tempo constante
    if not hmac.compare_digest(sig, expected):
        raise HTTPException(401, 'invalid signature')

    event = json.loads(body)
    if event['event'] == 'pix.received':
        ...  # processa pagamento
    return {'ok': True}
⚠ Mudança v1.1: a partir de 2026-05-06, a assinatura passou a incluir o timestamp pra prevenir ataques de replay. Se você implementou validação no formato antigo (HMAC só sobre o body), atualize seu código com o novo formato — o header X-Webhook-Timestamp agora é obrigatório.
Deduplicação via X-Webhook-Id: cada disparo tem um UUID único nesse header. Mesmo evento entregue duas vezes via retry chega com o mesmo id. Recomendamos sua integração armazenar os ids processados (numa cache curta, ex: 24h) e ignorar duplicatas. Isso é seguro mesmo se o webhook chegar antes do POST /v1/charges retornar (race condition rara).
Retentativas: se sua URL retornar status diferente de 2xx, o LiquiBR tenta novamente até 3 vezes com backoff linear (1s entre 1ª e 2ª, 2s entre 2ª e 3ª). Após 3 falhas, o webhook é descartado e contabilizado como falha no dashboard. Timeout: 8 segundos por tentativa.
Testando antes de produção: chame POST /v1/webhooks/test com sua webhook_url e o tipo de evento que quer simular. Recebe um payload com assinatura HMAC válida — sua integração processa exatamente como faria com um evento real. Response do endpoint retorna status code que seu servidor devolveu, latência e (se houve) erro de rede — útil pra você cruzar com os logs do seu lado.

Auditoria de disparos

Pra debugar webhooks que falharam ou consultar o histórico de entregas:

GET/v1/webhooks/deliveries?event=pix.received&ok=false&limit=20

Retorna as últimas tentativas (até 3 por evento), incluindo body enviado, status code retornado pelo seu servidor, latência, erro de rede (se houver) e os primeiros 4KB da resposta.

Query paramDescrição
eventFiltra por tipo (pix.received | pix.expired | pix.med | usdt.sent)
oktrue = só sucessos 2xx; false = só falhas
charge_idFiltra por charge específica
limitDefault 50, máx 200
offsetPaginação

Rotação do Webhook Secret

Quando você regenera sua API key (botão 🔄 REGENERAR no dashboard), o webhook_secret também é trocado. A revogação é imediata — não há grace period no nosso lado:

Estratégia recomendada do lado cliente pra rotação sem perder webhooks:

  1. Configure 2 variáveis de ambiente: LIQUIBR_WEBHOOK_SECRET (atual) e LIQUIBR_WEBHOOK_SECRET_PREV (anterior)
  2. No handler do webhook, valide a assinatura contra ambos os secrets — aceita se qualquer um bater
  3. Quando rotacionar: pegue o secret novo no dashboard, copie o atual pra _PREV, coloque o novo em _SECRET, faça reload da app
  4. Após ~5 minutos (janela de webhooks em voo + retries), pode descartar o _PREV
javascript// Validação aceitando 2 secrets durante janela de rotação
const SECRETS = [
  process.env.LIQUIBR_WEBHOOK_SECRET,
  process.env.LIQUIBR_WEBHOOK_SECRET_PREV
].filter(Boolean);

function isValid(ts, body, sig) {
  return SECRETS.some(secret => {
    const expected = crypto.createHmac('sha256', secret)
      .update(`${ts}.${body}`)
      .digest('hex');
    return sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}
⚠ Quando rotacionar? Em incidentes de segurança (suspeita de vazamento), no offboarding de funcionário com acesso, ou periodicamente (a cada 90-180 dias é boa prática). Não regenere por costume — é operação que exige cuidado e deploy coordenado no seu lado.

Fluxo de integração CRM (end-to-end)

Esse é o fluxo típico pra integrar um CRM/sistema de gestão de pedidos com o LiquiBR. Cobre criação da cobrança, confirmação de pagamento via webhook, e reversão automática em caso de MED.

1. Setup inicial

  1. Gere a API key e o webhook secret em Dashboard → API Keys & Webhooks. Guarde os dois no seu vault (env vars). A API key começa com pk_live_, o secret com whsec_.
  2. Confirme que sua carteira USDT está cadastrada chamando GET /v1/account — confira que usdt_wallet.configured === true. Sem isso, charges criam normalmente mas o USDT fica pendente até o cadastro.
  3. Configure sua URL de webhook em Dashboard → Webhooks ou passe webhook_url inline em cada charge.
  4. Antes de produção: chame POST /v1/webhooks/test com sua URL e valide que seu validador HMAC funciona. Veja a resposta com signature_sent e timestamp_sent pra cross-check do seu lado.

2. Criar cobrança quando o cliente finaliza o pedido

Use o ID interno do seu pedido como external_id — isso te dá idempotência grátis e facilita reconciliação. Se o cliente clica "Pagar" duas vezes seguidas, você recebe a mesma charge (HTTP 200 + Idempotent-Replay: true) em vez de duplicar.

javascript// Quando o cliente confirma o pedido no CRM
async function gerarCobrancaPix(pedido) {
  const resp = await fetch('https://liquibr.com/v1/charges', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.LIQUIBR_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      amount: pedido.valor_brl,
      external_id: `pedido-${pedido.id}`,    // chave de idempotência
      description: `${pedido.produto} - ${pedido.cliente.nome}`,
      webhook_url: 'https://seu-crm.com/webhooks/liquibr',
      customer: {
        name:     pedido.cliente.nome,
        email:    pedido.cliente.email,
        document: pedido.cliente.cpf
      }
    })
  });
  const charge = await resp.json();

  // 200 = idempotent_replay (cliente clicou 2x); 201 = nova charge
  // Os 2 retornam a mesma estrutura — pode usar igual

  // Mostra QR Code pro cliente pagar
  return {
    qr_code_image: charge.pix.qr_code_image_url,  // <img src=...>
    copia_cola:    charge.pix.copy_paste,
    expira_em:     charge.expires_at,
    liquibr_id:    charge.id
  };
}

3. Receber confirmação de pagamento via webhook

Quando o pagador paga, o LiquiBR envia pix.received pra sua URL. Valide a assinatura e marque o pedido como pago.

javascript// Express/Hono — endpoint /webhooks/liquibr
app.post('/webhooks/liquibr', express.raw({ type: 'application/json' }), async (req, res) => {
  // 1. Valida HMAC (ver seção "Validação HMAC")
  if (!validarAssinatura(req)) return res.status(401).send('invalid sig');

  const evento = JSON.parse(req.body.toString());
  const externalId = evento.data.charge.external_id;       // "pedido-12345"
  const pedidoId = externalId.replace('pedido-', '');

  // 2. Idempotência: ignora se já processamos esse webhook (via X-Webhook-Id)
  const whId = req.header('X-Webhook-Id');
  if (await jaProcessado(whId)) return res.status(200).json({ ok: true });

  // 3. Roteia por evento
  switch (evento.event) {
    case 'pix.received':
      await Pedido.update(pedidoId, {
        status: 'pago',
        pago_em: evento.data.charge.paid_at,
        psp_id:  evento.data.charge.psp_id
      });
      await liberarProduto(pedidoId);
      break;

    case 'pix.expired':
      await Pedido.update(pedidoId, { status: 'expirado' });
      break;

    case 'pix.med':
      // CRÍTICO: pagador acionou MED, dinheiro volta pra ele
      await Pedido.update(pedidoId, {
        status:     'estornado',
        med_em:     evento.data.med.marked_at,
        med_motivo: evento.data.med.reason
      });
      await reverterProduto(pedidoId);  // cancela acesso/devolve estoque
      break;

    case 'usdt.sent':
      // USDT chegou na carteira do lojista (informativo)
      await Pedido.update(pedidoId, {
        usdt_tx_hash: evento.data.transaction.tx_hash,
        usdt_rede:    evento.data.transaction.network
      });
      break;
  }

  await marcarProcessado(whId);
  res.status(200).json({ ok: true });
});

4. Reconciliação periódica (cinto + suspensório)

Mesmo com webhooks robustos, vale fazer reconciliação diária pra cobrir casos extremos (servidor seu fora do ar quando o webhook tentou as 3 vezes, MED disparado em janela de manutenção, etc):

javascript// Rotina diária (cron 03:00) — varre o dia anterior
async function reconciliarDiario() {
  const ontem = new Date(Date.now() - 86400000).toISOString().slice(0, 10);

  const { data: charges } = await fetch(
    `https://liquibr.com/v1/charges?date_from=${ontem}&date_to=${ontem}&limit=200`,
    { headers: { 'Authorization': `Bearer ${process.env.LIQUIBR_API_KEY}` } }
  ).then(r => r.json());

  for (const c of charges) {
    const pedidoId = c.external_id?.replace('pedido-', '');
    if (!pedidoId) continue;

    const pedido = await Pedido.find(pedidoId);
    if (!pedido) continue;

    // Detecta divergências entre LiquiBR e seu CRM
    if (c.status === 'paid' && pedido.status !== 'pago') {
      console.warn(`PED ${pedidoId}: LiquiBR marcou pago, CRM não recebeu webhook`);
      await Pedido.update(pedidoId, { status: 'pago', pago_em: c.paid_at });
    }
    if (c.status === 'refunded' && pedido.status === 'pago') {
      console.warn(`PED ${pedidoId}: charge refunded mas CRM ainda pago — MED não processado`);
      await reverterProduto(pedidoId);
    }
  }
}

5. Cancelamento manual

Quando o cliente cancela o pedido no CRM antes de pagar, cancele a charge correspondente pra não deixar pending no painel:

javascriptasync function cancelarPedido(pedidoId) {
  await fetch(`https://liquibr.com/v1/charges/pedido-${pedidoId}`, {
    method: 'DELETE',
    headers: { 'Authorization': `Bearer ${process.env.LIQUIBR_API_KEY}` }
  });
  // Charge fica em refunded. Se já estava paga, retorna 409 — aí abra ticket pra estorno.
}
Resumo dos invariantes:
  • Sempre passe external_id (= ID do pedido no seu sistema). Sem isso, não dá pra idempotência nem reconciliação.
  • Sempre trate os 4 eventos: pix.received, pix.expired, pix.med, usdt.sent. Faltar qualquer um deixa pedidos em estado inconsistente.
  • Sempre deduplique via X-Webhook-Id — retries do LiquiBR enviam o mesmo id.
  • Sempre rode reconciliação diária — webhooks falham silenciosamente quando seu servidor cai.

SDKs / Bibliotecas

Atualmente não fornecemos SDKs oficiais — a API é pequena o suficiente pra ser consumida com qualquer client HTTP (axios, requests, Guzzle, etc.). SDKs em Node.js, PHP e Python estão no roadmap.

Changelog

Dúvidas? Abra um ticket em /support ou mande email para [email protected].

LiquiBR é uma solução de pagamento que converte PIX em USDT (stablecoin Tether). Não somos afiliados, parceiros ou licenciados pela Tether Limited. "USDT" refere-se ao ativo de liquidação utilizado pela plataforma.