BRL ↔ USDT · production
BRL ↔ USDT over API
Both directions on one key, with a price you can read before any commitment. The quote is a call that creates no order, consumes no limit and answers with the same math that will apply.
Who this is for
- A fintech or company that needs BRL → USDT and USDT → BRL running in production, not in a pilot.
- A treasury that converts daily and wants the price on screen before deciding.
- Anyone using a manual exchange today who wants the same result with a contract, state and receipt.
The problem
Converting BRL and USDT predictably has two enemies: a price that moves between decision and execution, and a process that depends on someone logging in somewhere. The first becomes a silent loss; the second becomes a dependency on business hours.
The answer is to separate quoting from executing. The preview answers with the same math as the real order — same rail, same minimums, same refusals — but persists nothing. You show the number, the user decides, and only then does the order come into being with the amount locked.
The flow, end to end
- Quote:
POST /cashin/preview(reais → crypto) orPOST /cash-outs/preview(crypto → reais). Nothing is created. - The preview returns the rail (
path), the quantity and, on the buy side, the route minimum in reais. - Buy:
POST /cashin/chargegenerates the PIX; once paid, the crypto goes to the named wallet. - Sell:
POST /cash-outs+/acceptreturns the address; once deposited, the PIX goes out. - Every operation has an
external_id, a signed webhook and a receipt — reconciliation is automatic.
Endpoints used
| Endpoint | What for |
|---|---|
POST /cashin/preview | Buy price and the route minimum in reais. Creates nothing. |
POST /cash-outs/preview | Sell price both ways, with no order and no limit consumed. |
POST /cashin/charge | Buy: generates the identified PIX and delivers the crypto. |
POST /cash-outs + /accept | Sell: locks the quote and returns the deposit address. |
GET /keys/me | Per-operation limits and your key's current tier. |
A example that runs
# preço antes de qualquer ordem — os dois sentidos
curl -X POST https://api.luniumpay.com/cashin/preview -H "X-API-Key: $LUNIUM_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_cents":100000,"asset":"usdt","chain":"polygon"}'
curl -X POST https://api.luniumpay.com/cash-outs/preview -H "X-API-Key: $LUNIUM_KEY" \
-H "Content-Type: application/json" \
-d '{"asset":"USDT","network":"polygon","amount":"1000"}'const h = { "X-API-Key": process.env.LUNIUM_KEY, "Content-Type": "application/json" };
// BRL -> USDT: quanto de cripto sai por R$ X (com o mínimo da rota)
export async function comprar(cents) {
const p = await (await fetch(`${API}/cashin/preview`, { method: "POST", headers: h,
body: JSON.stringify({ amount_cents: cents, asset: "usdt", chain: "polygon" }) })).json();
return { usdt: p.usdt_amount ?? p.amount, minimoBrlCents: p.min_brl_cents };
}
// USDT -> BRL: quanto de PIX sai por N USDT, sem criar ordem
export async function vender(qtd) {
const p = await (await fetch(`${API}/cash-outs/preview`, { method: "POST", headers: h,
body: JSON.stringify({ asset: "USDT", network: "polygon", amount: String(qtd) }) })).json();
return { brl: p.brl_amount, trilho: p.path };
}import os, requests
API, H = "https://api.luniumpay.com", {"X-API-Key": os.environ["LUNIUM_KEY"]}
def comprar(cents):
p = requests.post(f"{API}/cashin/preview", headers=H, timeout=30, json={
"amount_cents": cents, "asset": "usdt", "chain": "polygon"}).json()
return p.get("usdt_amount") or p.get("amount"), p.get("min_brl_cents")
def vender(qtd):
p = requests.post(f"{API}/cash-outs/preview", headers=H, timeout=30, json={
"asset": "USDT", "network": "polygon", "amount": str(qtd)}).json()
return p["brl_amount"], p["path"]Sandbox: test before spending
One call, no signup, no card. The lun_test_ key exercises the whole surface without
moving any money.
curl -X POST https://api.luniumpay.com/keys/sandbox \
-H "Content-Type: application/json" \
-d '{"name":"my-test"}'
What the sandbox does not prove: real settlement. No crypto moves and no PIX is paid — it is there to get the contract right, not to measure the rail.
Assets and networks
Instant (our own rail): USDT on Polygon and USDC na Polygon — settled in seconds, no exchange in the path.
Through the exchange: 1,478 assets across 241 networks on the sell side (1,668 asset×network pairs) and 1,795 routes on the buy side. The wait is the number of confirmations each chain requires, not a choice of ours.
Shared-address networks (ALGO, ATOM, EOS, HBAR, KAVA, LUNA, TON, XLM, XRP): the deposit must carry the memo. On the sell side it comes
as deposit_tag in the accept; on the buy side you send payout_tag with the address.
The catalog changes on its own as networks come and go. Read it from GET /catalog and GET /cashin/catalog instead of keeping a list in code.
Limits and holds
| What | Value |
|---|---|
| Cash-in and PIX payout, per operation | BRL 1.00 to BRL 6,000.00 |
| Cash-out, per operation | from BRL 6.00 |
| QR validity | 15 minutes |
| Payer ladder (CPF/CNPJ of whoever pays) | BRL 60.00 first ever → BRL 200.00 in the first 24 h → BRL 6,000.00/day |
| Above the ladder | the QR is still accepted and the provider holds it for 24 h (D+1), up to BRL 6,000.00/day per payer |
The ladder is per paying document, not per receiving account: ten payers are ten independent
limits. That is what makes this work for e-commerce and marketplaces. Check a payer's headroom before
charging with GET /cashin/limits?payer_tax=, and read your key's limits from
GET /keys/me — ceilings above the tier are contracted.
Measurements, not promises
Measured in production, 90-day window, on 2026-09-07:
| Leg | Median | p90 | Sample |
|---|---|---|---|
| PIX received → stablecoin delivered (Polygon) | 6 s | 70 s | 112 operations |
| Quote accepted → PIX paid (Polygon) | 81 s | 580 s | 27 operations |
Off Polygon the clock is the network's: the wait is the number of confirmations the exchange
requires, and it is in eta in the catalog. We do not promise "instant" off our own rail.
Idempotency
Send external_id on every money-moving call. Repeating the same call returns the
same operation, never a second one; the same parameters with a different id create two, and the
same id with different parameters answers 409 external_id_divergente instead of guessing.
A timeout is not a refusal. If the call did not answer, look the operation up by
external_id before repeating — it may already exist.
Webhooks and reconciliation
Every transition fires an event signed with HMAC SHA-256 plus a timestamp (header
X-Lunium-Signature, format t=…,v1=…) — the timestamp is what stops a captured
delivery from being replayed. Verify the signature before trusting the body.
The webhook is the primary signal; polling is optional. When you do poll, the status carries a
timeline stamped by the server — draw the stage from it, because your polling stopwatch
measures your network, not the operation.
Did not answer 2xx? Delivery is retried with backoff. You can resend by hand at
POST /webhooks/deliveries/{event_id}/retry and test your URL at POST /webhooks/test.
When it goes wrong
Below the minimum: the refusal names the number — on the buy side, min_brl_cents comes in the preview
itself, so the screen shows the minimum before the user types.
Above the maximum: valor_acima_do_maximo with the current ceiling. Read it from GET /keys/me
instead of hard-coding: ceilings change by contract.
Expired quote: the order has an expires_at. After it, the deposit arrives orphaned — quote again rather than
depositing late.
Security
- webhook assinado — Todo webhook vai assinado em HMAC SHA-256 com timestamp (header X-Lunium-Signature, formato t=…,v1=…), o que impede replay de uma entrega capturada.
- idempotencia — external_id em cash-in, cash-out e payout: repetir a mesma chamada devolve a MESMA operação, nunca uma segunda. Parâmetros diferentes no mesmo external_id respondem 409.
- egress validado — A URL de webhook é resolvida e validada antes de cada entrega: endereço interno, metadata de nuvem e redirect para rede privada são recusados, e a conexão é fixada no IP validado.
- retencao fail closed — A retenção é verificada na liquidação e repetida na reivindicação SQL: nenhum caminho (worker, consulta de status, fila) consegue liquidar antes da hora.
- payout duravel — O saque grava a operação e debita o saldo na MESMA transação, antes de qualquer chamada ao provedor: não existe débito sem ordem para reconciliar.
- verificacao aberta — GET /v1/verificar/{e2e} confere um PIX liquidado sem chave nenhuma — a contraparte não precisa confiar na nossa palavra.
Open verification is the one that matters in a negotiation: your counterparty checks the PIX themselves, with no key and without taking our word for it.
Time to first integration
The first useful integration is just the preview: one call, no side effects, already enough to put on a screen. Execution comes later, on the same contract you already tested.
Questions integrators ask
Does the preview consume a limit or create an order?
Neither. It runs the same math as the real quote and returns the same rail and the same errors, persisting nothing — it was built for screens that quote on every keystroke.
Is the price locked?
On the sell side, yes: the accept locks the quote and expires_at says how long it holds. On the buy side, the PIX amount is what you set, and the delivered crypto follows the operation's quote.
Which pairs work?
USDT and USDC on Polygon settle on our own rail, in seconds. Beyond those, 1478 assets across 241 networks on the sell side and 1795 routes on the buy side, through the exchange.
Can I operate outside banking hours?
Yes. PIX settles 24/7, and the API has no window — what varies is the confirmation time of the chosen network.
OpenAPI contract 1.25.0 · facts on this page in product-truth.json · status