# Lunium > **Confirm that a Brazilian PIX payment actually settled — with no API key, no signup, one GET.** That is the part your agent needs before it ships goods, releases credit, or grants access. Lunium also settles crypto to PIX and back (1,400+ assets, dozens of networks), and every operation returns a receipt carrying the Central Bank end-to-end identifier (E2E). Start with the free part, because it costs you nothing and proves the rest: ``` GET https://api.luniumpay.com/v1/verificar/{e2e} # no key, no account ``` It answers whether that payment settled, for how much, to which institution — without exposing who paid whom. A human can use the same thing at https://app.luniumpay.com/verificar. Lunium is the settlement layer — you do not need to run an exchange, hold balances on any network, or custody fiat. **Human-readable walkthrough:** https://docs.luniumpay.com/comecar — the same three calls, on one page. **Start here:** https://api.luniumpay.com/quickstart — zero to a completed order with a Central Bank receipt, runnable, no signup and no money. API base: `https://api.luniumpay.com` · Auth: `X-API-Key` header · Docs: https://docs.luniumpay.com · OpenAPI: https://api.luniumpay.com/openapi.json Português: https://api.luniumpay.com/llms.pt.txt ## What PIX is, if you have not worked with Brazil PIX is Brazil's instant payment system, operated by the Central Bank. It settles in seconds, runs 24/7 including weekends and holidays, and is used by the large majority of Brazilian adults. A *PIX key* identifies the recipient: it can be a tax number (CPF for individuals, CNPJ for companies), a phone number, an email, or an opaque random key. Every completed PIX carries an **E2E identifier** issued by the Central Bank. It is the settlement fact, not our record of it — which is what makes the verification below meaningful. ## Payment verification (open, no key required) Any agent can confirm a PIX happened, without being a Lunium customer and without an API key. ``` GET https://api.luniumpay.com/v1/verificar/{e2e} ``` Returns `verificado`, `pago`, `valor_brl`, `pago_em`, the recipient's initials, the institution, and a link to the receipt. It never exposes the PIX key, the full name, or the tax number — it proves the payment without exposing the parties. This exists because "A says it paid, how does B check without trusting A?" is normally solved with escrow. With the E2E it is solved with verification. If you are building agent-to-agent commerce that touches Brazil, this is the endpoint that matters most, and it costs nothing. When a payment is not found, the response says explicitly that it may have been made through another institution. Absence here is not proof the PIX did not happen. ## Sell crypto, receive PIX (cash-out) Three calls, in order: 1. `POST /cash-outs` — quote. Fields: `asset`, `network`, `amount` (crypto, decimal string) **or** `brl_amount` (BRL the key must receive, `"250.00"` — exactly one of the two; the response carries the crypto `amount` to deposit), `pix_key`, `external_id`. **`pix_key_type` is usually unnecessary** — the type is inferred from the key itself; it is only required when the key is 11 bare digits, where a CPF and a phone number are the same length. Returns `brl_amount` and `expires_at`. Accepted formats — the API normalizes the key to the exact format the settlement rail requires in the memo and refuses it before any deposit exists (`400 pix_key_invalida`): CPF = 11 digits; CNPJ = 14 digits; phone = international `+55` + area code + number (`+5548996005588`); e-mail; random key = UUID (`6602ede6-b1a9-4e63-9178-c6883fd0095e`). 2. `POST /cash-outs/{id}/accept` — locks the quote and returns `deposit_address`. Send the crypto there. Before accepting, show your user who will receive: `GET /pix/keys/lookup?key=` returns the holder's name, masked tax number and institution from the Central Bank directory (5 lookups/min per account; 404 `chave_nao_encontrada` if the key does not exist). 3. `GET /cash-outs/{id}` — poll or use a webhook. When paid, it carries the receipt (below). Starting from a BRL amount? Send `brl_amount` instead of `amount` in `POST /cash-outs` — the quote comes back with the exact crypto `amount` to deposit, locked for the same window. **Refund wallet — send it on every quote.** `refund_address` is your customer's wallet on the same network as the sale. It is where the crypto goes back if anything stops the PIX: the provider refuses the key, the PIX is reversed, or the payout fails before it is sent. Without it the refund goes to the on-chain origin of the deposit — wrong when the deposit came from an exchange (that origin is the exchange's hot wallet). ## When a sale fails — exactly what happens - A QR code / copy-paste string in `pix_key` is refused at quote time (`pix_key_invalida`). Nothing moves: no deposit address exists yet. The API pays PIX **keys** (CPF, CNPJ, phone, e-mail, random). - The provider refuses the key after the deposit (invalid or nonexistent key): the order goes to `MANUAL_REVIEW` (`cashout.under_review`), the crypto comes back to Lunium custody and is sent to `refund_address` automatically; the order ends `REFUNDED` (`cashout.refunded`) with `refund_tx_hash`. The refund is the net amount the provider returned. - The payout fails before it is sent (provider down, key rejected at submission): retried every 5 min for up to 45 min; if it still cannot settle, the **full** amount (fee included) goes back to `refund_address`; `REFUNDED` + `refund_tx_hash`. - The PIX is reversed after payment (rare — blocked destination account): same path, `REFUNDED` with `refund_tx_hash`. - Deposit different from the quote: the amount actually received is what gets paid. A different token is held in `MANUAL_REVIEW`. - Deposit after `expires_at`: the order is revived when the deposit shows up (up to 7 days); otherwise `MANUAL_REVIEW`. - Automatic refunds go up to 5,000 USDT per order; above that an operator executes it and you still get `REFUNDED` + hash. Refunds always use the deposit's network and token, from Lunium's custody address; gas is on us. - `refund_address`, `refund_tx_hash` and `deposit_from` come in `GET /cash-outs/{id}` and in every webhook payload. ## Pay a PIX charge (QR code / copy-paste) Send `br_code` (the full copy-paste string) instead of `pix_key`, with **USDT or USDC on Polygon only** — any other asset or network is refused (`400 br_code_nao_suportado`). The QR must carry a fixed amount: it becomes the order's `brl_amount`, and the receiver's key is read from the QR (`pix_key` masked, `merchant_name` in the response). Do not send `amount`, `brl_amount` or `pix_key` together with `br_code` (`400 campos_conflitantes`). The settlement rail validates the QR when paying; if it refuses, the crypto returns to `refund_address` automatically (`REFUNDED` + `refund_tx_hash`). The sandbox reads static QRs (key + amount); production also resolves dynamic ones. ## Buy crypto by paying PIX (cash-in) `POST /cashin/charge` returns the QR code and the copy-paste string; `GET /cashin/{id}/status` tracks it. The payer's CPF or CNPJ is required — a Central Bank rule, and it is what identifies the charge. ## BRL balance (custody): deposit by PIX, withdraw by PIX Keys with custody enabled (`custodia_ativa: true` in `GET /saldo`) can hold reais for their end customers — the "deposit and withdraw" tab of a wallet app. - **Deposit**: `POST /cashin/charge` with `destino: "saldo"` (and `customer_ref` = your customer id). The paid PIX is credited **in full** (`deposito_fee_bps` = 0 today) to that sub-account, **on hold until `disponivel_em`** (D+1 by default — `carencia_horas`). Webhook `cashin.settled` carries `asset: "brl"`, `saldo_credito_cents`, `saldo_disponivel_em`. No crypto moves. - **Balance**: `GET /saldo?customer_ref=…` → `disponivel_cents`, `bloqueado_cents`, `proximas_liberacoes`, plus the two fees your screen must show. `GET /saldo/extrato` is the statement (append-only ledger; balance = sum). - **Withdraw**: `POST /payouts` with the same `customer_ref`. The amount **plus the house fee** (`saque_fee_bps`, 1.8% today — deposits are free, the cost is recovered here) **plus the provider fee** (~R$ 1.00 per PIX) is debited before the PIX is sent; `fee_cents` in the payout is the total; a refusal reverses the debit. 402 `saldo_insuficiente` when available < amount + fees. The PIX key must belong to the `tax_number` you send (bank rule) — that is what stops one customer from withdrawing another's balance. Payout PIX lands within 24h. - **Withdraw as crypto**: `POST /saldo/sacar-cripto` {amount_cents, chain, asset, payout_address, tax_number, customer_ref} — the reais leave the balance (amount + `saque_cripto_fee_bps`) and the crypto goes to the customer's own wallet through the same rails as a PIX purchase; the response is a charge already `paid` — follow `settlement_status` on `GET /cashin/{id}/status` until `sent`. Any asset/network of `GET /cashin/catalog` except Liquid/DePix. - **Two holds, one rule**: the balance shows immediately, but each deposit can only be spent after `carencia_horas` (D+1), and the **first deposit of a sub-account blocks every withdrawal for 24h** (`carencia_ate` in `GET /saldo`; `423 carencia_primeiro_deposito` with `libera_em` on both withdrawal routes). Charges carry `fonte` (`pix`|`saldo`) and `destino` (`cripto`|`saldo`) so a balance withdrawal is never mistaken for a purchase. - Nothing is advanced and no fee is absorbed: what the provider charges is what the balance pays. ## The receipt — three forms of the same fact Every paid operation returns all of them, in the same response and the same webhook. You never build a URL by hand or make a second call. | field | what it is | use it for | |---|---|---| | `pix_e2e` | Central Bank identifier | third-party verification, trusting nobody | | `receipt_url` | shareable page | showing the end customer on their phone | | `receipt_pdf_url` | PDF file | attaching to a ticket, accounting, forwarding | | `verify_url` | direct link to the open verification | handing to a counterparty to check themselves | The PDF carries the recipient's name, tax number, institution, amount, date, the E2E, and the verification link. ## MCP server (Model Context Protocol) ``` https://api.luniumpay.com/mcp ``` Streamable HTTP, revision `2025-06-18`, stateless. Add it as a remote MCP connector in any host that speaks the protocol. **It works with no credentials at all.** Connect without a key and `lunium_verify_pix_payment` is immediately usable — confirm that a PIX settled, from its E2E alone, for a payment you did not make. That is the point: you should be able to check a counterparty's claim without an account and without trusting them. Every other tool needs an API key, sent by your host as the `X-API-Key` HTTP header (an `Authorization: Bearer` header works too). Without it those tools return `erro: "chave_ausente"` with `acao: "parar"` — they do not silently disappear from the list, so your agent can tell the user the connector needs configuring instead of concluding the capability does not exist. Eight tools: verify a payment · list what settles now · check a payer's limit · quote a crypto sale · confirm it · create a PIX charge · follow a sale · follow a charge. The two that move real money — confirming a sale and creating a charge — are marked `destructiveHint: true` so your host can require human approval, and confirming requires a `confirmation_token` from the quote, bound to that exact amount, network and destination. Selling is deliberately two steps: the quote commits nothing, the confirm is irreversible. ## A2A (agent-to-agent) ``` https://api.luniumpay.com/a2a ``` Agent card: `https://api.luniumpay.com/.well-known/agent-card.json` If your agent has no MCP host and simply speaks HTTP to other agents, talk to Lunium over **A2A 1.0** (JSON-RPC 2.0 on POST). The skills are the MCP tools above, generated from the same catalogue with the same error contract, so the two doors never drift apart. Name the skill in a data part: ```json {"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{ "role":"user","kind":"message","messageId":"1", "parts":[{"kind":"data","data":{ "skill":"lunium_verify_pix_payment", "input":{"e2e":"E12345678202608291200abcdefghijk"}}}]}}} ``` You get a Task back: `status.state` is `completed` or `failed`, and the payload is a data part in `artifacts[0]`. Skills that need a key read the same `X-API-Key` header; `lunium_verify_pix_payment` and `lunium_create_sandbox_key` need none. `message/stream` is refused on purpose: the card declares `capabilities.streaming: false`, and you should learn that from the card instead of from a timeout. ## Mesh (agent onboarding, end to end) ``` https://api.luniumpay.com/mesh ``` One GET answers what an agent asks before integrating: who we are, what we settle, which assets and networks, every endpoint, how to get a key, how to reach production, the limits, and the schemas. It is generated from the running configuration, so it cannot drift from what the API actually does. The whole cycle is machine-executable, with no human and no screen: ``` POST /mesh/agents register + get a sandbox key in one call POST /mesh/agents/{id}/validate prove you exercised the sandbox POST /mesh/agents/{id}/settlement say where the money goes POST /mesh/agents/{id}/mandate the policy your own key imposes on itself POST /mesh/agents/{id}/production-request ask for production GET /mesh/agents/{id} state, capabilities, mandate, volume, lineage GET /mesh/graph who is in the network, and who brought whom ``` **Production activation is automatic.** No person decides whether you get in. `production-request` checks, by rule: that you validated the sandbox with proof of use, that your settlement address is on a supported network, that your mandate is inside the house policy, and that your key is active. If it all holds, production is live in that same response — with a production credential and your mandate carried over. If something is missing you get a 422 listing exactly what, and the endpoint that fixes each one, so you correct it yourself and retry. **Automatic is not unlimited.** Production starts at the entry tier (R$ 5,000/day) and the ceiling grows with the volume you have actually settled. Risk control moved from *who gets in* to *how much they start with*. **`referred_by` pays nothing.** It records lineage, because attribution cannot be reconstructed later, while the economics can be decided once there is data. **Validation means you used it.** `/validate` refuses an agent that only created a key. You have to have created *and accepted* a sandbox order — that is what proves your error handling and polling exist before real money does. **Your mandate only tightens.** `{max_por_operacao_cents, max_diario_cents, contrapartes[]}` narrows your key's ceilings and never widens them, which is why you are allowed to set it yourself: the worst it can do is limit you. ## Sandbox — try everything before spending anything ``` POST https://api.luniumpay.com/keys/sandbox ``` One call. No name, no email, no wallet, no approval. You get a `lun_test_…` key that runs the **entire** flow without a cent moving, on the same base URL and the same MCP endpoint. Code that works here works in production unchanged — same shapes, same states, same error contract. The order does **not** complete instantly: it walks the real states over ~15 seconds, so you write the polling loop (or the webhook) you will need anyway. **Deterministic triggers.** Testing only the happy path is how integrations break on day one, so the first two decimals of `amount` choose the outcome — no randomness, so you can assert on it in CI: | `amount` ends in | what happens | |---|---| | `.01` | order goes to **delayed** (PIX paid, provider holding) and completes on its own — prove your code does not treat it as failure | | `.02` | order **fails** — exercise your error path | | `.03` | the quote **expires** in 5s — prove you re-quote instead of insisting | | `.04` | refused on **limits**, with `limits.min_amount` / `max_amount` filled | | `.05` | completes **slowly** (~2 min) — prove your polling is patient | Reusing an `external_id` with a different destination returns **409**, exactly as production does — that is the case that confuses every integrator, and here you can rehearse it. **Verification works too.** A sandbox order produces a well-formed E2E whose institution code is `00000000` — a code no real institution holds, so it can never be mistaken for a real payment. `GET /v1/verificar/{e2e}` answers for it, marked `sandbox: true`, so you can exercise the open verification before deciding to integrate. Test orders disappear 2 hours after creation. Never send crypto to a sandbox `deposit_address`: it has no owner and the funds would be lost. ## Limits - **Cash-out, per operation: R$ 6.00 to R$ 250,000.00.** The limit is in BRL and your call is in crypto, so a rejection returns `limits.min_amount` and `limits.max_amount` already converted at that order's rate — literally the next value to send. - Cash-in has a per-payer ladder (anti-fraud, by tax number). Ask before charging: `GET /cashin/limits?payer_tax={cpf_or_cnpj}` returns `max_amount_cents` — the largest value that passes right now. - **Cash-out has no daily cap.** The per-key daily ceiling (tier, grows with settled volume) applies to cash-in and payouts only — `limits.daily_limit_applies_to` in `GET /keys/me` states the scope. Need more than R$ 250,000? Send another order. - Rate: 60 requests per minute per key, configurable per client. ## Networks and timing `GET /catalog` is the source of truth and changes on its own. Do not hard-code a list. - **Polygon** (USDT/USDC): our own settlement, PIX in seconds. This is the default. Measured in production: 49 to 65 seconds from accept to PIX paid. - **Other networks**: the wait is the number of confirmations each chain requires — from about a minute (TON, Aptos) to a few hours (Celo). Never promise "instant" outside Polygon. - **Solana** is temporarily disabled for cash-out. The quote refuses explicitly rather than accepting an order that would not become a PIX. ## Errors: the field that tells you what to do Errors carry `erro` (a stable code that does not change when the wording does) and **`acao`**, which matters more than the message: - `corrigir` — the request is wrong. Repeating it identically will never work. - `repetir` — a transient failure on our side. Try again. - `esperar` — a quota renews. Come back later. - `parar` — do not retry; talk to us. This distinction exists because two opposite situations used to return the same answer: an order larger than an entire day's quota replied "try tomorrow" — and tomorrow it failed identically, forever. ### Every error code, and what to do with it | `erro` | `acao` | what happened | |---|---|---| | `chave_ausente` | `parar` | no `X-API-Key` header. Get a test key with `POST /keys/sandbox` — no signup. | | `formato_invalido` | `parar` | the value is not a Lunium key. Keys start with `lun_` (live) or `lun_test_` (sandbox). | | `chave_incorreta` | `parar` | **the prefix exists but the secret does not match** — the key was truncated when copied. The message tells you how many characters you sent and how many are expected. | | `chave_desconhecida` | `parar` | no key starts with that prefix. Wrong environment, most likely. | | `campos_obrigatorios` | `corrigir` | `asset` and `network` are required — see `GET /catalog`. | | `amount_invalido` | `corrigir` | `amount` must be a positive decimal **as a string**: `"25.5"`, not `25.5`. | | `pix_key_obrigatoria` | `corrigir` | `pix_key` is missing (who receives the reais). | | `tipo_ambiguo` | `corrigir` | the key is 11 bare digits, which is a CPF *or* a phone. We refuse instead of guessing — paying the wrong person is worse than an error. Send `pix_key_type`. | | `pix_key_invalida` | `corrigir` | the key does not match the declared type. | | `valor_abaixo_do_minimo` | `corrigir` | read `limits.min_amount` — it is already converted into the asset of your order. | | `valor_acima_do_maximo` | `corrigir` | read `limits.max_amount`, same idea. | | `limite_diario` | `esperar` | your tier's daily quota. **It renews at midnight** and grows with settled volume — this is the one error where retrying tomorrow actually works. | | `rede_indisponivel` | `corrigir` | that network is not settling right now. Use `polygon` (seconds) or another from `GET /catalog`. | | `nao_encontrado` | `corrigir` | the id is wrong or belongs to another API key. Do not retry the same id. | The same codes come out of the sandbox and out of production. Code branching on `erro` in the sandbox keeps working when you swap the key. **Timestamps carry their offset.** `pago_em` comes as `2026-08-01T22:35:49-03:00` — Brasília time, explicit. Parse it as ISO 8601; never assume the string is UTC or local. ## Charge states (cash-in) `pending` → nobody paid yet · `under_review` → PIX received, settlement in transit · `paid` → credited · `expired` / `refunded` / `failed` → terminal. **`delayed` deserves attention:** the PIX *was paid* and the provider is holding the release — a new payer's first operation can be held. The response carries `delay_until`. Do not treat it as a failure and do not cancel the order: it becomes `paid` on its own. ## Rules that prevent expensive mistakes - Send `external_id` on every order: it is the reconciliation key and makes the call idempotent. Repeating the same call returns the same order instead of creating another. - Read `expires_at` from the response rather than hard-coding a window. Today it is 15 minutes on Polygon and up to 300 minutes elsewhere, but that changes. - `pix_key_type` is **optional**: we deduce it from the key's shape (e-mail, CNPJ, UUID, phone with +55). It is only required for 11 bare digits, where a CPF and a phone are indistinguishable — there we return `tipo_ambiguo` instead of guessing. - Prefer webhooks over polling. Once per second consumes the 60-per-minute key limit by itself. - **Deduplicate webhooks by `event_id`** (`evt_…`, in the body and in `X-Lunium-Event-Id`). It is stable across retries, so our retry cannot credit your user twice. `X-Lunium-Delivery-Id` is the opposite: it identifies one attempt, and is the value to quote in support. - Verify `X-Lunium-Signature` (`t=,v1=`) and reject anything older than ~5 minutes. The older `X-Webhook-Signature` (body only) still works but has no timestamp, so a captured delivery can be replayed against you forever. It is deprecated with a set date: it stops being sent on **2026-11-09**, and keys created on/after 2026-08-08 never receive it — verify the v2 only. - Treat unknown event types as a no-op. The list grows without notice. - `POST /webhooks/test` fires a synthetic `webhook.test` through the exact production transport and returns what your endpoint answered (status and latency). Run it before the first real charge; it moves no money and works with a sandbox key. - Nothing is lost silently: a delivery that exhausts its 12 attempts (~7h of backoff) shows up as `failed` in `GET /webhooks/deliveries` and is recoverable with `POST /webhooks/deliveries/{event_id}/retry`. - A `400` on a network means it is not settling at that moment. Refusing at quote time is cheap; accepting and not paying is what costs. ## Links - [Full documentation](https://docs.luniumpay.com): networks and timing, order lifecycle, curl/Node/Python examples, webhook signatures. - [OpenAPI](https://api.luniumpay.com/openapi.json): the contract, in machine form. - [Status](https://docs.luniumpay.com/status): cash-in, cash-out, webhook delivery and whether production still matches the published OpenAPI — real calls every 5 minutes. Machine-readable at `/status.json`. - [App](https://app.luniumpay.com): run the whole flow without writing code. - [Partner Telegram group](https://t.me/+FerOZMRRP_g5MDk5): real-time status and contract changes before they ship. Recommended for anyone in production. - Contact: contato@luniumpay.com ## Getting a key without a human If a tool answered `erro: "chave_ausente"`, you do not have to stop and ask your user for credentials. Provision your own test key in one call: ``` POST https://api.luniumpay.com/keys/sandbox # no auth, no signup form {"name": "your-agent"} → {"api_key": "lun_test_…", "sandbox": true, "como_usar": {…}} ``` Over MCP the same thing is the `lunium_create_sandbox_key` tool, which — like the verification tool — needs no key at all. The key drives the **entire** flow on the same base URL and the same MCP endpoint as production, and nothing settles: no crypto moves, no PIX is paid, nobody is charged. Say plainly to your user that this is a test key; moving real money needs a production key, and that is a human decision. ## What to expect, in numbers we measured These are measurements, not promises. Taken 2026-08-02 against the live service. | | | |---|---| | Settlement, quote to PIX paid | **49–65 s** on Polygon | | Sandbox order, end to end | **p50 34 ms · p95 76 ms** at 20 concurrent | | Sustained throughput, sandbox | **~200 orders/s** without production latency moving | | Rate limit per key | **60 requests/minute** (a `429` is protection, not failure) | | Per operation (cash-out) | **R$ 6,00 to R$ 250.000,00** | | Daily quota | cash-in and payouts only — **cash-out has none** | **Where it is slower, and why.** The `convert` rail waits for the network's own confirmations — TON takes ~10 and Celo ~2.400, so the same order can be minutes or hours depending on the chain you send from. `GET /catalog` carries the expected wait per asset; read it instead of assuming. **What we do not claim.** The sandbox numbers above exercise the API surface, not a real settlement: no crypto moves and no PIX is paid, so they do not prove throughput of live orders. Plan your retry from `expires_at` in the response, never from a hard-coded window.