LunarPay API v2 — Guia de Integração

Pix + Subcarteiras + Cripto + OTC/P2P (JWT + IP allowlist + Webhook Global)

Visão geral

Base: https://SEU-DOMINIO.com/api

  • Todas as rotas são POST.
  • Envie Content-Type: application/json.
  • Autenticação: Basic apenas no token; Bearer JWT nas demais rotas.

Credenciais e IP (IMPORTANTE)

  • CLIENT_ID e CLIENT_SECRET são obtidos dentro da sua plataforma/painel (usuário integrador).
  • O IP do servidor que chama a API precisa estar permitido na plataforma (allowlist), senão a API retorna 403.

1) Autenticação (JWT)

1.1 Gerar token

POST /v2/oauth/token — header Authorization: Basic base64(client_id:client_secret)

curl -sS -X POST "https://SEU-DOMINIO.com/api/v2/oauth/token" \
  -H "Authorization: Basic $(printf '%s' 'CLIENT_ID:CLIENT_SECRET' | base64)"

Resposta:

{ "access_token": "...jwt...", "expires_in": 1800 }
1.2 Usar token

Em todas as rotas /v2/* (exceto /v2/oauth/token):

  • Header: Authorization: Bearer <access_token>

Exemplos por linguagem (Ruby / PHP / Python / Node.js)

Todos os exemplos seguem o mesmo padrão: token com Basic em /v2/oauth/token, chamadas com Bearer, status com wait=1 e webhook global (quando configurado no painel).

De onde vêm as credenciais?
client_id e client_secret você pega no seu painel/plataforma. E lembre de cadastrar o IP do seu servidor na allowlist do usuário integrador.
Ruby (net/http)
require 'net/http'
require 'json'
require 'base64'

BASE_URL = 'https://SEU-DOMINIO.com/api'
CLIENT_ID = ENV.fetch('LUNARPAY_CLIENT_ID')
CLIENT_SECRET = ENV.fetch('LUNARPAY_CLIENT_SECRET')

def post_json(url, headers, body)
  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = (uri.scheme == 'https')
  req = Net::HTTP::Post.new(uri)
  headers.each { |k, v| req[k] = v }
  req.body = body.nil? ? '' : JSON.generate(body)
  res = http.request(req)
  [res.code.to_i, res.body]
end

basic = Base64.strict_encode64("#{CLIENT_ID}:#{CLIENT_SECRET}")
st, body = post_json("#{BASE_URL}/v2/oauth/token", { 'Authorization' => "Basic #{basic}", 'Content-Type' => 'application/json' }, nil)
token = JSON.parse(body).fetch('access_token')

payload = { amount: 10.50, payerName: 'João da Silva', payerDocument: '123.456.789-09', payerQuestion: 'Pedido #123', external_id: 'pedido-123', expires_in_minutes: 30 }
st, body = post_json("#{BASE_URL}/v2/pix/qrcode", { 'Authorization' => "Bearer #{token}", 'Content-Type' => 'application/json' }, payload)
puts body

# Cripto: obter subcarteira
st, body = post_json("#{BASE_URL}/v2/crypto/wallets", { 'Authorization' => "Bearer #{token}", 'Content-Type' => 'application/json' }, { coin: 'USDT' })
puts body

# P2P: cotação
st, body = post_json("#{BASE_URL}/v2/p2p/quote", { 'Authorization' => "Bearer #{token}", 'Content-Type' => 'application/json' }, { from_coin: 'BRL', to_coin: 'USDT', amount: '100' })
puts body
PHP (cURL)

Script executável pronto: dev/tef.php

<?php
$baseUrl = 'https://SEU-DOMINIO.com/api';
$clientId = getenv('LUNARPAY_CLIENT_ID');
$clientSecret = getenv('LUNARPAY_CLIENT_SECRET');

$ch = curl_init();
curl_setopt_array($ch, [
  CURLOPT_URL => $baseUrl . '/v2/oauth/token',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Basic ' . base64_encode($clientId . ':' . $clientSecret),
    'Content-Type: application/json',
  ],
]);
$tokenResp = curl_exec($ch);
curl_close($ch);

$token = (json_decode($tokenResp, true)['access_token'] ?? null);

$payload = [
  'amount' => 10.50,
  'payerName' => 'João da Silva',
  'payerDocument' => '123.456.789-09',
  'payerQuestion' => 'Pedido #123',
  'external_id' => 'pedido-123',
  'expires_in_minutes' => 30,
];

$ch = curl_init();
curl_setopt_array($ch, [
  CURLOPT_URL => $baseUrl . '/v2/pix/qrcode',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode($payload),
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
  ],
]);
echo curl_exec($ch);
curl_close($ch);

$ch = curl_init();
curl_setopt_array($ch, [
  CURLOPT_URL => $baseUrl . '/v2/crypto/wallets',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode(['coin' => 'USDT']),
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
  ],
]);
echo curl_exec($ch);
curl_close($ch);

$ch = curl_init();
curl_setopt_array($ch, [
  CURLOPT_URL => $baseUrl . '/v2/p2p/quote',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode(['from_coin' => 'BRL', 'to_coin' => 'USDT', 'amount' => '100']),
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
  ],
]);
echo curl_exec($ch);
curl_close($ch);
?>
Python (urllib — padrão)
import base64
import json
import os
import urllib.request

BASE_URL = 'https://SEU-DOMINIO.com/api'
CLIENT_ID = os.environ['LUNARPAY_CLIENT_ID']
CLIENT_SECRET = os.environ['LUNARPAY_CLIENT_SECRET']

def post_json(path, headers=None, data=None):
    url = BASE_URL + path
    h = {'Content-Type': 'application/json'}
    if headers:
        h.update(headers)
    payload = b'' if data is None else json.dumps(data).encode('utf-8')
    req = urllib.request.Request(url, data=payload, headers=h, method='POST')
    with urllib.request.urlopen(req, timeout=40) as resp:
        return resp.status, resp.read().decode('utf-8')

basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
st, body = post_json('/v2/oauth/token', headers={'Authorization': f'Basic {basic}'}, data=None)
token = json.loads(body)['access_token']

payload = {"amount": 10.50, "payerName": "João da Silva", "payerDocument": "123.456.789-09", "payerQuestion": "Pedido #123", "external_id": "pedido-123", "expires_in_minutes": 30}
st, body = post_json('/v2/pix/qrcode', headers={'Authorization': f'Bearer {token}'}, data=payload)
print(body)

# Cripto: obter subcarteira
st, body = post_json('/v2/crypto/wallets', headers={'Authorization': f'Bearer {token}'}, data={"coin": "USDT"})
print(body)

# P2P: cotação
st, body = post_json('/v2/p2p/quote', headers={'Authorization': f'Bearer {token}'}, data={"from_coin": "BRL", "to_coin": "USDT", "amount": "100"})
print(body)
Node.js (fetch — Node 18+)
const baseUrl = 'https://SEU-DOMINIO.com/api';
const clientId = process.env.LUNARPAY_CLIENT_ID;
const clientSecret = process.env.LUNARPAY_CLIENT_SECRET;

async function postJson(path, headers = {}, body = null) {
  const res = await fetch(baseUrl + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...headers },
    body: body === null ? '' : JSON.stringify(body),
  });
  return { status: res.status, text: await res.text() };
}

(async () => {
  const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
  const tokenResp = await postJson('/v2/oauth/token', { Authorization: `Basic ${basic}` }, null);
  const token = JSON.parse(tokenResp.text).access_token;

  const payload = { amount: 10.5, payerName: 'João da Silva', payerDocument: '123.456.789-09', payerQuestion: 'Pedido #123', external_id: 'pedido-123', expires_in_minutes: 30 };
  const pixResp = await postJson('/v2/pix/qrcode', { Authorization: `Bearer ${token}` }, payload);
  console.log(pixResp.text);

  const walletsResp = await postJson('/v2/crypto/wallets', { Authorization: `Bearer ${token}` }, { coin: 'USDT' });
  console.log(walletsResp.text);

  const quoteResp = await postJson('/v2/p2p/quote', { Authorization: `Bearer ${token}` }, { from_coin: 'BRL', to_coin: 'USDT', amount: '100' });
  console.log(quoteResp.text);
})();

2) IP allowlist (segurança)

Se existirem entradas na tabela ip_whitelist para o seu user_id, a API bloqueia chamadas (token e Bearer) fora desses IPs. Em Hostinger direto (sem proxy), o IP considerado é REMOTE_ADDR.

3) Pix — Depósito (QR Code)

Você pode criar o depósito via checkout/pix (checkout) ou pix/qrcode (direto). Ambos criam uma transação DEPOSIT com status=PENDING. O brcode é opcional (configurado no banco em config.pix_receiver_*, sem PIX_KEY na env). Se não estiver configurado, acompanhe confirmação por status/webhook.

3.1 Criar QRCode

POST /v2/pix/qrcode

Body:

{
  "amount": 10.50,
  "payerName": "João da Silva",
  "payerDocument": "123.456.789-09",
  "payerQuestion": "Pedido #123",
  "external_id": "pedido-123",
  "expires_in_minutes": 30
}

Resposta:

{
  "status": "PENDING",
  "transactionId": "...",
  "external_id": "pedido-123",
  "message": "Depósito criado. Faça o pagamento via Pix e aguarde confirmação.",
  "amount": "10.50",
  "tax": "0.50",
  "net_amount": "10.00",
  "expires_at": "2026-01-11 12:34:56"
}
  • external_id deve ser único.
  • Webhook é global (ver seção 6.1).

4) Pix — Saque (Payment)

POST /v2/pix/payment

Importante (chave PIX do saque): creditParty.key é a chave PIX de destino (para onde o saque será enviado).
EVP é a chave aleatória do Pix (gerada pelo banco/PSP; geralmente no formato UUID).
Você pode enviar creditParty.keyType (opcional) para indicar o tipo da chave (EMAIL, TELEFONE, CPF, CNPJ, EVP).

Body:

{
  "amount": 25.00,
  "description": "Saque do usuário",
  "debtorName": "João da Silva",
  "debtorDocument": "123.456.789-09",
  "creditParty": { "key": "CHAVE_PIX_DESTINO", "keyType": "EVP" }
}

Resposta:

{
  "status": "PENDING",
  "transactionId": "...",
  "external_id": "...",
  "amount": "25.00",
  "tax": "0.50",
  "total_debit": "25.50"
}

5) Status Pix por ID (com wait/polling)

POST /v2/transactions/status

Body (use um deles):

{ "transactionId": "...", "wait": 1, "timeout_ms": 30000, "interval_ms": 1000 }
{ "external_id": "pedido-123", "wait": 1 }

Resposta:

{
  "transactionId": "...",
  "external_id": "pedido-123",
  "type": "DEPOSIT",
  "status": "PENDING|PAID|CANCELLED|EXPIRED|REVERSED",
  "amount": "10.50",
  "tax": "0.50",
  "net_amount": "10.00",
  "expires_at": "...",
  "confirmed_date": null,
  "created_at": "..."
}

wait=1 segura a requisição até sair de PENDING (ou até timeout_ms).

6) Webhook

Webhook é GLOBAL (por usuário) e aceita apenas HTTPS. Quando a API detectar mudança de status durante a reconciliação (polling), ela faz um POST JSON best-effort para a URL global configurada.

Payload (exemplo):

{
  "transactionType": "PIX",
  "event": "STATUS_CHANGED",
  "transactionId": "...",
  "external_id": "pedido-123",
  "type": "DEPOSIT",
  "status": "PAID",
  "amount": 10.5,
  "tax": 0.5,
  "net_amount": 10.0,
  "confirmed_date": "2026-01-11 12:35:10",
  "created_at": "2026-01-11 12:05:00",
  "end2end": "..."
}
  • Envio “best-effort” (sem retry garantido) e não quebra a requisição principal.
  • Dispara quando você chama status e a API detecta transição.
6.1) Webhook global (Pix + Cripto + OTC/P2P)

Pré-requisito: tabela webhook_subscriptions (já no dump api/testelunarpay07299.sql).

A URL do webhook global é cadastrada no painel/plataforma (fluxo de administrador).
6.1.1) Consultar webhook global

POST /v2/webhooks/get

{ "urls": ["https://seu-sistema.com/webhook/lunarpay"] }
6.1.3) Quando dispara
  • Pix: quando /v2/transactions/status detecta transição.
  • Cripto depósito: quando /v2/crypto/deposits/status detecta confirmação.
  • Cripto saque: quando /v2/crypto/transactions/status detecta mudança.
  • OTC/P2P: quando /v2/p2p/status detecta mudança derivada.

Importante: a API não envia webhooks em background; dispara quando você consulta status.

7) Cripto — Moedas, Subcarteiras e Depósitos

7.1 Listar moedas

POST /v2/crypto/coins

{ "coins": ["BTC","ETH","USDT", "..."] }
7.2 Obter endereços (subcarteiras)

POST /v2/crypto/wallets

Body (todas):

{}

Body (uma moeda):

{ "coin": "USDT" }

Resposta:

{
  "wallets": [
    { "coin": "USDT", "network": "TRC20", "address": "..." }
  ]
}
  • Evita reutilizar o mesmo endereço para usuários diferentes.
  • network é fixa por moeda (conforme configuração interna).
7.3 Verificar depósito cripto (por id/hash/address)

POST /v2/crypto/deposits/status

{ "coin": "USDT", "address": "...", "wait": 1, "timeout_ms": 30000, "interval_ms": 1000 }

Resposta:

{
  "coin": "USDT",
  "status": "PENDING|CONFIRMED|...",
  "confirmed": true,
  "provider_ref": "...",
  "amount": "12.34",
  "network": "trc20",
  "address": "...",
  "hash": "..."
}

8) Cripto — Saque e Status

8.1 Saque cripto

POST /v2/crypto/withdraw

{
  "coin": "USDT",
  "address": "...",
  "amount": "10.0",
  "network": "TRC20",
  "pin": "1234"
}

Resposta:

{
  "status": "PENDING",
  "coin": "USDT",
  "network": "trc20",
  "amount": "10.00000000",
  "platform_fee": "0.10000000",
  "total_debit": "10.10000000",
  "provider_ref": "..."
}
8.2 Status do saque cripto (com wait/polling)

POST /v2/crypto/transactions/status

{ "provider_ref": "...", "wait": 1 }
{ "id": "...", "wait": 1 }

Resposta:

{
  "id": "...",
  "direction": "OUT",
  "coin": "USDT",
  "network": "trc20",
  "amount": "10.00000000",
  "platform_fee": "0.10000000",
  "provider": "LUNARPAY_ADQ",
  "provider_ref": "...",
  "status": "PENDING|sent|processing|canceled",
  "provider_status": "sent|processing|canceled",
  "tx_hash": null,
  "confirmed_at": null,
  "reversed_at": null
}

9) OTC/P2P — Cotação, Execução e Status

9.1 Cotação

POST /v2/p2p/quote

{ "from_coin": "BRL", "to_coin": "USDT", "amount": "100" }

Resposta:

{
  "from_coin": "BRL",
  "to_coin": "USDT",
  "amount_in": "100.00000000",
  "platform_fee_in": "1.00000000",
  "net_to_convert": "99.00000000",
  "estimated_out": "19.87654321"
}
9.2 Executar

POST /v2/p2p/execute

{ "from_coin": "BRL", "to_coin": "USDT", "amount": "100", "pin": "1234" }

Resposta:

{
  "status": "COMPLETED",
  "group_ref": "...",
  "provider_txid_out": "...",
  "provider_txid_in": "...",
  "from_coin": "BRL",
  "to_coin": "USDT",
  "amount_in": "100.00000000",
  "platform_fee_in": "1.00000000",
  "net_converted": "99.00000000"
}
9.3 Status (wait/polling)

POST /v2/p2p/status

{ "group_ref": "...", "wait": true, "timeout_ms": 30000, "interval_ms": 1000 }
  • Também pode consultar por provider_txid (se existir no banco).
  • apply_balance=true tenta aplicar/reverter saldo automaticamente se o provedor mudou o status.

10) Sobre “respostas completas” da LUNARPAY_ADQ

Esta API é a intermediária: expõe apenas o necessário para integração (IDs, status, valores e campos operacionais).

  • Não há payload bruto público do provedor nos endpoints de integração.
  • Para auditoria/diagnóstico, isso deve existir apenas em rotas internas/admin (não documentadas publicamente).

Recomendação: ative ip_whitelist e use apenas URLs https para webhook.