Build

Build with Lunium

Pick what you want to build. Each case comes in REST, Node and Python — and with a ready prompt, if you would rather have a coding agent do it.

The prompts are deliberately generic: none names an endpoint, a field or an asset. If they had to, discovery would not really be working.

PIX → USDT

Receive PIX from customers and settle the value into USDT in a wallet.

# 1. test key — one call
curl -X POST https://api.luniumpay.com/keys/sandbox -H 'Content-Type: application/json' \
  -d '{"name":"my-project"}'

# 2. PIX charge; the payer's tax ID is required by the Central Bank
curl -X POST https://api.luniumpay.com/cashin/charge \
  -H "X-API-Key: $LUNIUM_KEY" -H 'Content-Type: application/json' \
  -d '{"amount_cents": 15000,
       "payer_tax": "12345678901",
       "payout_address": "0xYourWallet",
       "chain": "polygon",
       "external_id": "order-123"}'

# 3. follow it until it settles
curl https://api.luniumpay.com/cashin/$ID/status -H "X-API-Key: $LUNIUM_KEY" 
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;

const call = (path, method = "GET", body) =>
  fetch(API + path, {
    method,
    headers: { "Content-Type": "application/json", "X-API-Key": key },
    body: body && JSON.stringify(body),
  }).then((r) => r.json());

// Creates the charge. The amount goes in CENTS — sending 150 instead of 15000
// is the most common mistake, and the API takes both because both are valid.
const charge = await call("/cashin/charge", "POST", {
  amount_cents: 15000,
  payer_tax: "12345678901",
  payout_address: "0xYourWallet",
  chain: "polygon",
  external_id: "pedido-" + Date.now(),
});

if (charge.erro) throw new Error(`${charge.erro}: ${charge.detail}`);
console.log("copia e cola:", charge.qr_copypaste);

// Then: follow it on a signed webhook, or poll the status.
const status = await call(`/cashin/${charge.public_id}/status`);
console.log(status.status, status.settlement_status);
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


charge = call("/cashin/charge", "POST", {
    "amount_cents": 15000,          # cents, not reais
    "payer_tax": "12345678901",     # required by the Central Bank
    "payout_address": "0xYourWallet",
    "chain": "polygon",
    "external_id": "order-123",
})
print("copia e cola:", charge["qr_copypaste"])
print(call(f"/cashin/{charge['public_id']}/status"))
Integrate Lunium starting from https://luniumpay.com. I need to receive PIX and settle into USDT. Discover the available capabilities and API yourself, start in sandbox, and preserve this project's existing architecture.

Hand this to a coding agent. It discovers the API on its own.

USDT → PIX

Sell USDT and have a PIX key receive Brazilian reais.

# 1. quote — commits to nothing, valid for 15 minutes
curl -X POST https://api.luniumpay.com/cash-outs \
  -H "X-API-Key: $LUNIUM_KEY" -H 'Content-Type: application/json' \
  -d '{"asset":"USDT","network":"polygon","brl_amount":"150.00",
       "pix_key":"who-receives@example.com",
       "refund_address":"0xYourUserWallet",
       "external_id":"sale-123"}'

# 2. accept — POINT OF NO RETURN. Returns the deposit address.
curl -X POST https://api.luniumpay.com/cash-outs/$ID/accept -H "X-API-Key: $LUNIUM_KEY"

# 3. follow it; at COMPLETED the Central Bank receipt arrives
curl https://api.luniumpay.com/cash-outs/$ID -H "X-API-Key: $LUNIUM_KEY" 
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;
const call = (p, m = "GET", b) => fetch(API + p, {
  method: m,
  headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: b && JSON.stringify(b),
}).then((r) => r.json());

// 1. quote: send brl_amount OR amount, never both — a quote runs in
// one direction only. Here we say "I want the key to receive R$ 150".
const quote = await call("/cash-outs", "POST", {
  asset: "USDT", network: "polygon", brl_amount: "150.00",
  pix_key: "who-receives@example.com",
  // where the crypto goes back to if the PIX cannot be paid. Without it,
  // it would return to the on-chain origin — an exchange, if that is where the user withdrew from.
  refund_address: "0xYourUserWallet",
  external_id: "venda-" + Date.now(),
});
if (quote.erro) throw new Error(`${quote.erro}: ${quote.detail}`);
console.log(`envie ${quote.amount} USDT → R$ ${quote.brl_amount}`);

