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.
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
Health check
Endpoint público pra monitoramento (UptimeRobot, statuspage etc):
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
| Rede | Token | Taxa de rede aprox. | Tempo de confirmação |
|---|---|---|---|
| TRON | USDT (TRC-20) | ~$1 USD | ~30 segundos |
| POLYGON | USDT (Polygon PoS) | ~$0.05 USD | ~5 segundos |
| BSC | USDT (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
}
}
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": { ... }
}
| Status | Code | Significado |
|---|---|---|
| 400 | validation_error | Body inválido ou campo faltando |
| 400 | amount_above_limit | Valor da cobrança ultrapassa o max_charge_brl da sua conta. Response inclui max_charge_brl com seu limite atual. |
| 401 | unauthorized | API key ausente, inválida ou revogada |
| 403 | profile_incomplete | Perfil do lojista não tem nome legal + WhatsApp cadastrados. Complete em Dashboard → Meu Perfil → Dados de Contato. |
| 403 | account_blocked_med | Conta bloqueada por débito de MED (devolução PIX) acima do limite. Anexe documentação em Dashboard → Compliance. |
| 403 | account_suspended | Conta suspensa pelo operador. Contate o suporte. |
| 404 | not_found | Cobrança ou recurso não existe |
| 409 | idempotency_conflict | external_id (ou header Idempotency-Key) já foi usado nas últimas 24h com valor diferente. Use external_id novo ou aguarde a janela expirar. |
| 409 | charge_not_pending | Tentativa de cancelar (DELETE /v1/charges/:id) charge que já está paid, expired ou refunded. |
| 429 | rate_limit_exceeded | Excedeu 100 requisições/minuto por IP. Header Retry-After indica em quantos segundos pode tentar de novo. |
| 501 | not_implemented | Endpoint reservado mas ainda não disponível (ex: POST /v1/charges/:id/refund). Response inclui contato de suporte pra operação manual. |
| 502 | psp_error | Erro no PSP que processa o PIX |
| 500 | internal_error | Erro interno do servidor |
Criar cobrança PIX
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
| Campo | Tipo | Descrição |
|---|---|---|
| amount obrigatório | number | Valor 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_id | string | ID do seu sistema (ex: ID do pedido). Funciona como chave de idempotência (janela de 24h — veja seção "Idempotência" abaixo). |
| description | string | Descrição livre (até 255 chars) |
| expires_in_minutes | number | Tempo 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_url | string (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.name | string | Nome do pagador |
| customer.email | string | Email do pagador |
| customer.document | string | CPF/CNPJ do pagador |
Headers opcionais
| Header | Descrição |
|---|---|
Idempotency-Key | Alternativa 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:
- Mesma chave + mesmo
amountnas últimas 24h: retorna a charge existente com HTTP 200 (não 201), headerIdempotent-Replay: true, e campoidempotent_replay: trueno body. Não cobra duas vezes. - Mesma chave +
amountdiferente nas últimas 24h: retorna HTTP 409idempotency_conflict. Use outra chave ou aguarde 24h. - Sem chave: sempre cria nova charge (sem proteção contra duplicação).
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 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.
pix.qr_code_base64epix.qr_code_image_url: strings prontas pra<img src=>(data URIdata: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}`} />.
<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
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
| Status | Significado |
|---|---|
| pending | Aguardando pagamento |
| paid | Pago e confirmado pelo PSP |
| expired | QR code expirou sem pagamento |
| failed | Erro no processamento |
| refunded | Estornado |
Listar cobranças
bashcurl "https://liquibr.com/v1/charges?status=paid&limit=50" \
-H "Authorization: Bearer pk_live_xxx"
Filtros disponíveis
| Query param | Tipo | Descrição |
|---|---|---|
status | string | pending | paid | expired | failed | refunded |
date_from | date | YYYY-MM-DD. Filtra created_at >= esta data (inclusive). Use pra reconciliar dia/período. |
date_to | date | YYYY-MM-DD. Filtra created_at <= fim deste dia (inclusive). |
payer_document | string | CPF/CNPJ do pagador. Aceita formatado (123.456.789-01) ou só dígitos. |
customer_email | string | Email do cliente cadastrado (match exato, case-insensitive) |
external_id | string | ID do seu sistema (match exato) |
limit | number | Default 50, máx 200 |
offset | number | Paginação |
Cancelar cobrança
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"
}
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)
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
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
Retorna informações do dono da API key. Útil pra validar a chave durante setup da integração.
Saldo
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
| Campo | Descrição |
|---|---|
| gross_received_brl | Total bruto recebido em PIX (todo histórico) |
| pending_brl | Cobranças pending aguardando pagamento |
| sent_usdt_brl | Equivalente BRL do USDT já enviado pra sua carteira |
| total_fees_brl | Soma das taxas da plataforma cobradas até hoje |
| net_brl | Saldo líquido (gross − fees) |
| med.debt_brl | Débito acumulado por MEDs (devoluções PIX) abertos |
| med.block_threshold_brl | Limite a partir do qual a conta é bloqueada por MED |
| med.account_blocked | true se conta bloqueada por MED — POST /v1/charges vai retornar 403 account_blocked_med |
| account_status | active | 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.
bashcurl "https://liquibr.com/v1/transactions?status=confirmed" \
-H "Authorization: Bearer pk_live_xxx"
Filtros disponíveis
| Query param | Tipo | Descrição |
|---|---|---|
| status | string | pending | sent | confirmed |
| charge_id | string | ID interno LiquiBR da cobrança |
| external_id | string | ID que você passou na criação da cobrança |
| network | string | TRON | POLYGON | BSC |
| limit | number | Default 50, máx 200 |
| offset | number | Paginaçã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:
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.
| Evento | Quando dispara |
|---|---|
pix.received | Cobrança PIX confirmada como paga pelo PSP. Sua integração marca o pedido como pago aqui. |
pix.expired | Cobrança expira sem pagamento. |
pix.med | Operador 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.sent | USDT 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.test | Disparo manual via POST /v1/webhooks/test. Use durante setup pra validar sua assinatura HMAC antes de criar cobranças reais. |
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"
}
}
}
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:
X-Webhook-Timestamp— Unix timestamp (segundos) de quando o webhook foi assinadoX-Webhook-Signature— HMAC-SHA256 de"{timestamp}.{body_bruto}"usando seuwebhook_secretcomo chave
Sua validação deve fazer 2 checks:
- O
timestampestá dentro de uma janela de tolerância (recomendado: ±5 minutos) — se for fora, rejeita (suspeita de replay) - Recalcula HMAC-SHA256 sobre
"{timestamp}.{body}"e compara em tempo constante (usehmac.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}
X-Webhook-Timestamp agora é obrigatório.
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).
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:
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 param | Descrição |
|---|---|
event | Filtra por tipo (pix.received | pix.expired | pix.med | usdt.sent) |
ok | true = só sucessos 2xx; false = só falhas |
charge_id | Filtra por charge específica |
limit | Default 50, máx 200 |
offset | Paginaçã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:
- O secret antigo é marcado como
revokedatomicamente no momento da regeneração - Webhooks já em voo (disparados antes do clique) podem chegar com a assinatura do secret antigo, dependendo da latência
- Webhooks disparados depois usam o novo secret
Estratégia recomendada do lado cliente pra rotação sem perder webhooks:
- Configure 2 variáveis de ambiente:
LIQUIBR_WEBHOOK_SECRET(atual) eLIQUIBR_WEBHOOK_SECRET_PREV(anterior) - No handler do webhook, valide a assinatura contra ambos os secrets — aceita se qualquer um bater
- Quando rotacionar: pegue o secret novo no dashboard, copie o atual pra
_PREV, coloque o novo em_SECRET, faça reload da app - 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));
});
}
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
- 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 comwhsec_. - Confirme que sua carteira USDT está cadastrada chamando
GET /v1/account— confira queusdt_wallet.configured === true. Sem isso, charges criam normalmente mas o USDT fica pendente até o cadastro. - Configure sua URL de webhook em Dashboard → Webhooks ou passe
webhook_urlinline em cada charge. - Antes de produção: chame
POST /v1/webhooks/testcom sua URL e valide que seu validador HMAC funciona. Veja a resposta comsignature_sentetimestamp_sentpra 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.
}
- 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
- v1.3 · 2026-05-30
- Novos endpoints:
DELETE /v1/charges/{id}(cancelar pending),POST /v1/webhooks/test(validar integração),GET /v1/webhooks/deliveries(auditoria). - Endpoint
POST /v1/charges/{id}/refunddocumentado (retorna 501 — estornos seguem via suporte por enquanto). GET /v1/charges: novos filtrosdate_from,date_to,payer_document,customer_emailpra reconciliação.- Suporte ao header
Idempotency-Key(padrão Stripe-like) como alternativa aoexternal_idno body. - Webhook
pix.meddocumentado — dispara quando o operador marca devolução PIX. max_charge_brldefault subiu de R$ 200 → R$ 500.- Domínio principal migrado de
liquibr.com→liquibr.com. URL antiga ainda resolve mas o canônico é o .trade. - Headers
X-Webhook-SourceeX-Webhook-Iddocumentados (já eram enviados desde v1.1 — agora cobertos formalmente). - Rate limit corrigido na doc: 100 requisições/minuto por IP (a doc anterior mencionava 50k/24h global, mas isso nunca foi implementado).
- Novos endpoints:
- v1.2 · 2026-05-15
- Taxa da plataforma simplificada: 3% + R$ 0,55 fixo aplicados em TODAS as cobranças (antes a taxa fixa só caía em PIX abaixo de R$ 50). Campos
fees.percentage_fee_brl,fees.fixed_fee_brlefees.total_fee_brlrefletem a nova fórmula. POST /v1/charges: response agora tem 3 campos pra QR Code —pix.qr_code_base64epix.qr_code_image_url(string pronta pra<img src=>) +pix.qr_code_base64_raw(base64 puro pra montar manual).- Limite por cobrança agora é per-conta (
max_charge_brl, default R$ 200). Erro 400amount_above_limitretorna o limite atual. - Gate de perfil:
POST /v1/chargesexige nome legal + WhatsApp cadastrados — retorna 403profile_incompletese incompleto. - Erro 403
account_blocked_med: se conta bloqueada por débito de MED, novas cobranças são rejeitadas. Estado consultável viaGET /v1/balance.med. - Endpoint novo:
GET /v1/transactionseGET /v1/transactions/{id}— histórico de envios USDT comtx_hash,network,explorer_url. GET /v1/balance: response expandido comsent_usdt_brl,med.*eaccount_status.
- Taxa da plataforma simplificada: 3% + R$ 0,55 fixo aplicados em TODAS as cobranças (antes a taxa fixa só caía em PIX abaixo de R$ 50). Campos
- v1.1 · 2026-05-06
- Webhook signature agora inclui
X-Webhook-Timestamp(HMAC sobre"{timestamp}.{body}", anti-replay com janela ±5min). - Novo evento
usdt.sentcom TX hash on-chain.
- Webhook signature agora inclui
- v1.0 · lançamento
- Endpoints
/v1/charges,/v1/account,/v1/balance. - Webhooks
pix.receivedepix.expiredcom assinatura HMAC-SHA256.
- Endpoints
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.