Cash-in · PIX → stablecoin
PIX to USDT API
Your customer pays an ordinary PIX. You receive USDT or USDC in the wallet you name, with no connected wallet and no friction for the payer. One call creates the charge; the rest is a webhook.
Who this is for
- A product already selling in Brazil that wants the money in stablecoin instead of a bank account.
- A company abroad that needs to accept PIX without opening a Brazilian entity, tax id or acquirer.
- A team replacing an on-ramp with one that has an open contract, a signup-free sandbox and verifiable receipts.
The problem
Accepting PIX and ending up with stablecoin usually takes three parts that do not talk to each other: an acquirer for the PIX, a Brazilian bank account for the money to land in, and an exchange to convert. Each with its own onboarding, its own limit and its own reconciliation — with the money idle in between.
Here it is one call. The PIX comes in, the conversion happens, and the crypto goes out to the wallet you named. There is no balance waiting to be withdrawn and no manual step in the middle.
The flow, end to end
- You call
POST /cashin/chargewith the amount, asset, network, your wallet and the payer's tax id. - The response carries
qr_copypaste: that is the PIX you show the customer. It is valid for 15 minutes. - The customer pays in their bank. The
cashin.paidwebhook arrives. - Conversion and delivery happen on their own.
cashin.settledarrives with the on-chain hash. - Need to check?
GET /cashin/{cashin_id}/statuscarries a server-stamped timeline.
Endpoints used
| Endpoint | What for |
|---|---|
POST /cashin/preview | How much crypto comes out for an amount in reais, with the route minimum. Creates nothing. |
POST /cashin/charge | Creates the charge and returns the copy-and-paste code. |
GET /cashin/{cashin_id}/status | Current state, delivery hash and timeline. |
GET /cashin/limits?payer_tax= | How much that payer can pay right now, before you charge. |
GET /cashin/catalog | Assets, networks, timing and delivery minimums. |
A example that runs
curl -X POST https://api.luniumpay.com/cashin/charge \
-H "X-API-Key: $LUNIUM_KEY" -H "Content-Type: application/json" \
-d '{
"amount_cents": 25000,
"asset": "usdt",
"chain": "polygon",
"payout_address": "0xSuaCarteira",
"payer_tax_number": "12345678909",
"external_id": "pedido-7781"
}'const r = await fetch("https://api.luniumpay.com/cashin/charge", {
method: "POST",
headers: { "X-API-Key": process.env.LUNIUM_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
amount_cents: 25000,
asset: "usdt",
chain: "polygon",
payout_address: "0xSuaCarteira",
payer_tax_number: "12345678909",
external_id: "pedido-7781",
}),
});
const cobranca = await r.json();
console.log(cobranca.cashin_id, cobranca.qr_copypaste);import os, requests
r = requests.post(
"https://api.luniumpay.com/cashin/charge",
headers={"X-API-Key": os.environ["LUNIUM_KEY"]},
json={
"amount_cents": 25000,
"asset": "usdt",
"chain": "polygon",
"payout_address": "0xSuaCarteira",
"payer_tax_number": "12345678909",
"external_id": "pedido-7781",
},
timeout=30,
)
cobranca = r.json()
print(cobranca["cashin_id"], cobranca["qr_copypaste"])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
The PIX is paid by a different document than the one declared: the charge is refunded to the payer
and the status carries refund_reason. Nothing is delivered to the wrong owner.
The settlement provider goes down: POST /cashin/charge answers 502
provedor_indisponivel with acao: "repetir". No charge is created in that case — repeat
the same call with the same external_id.
The customer did not pay in time: the charge becomes expired and nothing happens. Create another.
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
From zero to the first QR: one call for the sandbox key and one for the charge. The integrators who arrived through search did it in minutes, without talking to anyone.
For production you need a production key and a webhook URL. The rest of the code does not change: the sandbox uses the same contract, the same fields and the same errors.
Questions integrators ask
Do I need a Brazilian company?
Not to integrate and test. The sandbox key takes one call, and a production key does not require a Brazilian entity — what exists is review by volume, like on any payment rail.
Why is the payer tax id mandatory?
Because it is what ties each real that comes in to the right customer, applies the per-payer limit and lets us automatically refund a PIX that came from a different document. Without it, money arrives with no owner.
Can I receive assets other than USDT?
Yes. USDT and USDC on Polygon go out on our own rail, in seconds. The other 1795 catalog routes go through the exchange, at the network's pace.
How long does it take?
6 s median and 70 s p90 from PIX paid to stablecoin delivered on Polygon, measured over 112 real operations in a 90-day window.
What if the same order is charged twice?
Send external_id. Repeating returns the same charge, with the same QR — never a second one.
OpenAPI contract 1.25.0 · facts on this page in product-truth.json · status