Infrastructure · sub-accounts and custody

PIX and stablecoin infrastructure, under your brand

The "Deposit and Withdraw" tab of your product, without you becoming a payment institution. Each end customer is a sub-account in the ledger, keyed by the id that already exists in your system.

Create a sandbox key Talk to integration Full documentation

Who this is for

The problem

Holding other people's money is where most products stall. It is not just receiving: it is separating per customer, holding what cannot leave yet, proving every movement and being able to explain a balance months later. Building that from scratch means a ledger, reconciliation and an audit nobody wanted to start today.

Here the separation is a field. customer_ref travels on the charge, the balance, the withdrawal and the statement; the ledger is append-only and every line has an origin. You ship the experience under your brand and write no reconciliation.

The flow, end to end

  1. Deposit: POST /cashin/charge with destino: "saldo" and the customer's customer_ref. The PIX becomes a BRL balance in their sub-account.
  2. Read: GET /saldo?customer_ref= returns available, held and upcoming releases. GET /saldo/extrato returns movement by movement.
  3. Withdraw in reais: POST /payouts with the same customer_ref — it debits the right sub-account.
  4. Withdraw in crypto: POST /saldo/sacar-cripto delivers any catalog coin, with the route minimum checked before the debit.
  5. Move between customers: POST /saldo/transferir settles inside the ledger, touching no external rail.

Endpoints used

EndpointWhat for
POST /cashin/charge (destino: saldo)BRL deposit into the end customer's sub-account.
GET /saldo?customer_ref=Balance, current fees and upcoming releases.
GET /saldo/extratoMovement by movement, with each line's origin.
GET /saldo/clientes · /saldo/consolidadoAll sub-accounts and the total under custody.
POST /saldo/transferirTransfer between sub-accounts, inside the ledger.
POST /saldo/sacar-cripto · POST /payoutsThe two exits: crypto to the customer's wallet, or PIX to their key.

A example that runs

# depósito na subconta de um cliente final
curl -X POST https://api.luniumpay.com/cashin/charge -H "X-API-Key: $LUNIUM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount_cents":50000,"destino":"saldo","customer_ref":"cliente_412",
       "payer_tax_number":"12345678909","external_id":"dep-412-88"}'

# saldo daquele cliente
curl "https://api.luniumpay.com/saldo?customer_ref=cliente_412" -H "X-API-Key: $LUNIUM_KEY"

# transferência entre subcontas, sem sair do livro-razão
curl -X POST https://api.luniumpay.com/saldo/transferir -H "X-API-Key: $LUNIUM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from_customer_ref":"cliente_412","to_customer_ref":"cliente_907",
       "amount_cents":15000,"external_id":"tr-9912"}'
const h = { "X-API-Key": process.env.LUNIUM_KEY, "Content-Type": "application/json" };
const api = (rota, corpo) => fetch(`${API}${rota}`, corpo
  ? { method: "POST", headers: h, body: JSON.stringify(corpo) }
  : { headers: h }).then((r) => r.json());

export const depositar = (cliente, cents, cpf, id) => api("/cashin/charge", {
  amount_cents: cents, destino: "saldo", customer_ref: cliente,
  payer_tax_number: cpf, external_id: id,
});

export const saldoDe = (cliente) => api(`/saldo?customer_ref=${encodeURIComponent(cliente)}`);

export const sacarPix = (cliente, cents, chave, tipo, doc, id) => api("/payouts", {
  amount_cents: cents, pix_key: chave, pix_key_type: tipo,
  tax_number: doc, customer_ref: cliente, external_id: id,
});

export const sacarCripto = (cliente, cents, ativo, rede, carteira, doc, id) => api("/saldo/sacar-cripto", {
  amount_cents: cents, asset: ativo, chain: rede, payout_address: carteira,
  tax_number: doc, customer_ref: cliente, external_id: id,
});
import os, requests

API, H = "https://api.luniumpay.com", {"X-API-Key": os.environ["LUNIUM_KEY"]}

def depositar(cliente, cents, cpf, ident):
    return requests.post(f"{API}/cashin/charge", headers=H, timeout=30, json={
        "amount_cents": cents, "destino": "saldo", "customer_ref": cliente,
        "payer_tax_number": cpf, "external_id": ident}).json()

def saldo_de(cliente):
    return requests.get(f"{API}/saldo", headers=H, params={"customer_ref": cliente}, timeout=20).json()

def transferir(de, para, cents, ident):
    return requests.post(f"{API}/saldo/transferir", headers=H, timeout=30, json={
        "from_customer_ref": de, "to_customer_ref": para,
        "amount_cents": cents, "external_id": ident}).json()

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

First-PIX hold: the credit is available immediately; only the first PIX from a document above BRL 100.00 is held for 24 h at the provider and shows as delayed in GET /saldo, releasing on its own with saldo.liberado — show the time to the customer, not a generic error.

Insufficient balance: 402 with available and held stated separately. Nothing is debited.

Withdrawal below the coin's minimum: refused before the debit, with the estimated minimum in reais — the customer's balance never leaves for a delivery that would die at the exchange.

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

The deposit screen is one call; the balance screen is another. What usually takes longer is designing your balance screen — current fees and holds are visible in the API, so your interface can explain them without hard-coding anything.

Questions integrators ask

Does this make me a payment institution?

Custody and settlement are ours; you run the experience and the end-customer relationship. Your product's regulatory framing is a decision for you and your counsel — we do not sell a licence or a legal opinion.

How do I separate each user's money?

By customer_ref, the same id you already use. The ledger is append-only and every movement has a traceable origin; GET /saldo/consolidado closes the total under custody.

Can I set the holding period?

By default there is no holding period: the credit is available immediately (GET /saldo shows carencia_horas: 0). A holding period exists only by contractual agreement; if it does, your interface reads it from there.

Can the end customer withdraw in crypto?

Yes: POST /saldo/sacar-cripto delivers any catalog coin through the same rail as a purchase, with the route minimum checked before debiting.

Create a sandbox key Talk to integration

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

The rail under your brand. 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 at checkout, stablecoin in treasuryCharge in PIX, hold in digital dollars.PIX and stablecoin for walletsGive your users a way in and out.

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