// 2. accepting locks the quote and returns the address. After that there is no going back.
const accepted = await call(`/cash-outs/${quote.cashout_id}/accept`, "POST", {});
console.log("deposit em:", accepted.deposit_address);

// 3. at COMPLETED: pix_e2e, receipt_url and verify_url
const done = await call(`/cash-outs/${quote.cashout_id}`);
console.log(done.state, done.pix_e2e ?? "");
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


quote = call("/cash-outs", "POST", {
    "asset": "USDT", "network": "polygon",
    "brl_amount": "150.00",                    # ou "amount", nunca os dois
    "pix_key": "who-receives@example.com",
    "refund_address": "0xYourUserWallet",
    "external_id": "sale-123",
})
print(f"envie {quote['amount']} USDT -> R$ {quote['brl_amount']}")

# point of no return
aceita = call(f"/cash-outs/{quote['cashout_id']}/accept", "POST", {})
print("deposit em:", aceita["deposit_address"])

final = call(f"/cash-outs/{quote['cashout_id']}")
print(final["state"], final.get("pix_e2e", ""))
Integrate Lunium starting from https://luniumpay.com. I need to convert USDT into BRL via PIX. Discover the API yourself, start in sandbox, and preserve this project's existing architecture.

Hand this to a coding agent. It discovers the API on its own.

Payment verification

Confirm a PIX settled before releasing goods, credit or access. No API key needed.

# No key, by design: it lets you check a payment that
# YOU did not make, from a counterparty you have no reason to trust.
curl https://api.luniumpay.com/v1/verificar/E12345678202608291200abcdefghijk
// No credentials. That is the point: anyone can check.
const e2e = "E12345678202608291200abcdefghijk";
const r = await fetch(`https://api.luniumpay.com/v1/verificar/${e2e}`);
const proof = await r.json();

// `nao_encontrado` does NOT mean the PIX never existed — it means
// Lunium did not settle it. Another institution may have.
if (!proof.verificado) {
  console.log("not proven:", proof.erro);
} else {
  // Check the amount and the time yourself: a valid E2E for R$ 1.00
  // is not proof of a R$ 1,000.00 payment.
  console.log(proof.valor_brl, proof.pago_em, proof.instituicao);
}
import json, urllib.error, urllib.request

E2E = "E12345678202608291200abcdefghijk"
url = f"https://api.luniumpay.com/v1/verificar/{E2E}"

try:
    with urllib.request.urlopen(url, timeout=20) as r:
        proof = json.load(r)
except urllib.error.HTTPError as e:
    # 404 is a legitimate answer, not a bug in your code: it means
    # Lunium did not settle this payment — not that it does not exist.
    proof = json.loads(e.read() or b"{}")

if proof.get("verificado"):
    print(proof["valor_brl"], proof["pago_em"], proof["instituicao"])
else:
    print("not proven:", proof.get("erro"))
Integrate Lunium starting from https://luniumpay.com. Build a check that confirms whether a Brazilian PIX payment actually settled, from its end-to-end identifier, before releasing anything. This capability needs no API key — discover it yourself.

Hand this to a coding agent. It discovers the API on its own.

Treasury agent

Move balance between reais and stablecoins on a rule or a schedule.

# The catalog is the source of truth for what settles right now — and it changes on its own.
# Do not hardcode the list.
curl https://api.luniumpay.com/catalog -H "X-API-Key: $LUNIUM_KEY"

# Your limits today: per-transaction ceiling, the tier daily ceiling (which
# applies to cash-in and payouts), the per-recipient daily ceiling on the sale
# and your own mandate
curl https://api.luniumpay.com/keys/me -H "X-API-Key: $LUNIUM_KEY" 
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;
const call = (p, m = "GET", b) => fetch(API + p, {
  method: m, headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: b && JSON.stringify(b),
}).then((r) => r.json());

