E-commerce · many payers
PIX at checkout, stablecoin in treasury
Each buyer pays an ordinary PIX in their own bank. The store receives stablecoin in a corporate wallet. Because the limit belongs to the paying tax id, ten buyers are ten independent limits.
Who this is for
- A B2C store or marketplace selling in Brazil that wants its cash in stablecoin.
- An operation with many different payers and a low-to-mid average ticket.
- Anyone reconciling PIX by hand who wants a paid order to become an event in their system.
The problem
Every e-commerce settling in crypto fears the ceiling: "what if the day's volume blows the account limit?". That question comes from the wrong model of the product. The limit that rules here belongs to the paying document, and it is per ticket — the receiving account has no daily ceiling of its own that sums everything.
The second problem is reconciliation. A PIX to a fixed key arrives with no owner: you see the amount and cannot tell which
order it belongs to. Here each charge is born tied to the buyer's tax id and to your external_id, and a PIX paid
by a different document is refunded instead of becoming an orphan credit.
The flow, end to end
- At checkout you call
POST /cashin/chargewith the total, the buyer's tax id and the order number inexternal_id. - Show
qr_copypasteon screen. It is valid for 15 minutes — generate it at pay time, the way Mercado Pago and banks do. - The buyer pays.
cashin.paidarrives: that is the trigger to release the order. - The stablecoin lands in the company wallet and
cashin.settledarrives with the hash. - Reconciliation:
GET /cashin/charges?external_id=always returns that order's charge.
Endpoints used
| Endpoint | What for |
|---|---|
POST /cashin/charge | One charge per order, with the buyer's tax id and your order number. |
GET /cashin/limits?payer_tax= | How much that buyer can pay right now — useful on high tickets. |
GET /cashin/charges | List filtered by external_id, status and period: this is reconciliation. |
POST /webhooks/test | Test your webhook URL before the first sale depends on it. |
GET /cashin/events | Operations feed for your key, for an internal screen, with no state to keep. |
A example that runs
# um pedido = uma cobrança, identificada pelo CPF de quem compra
curl -X POST https://api.luniumpay.com/cashin/charge -H "X-API-Key: $LUNIUM_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_cents":34990,"asset":"usdt","chain":"polygon",
"payout_address":"0xCarteiraDaEmpresa",
"payer_tax_number":"12345678909",
"payer_name":"Maria S.",
"external_id":"pedido-2026-33871"}'// checkout: gera o QR no momento em que o comprador vai pagar
app.post("/checkout/:pedido/pix", async (req, res) => {
const { total_cents, cpf, nome } = req.body;
const r = await fetch(`${API}/cashin/charge`, {
method: "POST",
headers: { "X-API-Key": process.env.LUNIUM_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
amount_cents: total_cents, asset: "usdt", chain: "polygon",
payout_address: process.env.CARTEIRA_EMPRESA,
payer_tax_number: cpf, payer_name: nome,
external_id: `pedido-${req.params.pedido}`,
}),
});
const c = await r.json();
if (!r.ok) return res.status(r.status).json({ erro: c.erro, detalhe: c.detail });
res.json({ copiaECola: c.qr_copypaste, expiraEm: c.expires_at });
});
// webhook: só marca o pedido como pago depois de conferir a assinatura
app.post("/webhooks/lunium", verificarAssinatura, async (req, res) => {
if (req.body.event === "cashin.paid") await marcarPago(req.body.data.external_id);
res.sendStatus(200);
});import os, requests
from flask import Flask, request, jsonify
API, H = "https://api.luniumpay.com", {"X-API-Key": os.environ["LUNIUM_KEY"]}
app = Flask(__name__)
@app.post("/checkout/<pedido>/pix")
def cobrar(pedido):
dados = request.get_json()
r = requests.post(f"{API}/cashin/charge", headers=H, timeout=30, json={
"amount_cents": dados["total_cents"], "asset": "usdt", "chain": "polygon",
"payout_address": os.environ["CARTEIRA_EMPRESA"],
"payer_tax_number": dados["cpf"], "payer_name": dados.get("nome"),
"external_id": f"pedido-{pedido}"})
c = r.json()
if not r.ok:
return jsonify(erro=c.get("erro"), detalhe=c.get("detail")), r.status_code
return jsonify(copia_e_cola=c["qr_copypaste"], expira_em=c["expires_at"])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
A first-time buyer with a high ticket: the ladder starts at BRL 60.00. Above it the QR is still accepted,
held for 24 h — the order can wait for D+1, or you send allow_hold: false and refuse immediately, showing
how much fits.
Someone else paid: a PIX from a different document is refunded automatically. That is the case that hurts most on a fixed key, and here it does not happen silently.
The QR expired: the charge becomes expired. Create another with a new external_id —
repeating the same id would return the old, already-expired QR.
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
One endpoint at checkout and one webhook handler. The teams that did it took an afternoon, because there is no onboarding: the sandbox key takes one call and the contract is the same as production.
Questions integrators ask
Is there a daily ceiling that stops the store?
The governing limit is the paying tax id's, per ticket: BRL 60.00 first ever, BRL 200.00 in the first 24 h and BRL 6,000.00/day afterwards. Your key has its own tier, visible in GET /keys/me, and higher ceilings are contracted — but ten buyers remain ten separate limits.
Can I use a fixed PIX key instead of a QR per order?
No, and that is deliberate. A fixed key receives from anyone with no identification: you would not know which order the amount belongs to, and that is exactly the scenario that produces refunds and disputes. One charge per order is what makes reconciliation automatic.
Does the buyer need crypto or a wallet?
No. They pay an ordinary PIX in their bank app. The crypto side is yours.
How do I refund a cancelled order?
Refunding the buyer is operated by you, with money that already settled; the API does not reverse a PIX for a commercial decision. What it refunds on its own is a PIX that came from the wrong document.
OpenAPI contract 1.25.0 · facts on this page in product-truth.json · status