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.

Create a sandbox key Talk to integration Full documentation

Who this is for

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

  1. At checkout you call POST /cashin/charge with the total, the buyer's tax id and the order number in external_id.
  2. Show qr_copypaste on screen. It is valid for 15 minutes — generate it at pay time, the way Mercado Pago and banks do.
  3. The buyer pays. cashin.paid arrives: that is the trigger to release the order.
  4. The stablecoin lands in the company wallet and cashin.settled arrives with the hash.
  5. Reconciliation: GET /cashin/charges?external_id= always returns that order's charge.

Endpoints used

EndpointWhat for
POST /cashin/chargeOne 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/chargesList filtered by external_id, status and period: this is reconciliation.
POST /webhooks/testTest your webhook URL before the first sale depends on it.
GET /cashin/eventsOperations 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 prior approval, no card; only your email is required (that is where we send the manual, the flowchart and the integration steps). 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 '{"email":"you@company.com","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

Use GET /catalog for incoming crypto and GET /cashin/catalog for delivery after Pix. Asset availability, networks and minimums differ between the two directions and can change.

Send the returned deposit_tag with a deposit when required. Use payout_tag for an outgoing route that requires a memo. See the API manual for the current contract.

Limits and holds

Read account limits and fees from GET /keys/me, payer availability from GET /cashin/limits?payer_tax=... and route minimums from previews. Cash-in, cash-out and payouts have different constraints.

A received payment can be held before settlement. Track status, timeline and signed events until the final outcome; read expiry from expires_at.

Measurements, not promises

Settlement time depends on the payment flow, route availability, network confirmations and any hold. Track the final state and check service status. Sandbox timing does not measure real settlement.

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 100.00. Above it the QR is still accepted, held for 24 h — the order can wait the 24 h, 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

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 100.00 on the first operation, BRL 200.00 in the first 24 h and BRL 6,000.00/day afterwards. There is no daily ceiling per key — current limits are in GET /keys/me — and 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.

Create a sandbox key Talk to integration

Current OpenAPI contract · facts on this page in product-truth.json · status

PIX at checkout, dollars in treasury. Start with a test key.

The test key takes one call, no prior approval and no cost — and your case goes straight to the integration team.

Get a sandbox key and talk to integration 3.3% + R$ 1.00 on cash-in · 1% + provider fee on PIX → PIX · no monthly fee

Other paths

PIX to USDT APITake reais, deliver USDT to your customer’s wallet.USDT to PIX APISell stablecoin and pay any PIX key.PIX payout APIPay suppliers and users in reais, over the API.BRL ↔ USDT over APIBoth directions, with a quote and a receipt.Brazil on-ramp and off-rampCrypto in and out of Brazil, in one integration.PIX and stablecoin for walletsGive your users a way in and out.Infrastructure under your brandThe whole rail running behind your product.

Talk to the people who built it

A sandbox key takes one call, no prior approval — if that's what you're after, start with the docs. This form is for the rest: volume pricing, a case the docs don't cover, or a question before you integrate.

Rather email us? contato@luniumpay.com