// A sale does NOT consume the tier daily quota (that one is for cash-in and payouts).
// Two ceilings bound it: the per-transaction one and the one of whoever RECEIVES the PIX today.
// Read both from /keys/me — hardcoding a number ages badly.
const me = await call("/keys/me");
const perTx = me.limits?.per_operation?.cashout?.max_brl_cents ?? Infinity;  // R$ 250,000.00
const perRecipient = me.limits?.payee_limits?.daily_cents ?? Infinity;      // R$ 100,000.00/day
const ceiling = Math.min(perTx, perRecipient);

const alvoCents = 500_00;
if (alvoCents > ceiling) {
  console.log("above today's ceiling; split the operation");
} else {
  const quote = await call("/cash-outs", "POST", {
    asset: "USDT", network: "polygon",
    brl_amount: (alvoCents / 100).toFixed(2),
    pix_key: process.env.PIX_TESOURARIA,
    refund_address: process.env.CARTEIRA,
    external_id: "tesouraria-" + new Date().toISOString().slice(0, 10),
  });
  console.log(quote.cashout_id, quote.amount, "USDT");
}
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


# A sale does not consume the tier quota (that one is for cash-in and payouts):
# what bounds it is the per-transaction ceiling and the daily ceiling of whoever RECEIVES the PIX.
me = call("/keys/me")
print("per transaction:", me["limits"]["per_operation"]["cashout"]["max_brl_cents"])
print("per recipient/day:", me["limits"]["payee_limits"]["daily_cents"])

quote = call("/cash-outs", "POST", {
    "asset": "USDT", "network": "polygon", "brl_amount": "500.00",
    "pix_key": os.environ["PIX_TESOURARIA"],
    "refund_address": os.environ["CARTEIRA"],
    "external_id": "treasury-2026-08-29",
})
print(quote["cashout_id"], quote["amount"], "USDT")
Integrate Lunium starting from https://luniumpay.com. Build a treasury agent that moves balance between BRL over PIX and USDT according to a rule. Discover the API yourself and start in sandbox.

Hand this to a coding agent. It discovers the API on its own.

Payment agent

Let autonomous software pay in Brazil, inside a spending policy it sets for itself.

# 1. the agent registers and gets a test credential right away
curl -X POST https://api.luniumpay.com/mesh/agents -H 'Content-Type: application/json' \
  -d '{"name":"my-agent","contact":"me@example.com",
       "capabilities":["usdt_to_pix"]}'

# 2. the policy it imposes on itself. It only TIGHTENS the key limits,
#    never widens them — which is why the agent may set it itself.
curl -X POST https://api.luniumpay.com/mesh/agents/$AGENT_ID/mandate \
  -H "X-API-Key: $LUNIUM_KEY" -H 'Content-Type: application/json' \
  -d '{"max_por_operacao_cents": 30000, "max_diario_cents": 200000}'
const API = "https://api.luniumpay.com";

// 1. registration: returns agent_id and the test credential in one response
const reg = await fetch(`${API}/mesh/agents`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "my-agent", contact: "me@example.com",
    capabilities: ["usdt_to_pix"],
  }),
}).then((r) => r.json());

const { agent_id } = reg.agent;
const key = reg.api_key;   // shown only once

// 2. the agent's own spending policy
await fetch(`${API}/mesh/agents/${agent_id}/mandate`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: JSON.stringify({
    max_por_operacao_cents: 30_000,   // R$ 300 per operation
    max_diario_cents: 200_000,        // R$ 2,000 a day
  }),
});

// From here on, an order above the ceiling is refused with
// `mandate_valor_acima_do_teto` — by its own policy, not by ours.
import json, urllib.request

API = "https://api.luniumpay.com"


def post(path, body, key=None):
    req = urllib.request.Request(API + path, data=json.dumps(body).encode(), method="POST")
    req.add_header("Content-Type", "application/json")
    if key:
        req.add_header("X-API-Key", key)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


reg = post("/mesh/agents", {
    "name": "my-agent", "contact": "me@example.com",
    "capabilities": ["usdt_to_pix"],
})
agent_id, key = reg["agent"]["agent_id"], reg["api_key"]

