Wallets and apps · replacing the ramp

PIX and stablecoin for wallets and apps

Your app is already live and the BRL deposit is the bottleneck. Here each user has their own limit, each operation has state, and the screen can show which stage it is in — without you storing anything.

Create a sandbox key Talk to integration Full documentation

Who this is for

The problem

In a wallet app, the deposit is where users give up. They type an amount, the provider refuses without saying what would fit, and the screen shows "error". After paying, they stare at a spinner with no idea whether it is five seconds or five minutes. Those two moments cost more users than any fee.

Both have an API answer. Each payer's limit is queryable before the keypad appears, and the status carries a server-stamped timeline with the measured median of each stage — you can write "step 3 of 5, usually 39 s" without making a number up.

The flow, end to end

  1. The user enters their tax id once. You call GET /cashin/limits?payer_tax= and already show how much they can deposit right now and how much fits with a hold.
  2. They pick an amount. POST /cashin/charge with customer_ref (their id in your system) returns the copy-and-paste code.
  3. They pay. The cashin.paid webhook arrives; the screen changes stage without polling.
  4. Delivery happens and cashin.settled brings the hash. If the destination is saldo, the credit is already available.
  5. To withdraw: POST /payouts (PIX) or POST /saldo/sacar-cripto (any catalog coin), always with the same customer_ref.

Endpoints used

EndpointWhat for
GET /cashin/limits?payer_tax=How much this user can pay right now — before the screen asks for an amount.
POST /cashin/chargeDeposit, with customer_ref tying the operation to the user.
GET /cashin/{cashin_id}/statusTimeline and typical times per stage: this is what feeds the tracker.
GET /saldo?customer_ref=That user's balance, hold and upcoming releases.
POST /saldo/sacar-criptoWithdrawal in any catalog coin, debited from the user's balance.
POST /payoutsPIX withdrawal to the user's own key or a third party's.

A example that runs

# depósito do usuário: PIX identificado pelo CPF dele, cripto na carteira dele
curl -X POST https://api.luniumpay.com/cashin/charge -H "X-API-Key: $LUNIUM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount_cents":15000,"asset":"usdt","chain":"polygon",
       "payout_address":"0xCarteiraDoUsuario",
       "payer_tax_number":"12345678909",
       "customer_ref":"user_8842","external_id":"dep-8842-17"}'

# limite daquele usuário ANTES de mostrar o teclado de valor
curl "https://api.luniumpay.com/cashin/limits?payer_tax=12345678909" -H "X-API-Key: $LUNIUM_KEY"
async function limiteDoUsuario(cpf) {
  const r = await fetch(`${API}/cashin/limits?payer_tax=${cpf}`, {
    headers: { "X-API-Key": process.env.LUNIUM_KEY },
  });
  const l = await r.json();
  return { naHora: l.instant_available_cents, comRetencao: l.held_qr?.available_cents ?? 0 };
}

async function depositar({ cents, cpf, carteira, userId, idempotencia }) {
  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: cents, asset: "usdt", chain: "polygon",
      payout_address: carteira, payer_tax_number: cpf,
      customer_ref: userId, external_id: idempotencia,
    }),
  });
  if (r.status === 403) throw new Error("acima do limite deste pagador");
  return r.json();
}
import os, requests

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

def limite_do_usuario(cpf):
    l = requests.get(f"{API}/cashin/limits", headers=H, params={"payer_tax": cpf}, timeout=20).json()
    return l["instant_available_cents"], (l.get("held_qr") or {}).get("available_cents", 0)

def depositar(cents, cpf, carteira, user_id, idempotencia):
    r = requests.post(f"{API}/cashin/charge", headers=H, timeout=30, json={
        "amount_cents": cents, "asset": "usdt", "chain": "polygon",
        "payout_address": carteira, "payer_tax_number": cpf,
        "customer_ref": user_id, "external_id": idempotencia})
    if r.status_code == 403:
        raise RuntimeError("acima do limite deste pagador")
    return r.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

Above the payer's limit: 403 limite_do_pagador with acao: "corrigir" and how much fits. Show the number, not the word "error" — that is what converted best in the apps already using it.

Below the route minimum: the response carries the estimated minimum in reais and nothing is debited. Offer the right amount in one tap.

A document's first deposit: the credit is available immediately; only the first PIX from a document above BRL 100.00 is held for 24 h at the provider (delayed, released on its own with saldo.liberado). Show the time, not "try later".

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 app's critical path is two calls: limit and charge. Both exist in the sandbox with the same contract — you can build the whole screen before a commercial contract exists.

Questions integrators ask

Do I need to store operation state?

No. GET /cashin/{id}/status carries the server-stamped timeline and typical times per stage; the webhook announces each transition. Your database keeps the external_id and the rest comes from here.

How do I separate users?

By customer_ref. It travels on the charge, the balance, the withdrawal and the statement — the ledger keeps each customer apart without you creating a key per user.

Can users withdraw coins other than USDT?

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

Can a user deposit more than the initial limit?

Yes: above the no-hold ladder the QR is still accepted and the provider holds it for 24 h. If your product cannot wait, send allow_hold: false and get a 403 stating how much goes out immediately.

Create a sandbox key Talk to integration

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

A PIX ramp in your wallet. 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.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