# A mandate only TIGHTENS limits — the worst it can do is restrict you.
post(f"/mesh/agents/{agent_id}/mandate", {
    "max_por_operacao_cents": 30000,
    "max_diario_cents": 200000,
}, key=key)
print("agent", agent_id, "with its own policy")
Integrate Lunium starting from https://luniumpay.com. Build an agent that pays a PIX key using stablecoins, with a per-operation and daily limit it configures itself. Discover the API yourself and start in sandbox.

Hand this to a coding agent. It discovers the API on its own.

Wallet with PIX

Give wallet users PIX deposits and withdrawals against USDT or USDC.

# deposit: PIX comes in, USDT goes to the user's wallet
curl -X POST https://api.luniumpay.com/cashin/charge -H "X-API-Key: $LUNIUM_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"amount_cents":10000,"payer_tax":"$USER_CPF",
       "payout_address":"$USER_WALLET","chain":"polygon",
       "external_id":"deposit-$UID"}'

# withdrawal: USDT leaves the wallet, reais land on the user's PIX key
curl -X POST https://api.luniumpay.com/cash-outs -H "X-API-Key: $LUNIUM_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"asset":"USDT","network":"polygon","brl_amount":"100.00",
       "pix_key":"$USER_PIX_KEY","pix_key_type":"cpf",
       "refund_address":"$USER_WALLET","external_id":"withdrawal-$UID"}'
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;
const call = (p, m = "GET", b) => fetch(API + p, {
  method: m, headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: b && JSON.stringify(b),
}).then((r) => r.json());

// DEPOSIT — the user pays a PIX, their wallet receives USDT
export async function deposit(user, centavos) {
  return call("/cashin/charge", "POST", {
    amount_cents: centavos,
    payer_tax: user.cpf,            // required by the Central Bank
    payout_address: user.wallet,
    chain: "polygon",
    external_id: `dep-${user.id}-${Date.now()}`,
  });
}

// WITHDRAWAL — the user sends USDT, their PIX key receives reais
export async function withdraw(user, reais) {
  const q = await call("/cash-outs", "POST", {
    asset: "USDT", network: "polygon", brl_amount: reais.toFixed(2),
    pix_key: user.pixKey,
    // pix_key_type is only required when the key is 11 bare digits:
    // CPF and phone numbers are the same length and cannot be told apart.
    pix_key_type: user.pixKeyType,
    refund_address: user.wallet,
    external_id: `wd-${user.id}-${Date.now()}`,
  });
  return call(`/cash-outs/${q.cashout_id}/accept`, "POST", {});
}
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


def deposit(user, centavos):
    """PIX comes in, USDT goes to the user's wallet."""
    return call("/cashin/charge", "POST", {
        "amount_cents": centavos,
        "payer_tax": user["cpf"],
        "payout_address": user["carteira"],
        "chain": "polygon",
        "external_id": f"dep-{user['id']}",
    })


def withdraw(user, reais):
    """USDT sai, reais caem na chave PIX dele."""
    q = call("/cash-outs", "POST", {
        "asset": "USDT", "network": "polygon",
        "brl_amount": f"{reais:.2f}",
        "pix_key": user["chave_pix"],
        "pix_key_type": user["tipo_chave"],   # required when they are 11 digits
        "refund_address": user["carteira"],
        "external_id": f"wd-{user['id']}",
    })
    return call(f"/cash-outs/{q['cashout_id']}/accept", "POST", {})
Integrate Lunium starting from https://luniumpay.com. Add PIX deposits and withdrawals to a crypto wallet, settling into USDT. Discover the capabilities and required fields yourself, and start in sandbox.

Hand this to a coding agent. It discovers the API on its own.

They all start the same way

# the whole catalog, no credential
curl https://api.luniumpay.com/mesh

# a test key, one call
curl -X POST https://api.luniumpay.com/keys/sandbox

The sandbox runs the whole flow without moving a cent, and the first two decimals of the amount choose the outcome — you can exercise the failure paths in CI instead of discovering them in production.

How discovery works More examples

By use case

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.Infrastructure under your brandThe whole rail running behind your product.

Talk to the people who built it

A sandbox key takes one call and no signup — 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