<!-- Lunium API integration manual · Markdown version for agents and LLMs · source: https://docs.luniumpay.com/en/manual · PT: https://docs.luniumpay.com/manual.md · PDF: https://docs.luniumpay.com/en/manual.pdf · contract: https://api.luniumpay.com/openapi.json · summary: https://api.luniumpay.com/llms.txt -->

# Lunium API · Integration manual

**For B2B integrators** · version of **2026-09-02** · base `https://api.luniumpay.com` · authentication: `X-API-Key` header · OpenAPI contract **1.12.0**

Lunium is the settlement layer between PIX and crypto in Brazil. A single key does all three things: **cash-in** (the customer pays a PIX and receives crypto in their wallet), **cash-out** (the customer delivers crypto and a PIX key receives reais) and **balance/payouts** (BRL custody for the end customer, withdrawable by PIX or as crypto). Every paid operation returns a receipt and the Central Bank's official identifier (E2E), which anyone can verify without a key. Everything in this manual is **in production and was measured** on the date above.

- **R$ 200 / day** — instant cash-in per CPF/CNPJ, from the very first operation
- **up to R$ 6,000 / day** — cash-in accepted with a 24h provider hold (state `delayed`)
- **2 s · 7 s** — PIX paid → USDT / USDC in the wallet (median, last 30 days, Polygon)
- **R$ 6 → R$ 250k** — cash-out per transaction · R$ 100k/day per recipient · no daily cap per key
- **49–65 s** — cash-out from accept to PIX paid, Polygon (measured in production)
- **2.5% + R$ 1.00** — default key fee on cash-in · cash-out: the quote is already net

### Contents

- 1. Flowchart
- 2. First steps: sandbox, production, operations group
- 3. Cash-in: PIX → crypto
- 3.1 Limits: instant × 24h hold (how to use it)
- 3.2 Request, response and states
- 3.3 Assets, timing and fees
- 4. Balance deposit (custody) and withdrawals
- 5. Cash-out: crypto → PIX
- 5.1 Limits, networks and timing
- 5.2 States, refunds and receipt
- 5.3 Sandbox: deterministic triggers
- 6. Payouts (direct PIX)
- 7. Webhooks
- 8. Errors: `erro` + `acao`
- 9. Summary table of limits and fees
- 10. Go-live checklist · 11. Links and channels

## 1. Flowchart

High-resolution version: `https://docs.luniumpay.com/manual-fluxograma.png` (labels in Portuguese).

![Lunium API integration flowchart](https://docs.luniumpay.com/manual-fluxograma.png)

## 2. First steps

### 2.1 Test key (sandbox) — one call

```
curl -s -X POST https://api.luniumpay.com/keys/sandbox \
  -H 'Content-Type: application/json' -d '{"name":"my-project"}'
→ {"api_key":"lun_test_…","sandbox":true,"como_usar":{…}}
```

The `lun_test_…` key runs on the **same base URL**, with the same shapes, states and error contract as production. Nothing moves. What the sandbox covers today:

| Surface | Sandbox | Notes |
|---|---|---|
| Cash-out (`POST /cash-outs` → accept → status) | `complete` | the order walks the real states in ~15 s; deterministic triggers (section 5.3); the test E2E is verifiable at `GET /v1/verificar/{e2e}`. |
| Cash-in (`POST /cashin/charge`) | `no QR` | answers `400 sandbox_sem_cashin`: a test QR would be payable for real. **Test cash-in with the production key and small amounts (R$ 1 to R$ 5)**; `GET /cashin/limits`, `POST /cashin/preview`, `GET /cashin/catalog` and `GET /keys/me` work in the sandbox. |
| Webhooks | `yes` | `POST /webhooks/test` fires a real `webhook.test` at your endpoint and returns status and latency. |
| MCP (AI agents) | `yes` | a sandbox key works at `https://api.luniumpay.com/mcp` with nothing special. |

### 2.2 Production key — only the name is required

```
curl -s -X POST https://api.luniumpay.com/keys \
  -H 'Content-Type: application/json' -d '{"name":"My Company"}'
→ {"api_key":"lun_…", "dashboard_url":"…", "grupos_obrigatorios":{…}, "limits":{…}}
```

The key is born immediately with cash-in and cash-out enabled, in the **Início** tier (R$ 5,000/day on cash-in + payouts; it rises to R$ 25,000 at R$ 50k settled and to R$ 100,000 at R$ 500k). The response carries the raw key **once only** (only the hash is stored); keep it in a vault. Limit of 5 production keys per IP per day.

**Never paste the `lun_…` key in the Telegram group.** If you need to recover it, create another (it is free) and swap it in your code; we can deactivate the old one.

### 2.3 Operations group on Telegram — strongly recommended

1. Create a group named **Lunium <> YourProject** with **@LuniumNotifyBot** (the agent) and **@luniumB2B** (the human on the other side). The API works without a group, but operating without one is operating blind: it is where you learn about a settlement that failed at 3 a.m.

2. In the bot's private chat, create or look up the key: `https://t.me/LuniumNotifyBot` → `/chave` shows the `lk…` code.

3. A **group admin** sends `/vincular lk…`. Without it the group receives no notification at all.

4. From then on the bot announces every PIX that comes in, every crypto that goes out, attaches the **receipt PDF** of every paid sale and answers questions when mentioned. Useful commands: `/configurar` (checklist), `/carteira 0x…`, `/status`, `/extrato`, `/limites`.

### 2.4 Configure the key

```
curl -s -X PATCH https://api.luniumpay.com/keys/me -H "X-API-Key: $KEY" \
  -H 'Content-Type: application/json' -d '{
    "settlement_address": "0xYOUR_POLYGON_WALLET",
    "webhook_url": "https://api.yourdomain.com/lunium/webhook"
  }'
→ returns webhook_secret (whk_…) ONCE; rotate_webhook_secret:true generates another
```

- `settlement_address`: default wallet that receives the cash-in crypto when the charge carries no `payout_address`.

- `webhook_url` + `webhook_secret`: HMAC signature of the events (section 7).

- `GET /keys/me` returns **every limit currently in force for your key** (tier, per operation, payer ladder, recipient ceiling). It is the number that never goes stale: read it instead of hard-coding the tables in this manual.

### 2.5 Test and reference links

| What | Where |
|---|---|
| Runnable quickstart (zero to an order with an E2E, in the sandbox) | `https://api.luniumpay.com/quickstart` |
| Documentation for humans (curl, Node, Python, networks × timing, webhooks) | `https://docs.luniumpay.com/en/` · walkthrough: `/en/comecar` |
| OpenAPI 3.1 contract (importable into Postman/Insomnia) | `https://api.luniumpay.com/openapi.json` |
| Summary for AI agents / LLMs | `https://api.luniumpay.com/llms.txt` (EN) · `/llms.pt.txt` (PT) |
| Live status (real calls every 5 min) | `https://docs.luniumpay.com/status` · machine: `/status.json` |
| MCP server (Streamable HTTP, 10 tools) | `https://api.luniumpay.com/mcp` |
| Examples that run (Node/Python) and skills for coding agents | `github.com/guilhermezanqueta-collab/lunium-examples` · `…/lunium-agent-skills` |
| App to try the whole flow without writing code | `https://app.luniumpay.com` |
| Partner channel (incidents and contract changes before they go live) | `https://t.me/+FerOZMRRP_g5MDk5` |

## 3. Cash-in: the customer pays a PIX → receives crypto

**Flow:** check the payer's limit → create the charge (QR + copy-and-paste) → the customer pays → the provider confirms → Lunium sends the crypto to `payout_address` → webhook `cashin.settled` with the transaction hash. The payer's CPF/CNPJ is **required** (a Central Bank requirement — the provider does not issue a QR without it) and it is what feeds the limit ladder.

### 3.1 Limits: instant × 24h hold — and how to use the hold

There is an anti-fraud step **per payer** (CPF/CNPJ), independent of the key. It has two levels and applies **from the document's very first operation** — no history and no 24h wait needed:

| Amount in the day for that CPF/CNPJ | What happens | How it shows in the API |
|---|---|---|
| **up to R$ 200.00** | **Instant.** PIX paid → crypto sent in seconds. | QR issued with `held:false`; status `pending → paid`; `settlement_status: sent`. |
| **from R$ 200.00 to R$ 6,000.00** | **Accepted with a 24h hold at the provider.** The PIX is paid normally; the amount is held and released on its own at the end of the window. Not a failure and not a refusal. | QR issued with `held:true, held_hours:24` (before payment). Once paid it becomes `delayed` with `delay_until`; on release it becomes `paid` and the crypto goes out. Webhooks `cashin.delayed` → `cashin.paid` → `cashin.settled`. |
| **above R$ 6,000.00/day** | Refused at creation. | `403 limite_do_pagador`, `acao: corrigir`, with `payer_limits` saying how much fits. |

Per charge: minimum **R$ 1.00** (with the default fee the effective minimum is R$ 1.02), maximum **R$ 6,000.00**. The ladder counts what was already paid **and what is in flight** (pending charges still valid); the clock only starts at the first confirmed payment — creating a QR does not mature the document.

> **How to use the hold in practice (what your app needs to do)**

> 1. **Ask before charging:** `GET /cashin/limits?payer_tax=CPF`. Read `instant_available_cents` (what settles instantly) and `max_amount_cents` (the largest amount the `POST` accepts right now, hold included). Above the instant amount and up to the maximum, the charge is issued with a hold.

> 2. **Show the customer before they pay:** the `POST /cashin/charge` response carries `held` and `held_hours`. If `held:true`, say "the amount is held for 24h and releases on its own".

> 3. **Do not treat `delayed` as a failure and do not cancel:** the PIX was paid. Keep `delay_until` and show "paid, releases at …". The order becomes `paid` by itself.

> 4. **Listen to the webhook** `cashin.delayed` (paid, held), then `cashin.paid` and `cashin.settled` (crypto sent, `settlement_tx_hash`). If you poll, call `GET /cashin/{id}/status` every 30–60 s while `delayed`.

> 5. The hold is the window for a dispute (MED) before the crypto leaves — it is what makes it possible to accept R$ 6,000 from a document that has never paid before.

```
GET /cashin/limits?payer_tax=52998224725   (real response, payer with no history)
{
  "stage": "first_transaction",
  "instant_available_cents": 20000,          ← settles instantly
  "max_amount_cents": 600000,                 ← largest amount accepted now (with hold)
  "held_qr": { "max_amount_cents": 600000, "used_cents": 0, "available_cents": 600000, "hold_hours": 24 },
  "ladder": { "first_transaction_cents": 20000, "first_24h_cents": 20000,
              "mature_day_cents": 600000, "held_max_cents": 600000, "hold_hours": 24 },
  "next_stage": { "stage": "first_24h", "limit_cents": 20000, "quando": "após o primeiro pagamento confirmado" },
  "explicacao": "Primeira operação deste CPF/CNPJ: até R$ 200.00 entra na hora; acima disso, até R$ 6000.00 o QR sai com retenção de 24h do provedor…"
}
```

> **Two provider refusals that are not outages (do not repeat the call):**

> - `403 pagador_recusado_pelo_provedor`, `acao: parar`, with `provider_reference`: the PIX provider refused that CPF/CNPJ by its own compliance decision. Only another document, or a review with the provider quoting the reference, resolves it.

> - `403 limite_do_pagador` with `stage: provider_first_qr`: the provider caps the first QR of a document it has not identified yet at R$ 200; from the next QR on, the document can use the whole ladder.

> - `502 provedor_indisponivel`, `acao: repetir`: this one is instability; try again shortly.

### 3.2 Request, response and states

```
POST /cashin/charge     (X-API-Key: lun_…)
{
  "amount_cents": 15000,                         // R$ 150.00 — ALWAYS in cents, integer
  "payer_tax_number": "12345678901",             // CPF (11) or CNPJ (14), digits only — required
  "payer_name": "Payer name",                    // optional
  "asset": "usdt", "chain": "polygon",           // pair from GET /cashin/catalog; omitted = usdt/polygon
  "payout_address": "0x51e3d4…bc75e0",           // YOUR customer's wallet; omitted = the key's settlement_address
  "external_id": "dep-0001"                      // idempotent: repeating returns the same charge (200)
}
→ 200
{
  "cashin_id": "ci_3ca9f23b…", "status": "pending",
  "amount_cents": 15000, "asset": "usdt", "chain": "polygon", "payout_address": "0x51e3…",
  "qr_copypaste": "00020126…", "qr_image_url": "https://…png",
  "expires_at": "2026-09-02T23:25:00.000Z",     // the QR is valid for 15 minutes
  "held": false, "held_hours": 0,                // true/24 when the amount requires a hold
  "delay_until": null, "liberar_em": null, "hold_hours": 0,
  "external_id": "dep-0001", "created_at": "…"
}
```

```
GET /cashin/{cashin_id}/status
{
  "cashin_id": "ci_…", "status": "paid",                     // pending | delayed | under_review | paid | expired | refunded | failed
  "settlement_status": "sent",                              // pending | sending | sent | incerto | failed
  "amount_cents": 15000, "depix_received_cents": 14625,     // net after fee
  "usdt_amount": "28.620000", "settlement_tx_hash": "0x…", "settlement_tx_url": "https://polygonscan.com/tx/0x…",
  "e2e": "E18236120202609021420…", "payer_name": "PAYER NAME", "payer_tax_number": "123.***.***-01",
  "held": false, "held_hours": 0, "delay_until": null, "liberar_em": null,
  "destino": "cripto", "customer_ref": null,
  "paid_at": "…", "settled_at": "…", "expires_at": "…", "created_at": "…"
}
```

| State | Meaning | Webhook |
|---|---|---|
| `pending` | QR issued, nobody has paid. | — |
| `delayed` | **PIX paid**, the provider is holding the release (24h hold). `delay_until` says when it releases. Not a failure. | `cashin.delayed` |
| `under_review` | PIX received, settlement in transit. | — |
| `paid` | Credited. The crypto goes out next (`settlement_status` pending → sending → **sent**). | `cashin.paid` → `cashin.settled` (with `settlement_tx_hash`) · `cashin.settlement_failed` if the send fails (retried) |
| `expired` | Nobody paid before `expires_at`. | `cashin.expired` |
| `refunded` | Returned to the payer (e.g. a dispute executed by the provider). | `cashin.refunded` |
| `failed` | The provider cancelled or errored before payment. | `cashin.failed` |

**The QR must be paid for the exact amount.** A fixed-amount charge does not reconcile with any difference, not even upwards. Tell the customer not to edit the amount in the banking app.

### 3.3 Assets, timing and fees

`GET /cashin/catalog` is the source of truth and changes on its own. Real shape: `{updatedAt, instant[], convert[], dropped[], totals}`.

| Rail | Assets | Measured time (PIX paid → crypto in the wallet, 30 days) |
|---|---|---|
| **instant** (our own settlement) | USDT and USDC on **Polygon** | USDT: median **2 s** (p90 2 s) · USDC: median **7 s** (p90 8 s) |
| **convert** (via exchange) | 1,300+ assets and networks (BTC, ETH, SOL, TON, BNB…), each with `eta`, minimum and withdrawal fee | minutes; depends on the network — e.g. BTC on-chain ~37 min. Read `etaSeconds` on each item. |
| **liquid** | `chain: "liquid"` → the payment is delivered as **DePix** (BRL stablecoin on Liquid) straight to the Liquid wallet given, with no key fee deducted | seconds after the provider confirms |

**Default key fee:** `admin_fee_bps: 250` (2.5%) + `admin_fee_fixed_cents: 100` (R$ 1.00) per charge, deducted from the amount paid before conversion. `POST /cashin/preview` shows the maths without creating anything:

```
POST /cashin/preview {"amount_cents":10000,"asset":"usdt","chain":"polygon"}
→ {"amount_cents":10000,"admin_fee_bps":250,"admin_fee_fixed_cents":100,"fee_total_cents":348,"net_cents":9652,"brl_per_usdt":"5.11","usdt_amount":"18.874787"}
```

Different commercial terms (fee, per-payer ceilings, holding period) are configured per key by Lunium and show up in `GET /keys/me`.

## 4. Balance deposit (custody) and withdrawals

For wallet apps that need a "deposit and withdraw" tab in reais. The key needs custody enabled by Lunium (`custodia_ativa: true` in `GET /saldo`) — ask in the group. No crypto leaves: the paid PIX becomes a BRL balance in your customer's sub-account (`customer_ref`).

| Step | Call | Rule |
|---|---|---|
| Deposit | `POST /cashin/charge` with `"destino":"saldo"` and `"customer_ref":"your-customer-id"` | Deposit fee **0%**. Same payer ladder (200 / 6,000 per day per CPF). |
| Hold above R$ 60 | automatic | **Up to R$ 60.00:** credited immediately, available on **D+1** (`disponivel_em`). **Above R$ 60.00 per deposit:** the QR is issued **held for 24h at the provider** (`held:true`); once paid, the balance shows as **frozen** (`delayed`) and, when the provider releases it, it becomes **available immediately** — the hold counts as the holding period. It is the window for a dispute (MED) before the money leaves. |
| Query | `GET /saldo?customer_ref=…` · `GET /saldo/extrato` | `disponivel_cents`, `bloqueado_cents`, `proximas_liberacoes`, `carencia_ate`, fees (`deposito_fee_bps`, `saque_fee_bps`, `retencao_acima_cents`). Append-only ledger: balance = sum. |
| Withdraw by PIX | `POST /payouts` with the same `customer_ref`, `amount_cents`, `pix_key`, `tax_number` | Debits amount + **1.8%** (house fee) + provider fee (~R$ 1.00) before the PIX leaves; `402 saldo_insuficiente` if short. The PIX key must belong to the `tax_number` (bank rule). The PIX lands within 24h, usually seconds. |
| Withdraw as crypto | `POST /saldo/sacar-cripto` {amount_cents, chain, asset, payout_address, tax_number, customer_ref} | Reais leave the balance, crypto goes to the customer's wallet through the same rail as cash-in; the response is a charge already `paid` — follow `settlement_status`. |
| 1st deposit | — | The first deposit of a sub-account blocks any withdrawal for 24h: `423 carencia_primeiro_deposito` with `libera_em`. |

Nothing is advanced and no fee is absorbed: what the provider charges is what the balance pays. Charges carry `fonte` (`pix` | `saldo`) and `destino` (`cripto` | `saldo`) so a withdrawal is never confused with a purchase.

## 5. Cash-out: the customer delivers crypto → a PIX key receives reais

Three steps, in this order. The quote is already **net**: `brl_amount` is exactly what lands on the PIX key.

```
1) POST /cash-outs                       (quote — commits nothing)
{
  "asset": "usdt", "network": "polygon",
  "brl_amount": "250.00",                 // OR "amount": "50.77" (crypto) — exactly one of the two
  "pix_key": "customer@example.com",      // CPF · CNPJ · +55AREANUMBER · e-mail · UUID; 11 bare digits require pix_key_type
  "refund_address": "0x51e3d4…bc75e0",    // the customer's RETURN wallet, same network — always send it
  "external_id": "sale-001"
}
→ 201 (real sandbox response)
{ "cashout_id": "sbx_141ce3e9…", "state": "QUOTE_CREATED", "amount": "50.774309", "brl_amount": "250.00",
  "pix_key_type": "email", "deposit_address": null, "expires_at": "2026-09-03T02:33:31Z", … }

2) POST /cash-outs/{cashout_id}/accept    (locks the quote — point of no return)
→ { "state": "AWAITING_DEPOSIT", "deposit_address": "0x2952552e…", "deposit_tag": null, … }
   The customer sends EXACTLY "amount" to deposit_address, on the quoted network.

3) GET /cash-outs/{cashout_id}            (follow it; or wait for the webhook)
→ { "state": "COMPLETED", "pix_e2e": "E0000000020260903…", "pix_paid_at": "…",
    "receipt_url": "https://app.luniumpay.com/comprovante/…", "receipt_pdf_url": "….pdf",
    "verify_url": "https://api.luniumpay.com/v1/verificar/E…", "refund_tx_hash": null, "deposit_from": "0x…" }
```

- **Before accepting, show who will receive:** `GET /pix/keys/lookup?key=…` returns the holder's name, masked document and institution (DICT). 5 lookups/min per account; unknown key → `404 chave_nao_encontrada`.

- **Never ask the seller for a CPF.** The PIX key already identifies the recipient; the field does not exist on the sale.

- `expires_at`: 15 min on Polygon, up to 300 min on other networks. Read the field; do not hard-code a window.

- If accept answers `503 liquidante_indisponivel` (`acao: esperar`, `retry_after_seconds: 120`): the PIX settlement provider is down at that instant; wait and **re-accept the same order**. No address is issued in the meantime, on purpose — crypto never enters a stalled pipeline.

- Paying a QR/copy-and-paste (`br_code`) instead of a key: **disabled in production today** (`503 br_code_indisponivel`). The sale pays PIX **keys**.

### 5.1 Limits, networks and timing

| Limit | Value | In the API |
|---|---|---|
| Per transaction | **R$ 6.00 to R$ 250,000.00** | a refusal returns `limits.min_amount` / `max_amount` already converted into the order's crypto |
| Per recipient per day | **R$ 100,000.00** per CPF/CNPJ (or per key, when the key is not a document). Resets at midnight, Brasília time. | `limits.max_brl_cents` = the **effective** ceiling (the lower of the two); `limits.max_brl_cents_por_transacao` = nominal. Exceeded: `limite_diario_recebedor`, `acao: esperar`. |
| Per key per day | **does not exist** on cash-out | the tier (R$ 5k/25k/100k) applies only to cash-in and payouts — `daily_limit_applies_to` in `GET /keys/me` |
| Minimums per asset | e.g. USDT 10, USDC 20 on the convert rail | `GET /catalog` → `min`/`max` per item |

| Network | Time from confirmed deposit to PIX paid |
|---|---|
| **Polygon** (USDT/USDC) — `fast` | our own settlement: **seconds**; measured 49–65 s from accept to PIX paid (the deposit itself takes ~10–20 s to confirm) |
| Other networks — `convert` (1,300+ assets via exchange) | the network's confirmation count: TON/Aptos ~1 min · Avalanche/NEAR ~2 min · BSC ~3 min · Base ~5 min · Ethereum ~20 min · Arbitrum ~21 min · Optimism ~1 h · Celo ~3 h. **Never promise "instant" outside Polygon.** |
| Solana | temporarily **out** of cash-out — the quote refuses (`rede_indisponivel`) |

### 5.2 States, refunds and receipt

`QUOTE_CREATED` → accept → `AWAITING_DEPOSIT` → `DEPOSIT_DETECTED` → `PROCESSING` → `COMPLETED`  ·  detours: `MANUAL_REVIEW` → `REFUNDING` → `REFUNDED` · `EXPIRED` · `FAILED`. Webhooks: `cashout.awaiting_deposit`, `deposit_detected`, `processing`, `under_review`, `refunding`, `completed`, `refunded`, `expired`, `failed`.

| Situation | What happens |
|---|---|
| The provider refuses the key after the deposit (invalid/non-existent) | `MANUAL_REVIEW` → the crypto is automatically returned to `refund_address` → `REFUNDED` with `refund_tx_hash`. The net amount the provider returned is refunded. |
| The send fails before leaving (settlement provider down, key rejected on submission) | retried every 5 min for up to 45 min; if it still does not settle, the **full** amount (fee included) returns to `refund_address`. While retrying the state is `PROCESSING`, not a failure. |
| PIX reversed after being paid (rare) | same path: `REFUNDED` + hash. |
| Deposit different from the quote | the amount actually received is paid; a different token goes to `MANUAL_REVIEW`. |
| Deposit after `expires_at` | the order is revived when the deposit shows up (up to 7 days); after that, `MANUAL_REVIEW`. |
| No `refund_address` | the refund goes to the on-chain origin of the deposit — **wrong** when it came from an exchange (that is the exchange's hot wallet). Hence: always send it. |
| Refund amount | automatic up to 5,000 USDT per order; above that a person from operations executes it and you receive the same `REFUNDED` + hash. Always on the deposit's network and token; the gas is ours. |

**Receipt — three forms of the same fact,** in the same response and the same webhook (never build URLs by hand): `pix_e2e` (Central Bank identifier), `receipt_url` (page), `receipt_pdf_url` (PDF with the recipient's name, document and institution, amount, date, E2E), `verify_url`. And **anyone**, with no key, can confirm the payment:

```
GET https://api.luniumpay.com/v1/verificar/{e2e}
→ { "verificado": true, "pago": true, "valor_brl": "250.00", "pago_em": "2026-09-03T02:18:47Z",
    "recebedor_iniciais": "T. S. T.", "instituicao": "…", "liquidado_por": "Lunium", "comprovante_url": "…" }
  (does not expose the PIX key, full name or document)
```

### 5.3 Sandbox: deterministic triggers

The **first two decimals of `amount`** (in crypto) choose the outcome — you can assert on it in CI:

| `amount` ends in | What happens | What it proves |
|---|---|---|
| `.01` | goes to `delayed` and completes on its own | your code does not call a hold a failure |
| `.02` | fails | your error path runs |
| `.03` | the quote expires in 5 s | you re-quote instead of insisting |
| `.04` | refused on limits with `limits` filled | you read `limits.min_amount`/`max_amount` |
| `.05` | completes in ~2 min | your polling is patient |
| `.06` | compliance refusal at settlement → `FAILED` with `failure_reason: pagador_recusado_pelo_provedor` | you show the reason and do not retry |
| `.07` | 1st accept → `503 provedor_indisponivel`; the 2nd accept of the **same** order works | your retry re-accepts instead of re-quoting |
| `.08` | refused at creation by `limite_do_pagador` | you read `payer_limits` |

Reusing an `external_id` with a different destination returns **409**, same as production. Sandbox orders vanish after 2 h. Never send crypto to a sandbox `deposit_address`: it has no owner.

## 6. Payouts (direct PIX, no crypto leg)

`POST /payouts` sends a PIX to a key. It requires `payout_enabled` on the key (born disabled; ask in the group). With custody, the amount leaves the sub-account's balance (`customer_ref`); without custody, it leaves Lunium's treasury as agreed commercially. Limits per operation: R$ 1.00 to R$ 6,000.00; counts towards the tier's daily quota.

```
POST /payouts { "amount_cents": 5000, "pix_key": "+5548999990000", "tax_number": "12345678901",
                "external_id": "withdrawal-001", "customer_ref": "customer-42" }
→ { "payout_id": "po_…", "status": "processing",   // processing | sent | failed | refunded
    "amount_cents": 5000, "fee_cents": 100, "pix_key": "+5548*****0000", "sent_at": null, … }
Webhooks: payout.sent · payout.failed · payout.refunded
```

## 7. Webhooks

One envelope for every event. `data` is exactly the object the status `GET` returns for that resource.

```
POST {webhook_url}
X-Lunium-Event-Id: evt_9f2c…          // id of the EVENT — stable across retries: deduplicate by it
X-Lunium-Delivery-Id: dlv_…           // id of the ATTEMPT — quote it to support
X-Lunium-Signature: t=1756850000,v1=<hex HMAC-SHA256(secret, "t.body")>
{ "event": "cashin.settled", "event_id": "evt_9f2c…", "created_at": "2026-09-02T23:10:11.000Z",
  "data": { "cashin_id": "ci_…", "status": "paid", "settlement_status": "sent", "settlement_tx_hash": "0x…", … } }
```

- **Verify the v2 signature** and reject anything older than ~5 minutes. The old `X-Webhook-Signature` header (body only) stops being sent on **2026-11-09**; keys created after 2026-08-08 no longer receive it.

- Answer **2xx within 15 s**. Without a 2xx: 12 attempts with backoff (~7 h). A delivery that runs out shows as `failed` in `GET /webhooks/deliveries` and is recoverable with `POST /webhooks/deliveries/{event_id}/retry`. Nothing is lost silently: the bot also warns in the group.

- Treat unknown events as a no-op — the list grows without notice.

- `POST /webhooks/test` fires `webhook.test` over the same transport and returns what YOUR endpoint answered. Run it before the first real charge.

- Prefer webhooks to polling: one query per second alone consumes the key's 60 req/min (`429` is protection, not failure).

| Cash-in | Cash-out | Payout / other |
|---|---|---|
| `cashin.held` (QR issued with a hold) · `cashin.delayed` · `cashin.paid` · `cashin.settled` · `cashin.settlement_failed` · `cashin.expired` · `cashin.refunded` · `cashin.failed` | `cashout.awaiting_deposit` · `cashout.deposit_detected` · `cashout.processing` · `cashout.under_review` · `cashout.refunding` · `cashout.completed` · `cashout.refunded` · `cashout.expired` · `cashout.failed` | `payout.sent` · `payout.failed` · `payout.refunded` · `webhook.test` |

```
// Node — verifying the v2 signature
const [t, v1] = req.get('X-Lunium-Signature').split(',').map(p => p.split('=')[1]);
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now()/1000 - t) < 300;
```

## 8. Errors: the field that tells you what to do

Every error carries `erro` (a stable code), `acao`, `detail`, `request_id` and `agent_guidance`. **Branch on `acao`**, not on the message:

| `acao` | Meaning | Examples of `erro` |
|---|---|---|
| **corrigir** | the request is wrong; repeating it unchanged never passes | `campos_obrigatorios` · `amount_invalido` (decimal as a **string**) · `pix_key_invalida` · `tipo_ambiguo` (11 digits: send `pix_key_type`) · `valor_abaixo_do_minimo` / `valor_acima_do_maximo` (read `limits`) · `limite_do_pagador` · `rede_indisponivel` · `nao_encontrado` · `campos_conflitantes` |
| **repetir** | transient failure on our side | `provedor_indisponivel` |
| **esperar** | a quota renews or a service comes back | `limite_diario` (tier, cash-in/payouts only) · `limite_diario_recebedor` (sale, R$ 100k/day) · `liquidante_indisponivel` (`retry_after_seconds`) · `br_code_indisponivel` · `cashout_indisponivel` |
| **parar** | do not insist; show it to the user / talk to us | `chave_ausente` · `chave_incorreta` (key cut when copying) · `chave_desconhecida` · `pagador_recusado_pelo_provedor` · `sandbox_sem_cashin` |

Every timestamp is ISO 8601 with an explicit offset/timezone (`2026-08-01T22:35:49-03:00` or `…Z`). Never assume local time or UTC.

## 9. Summary table of limits and fees (production, 2026-09-02)

| Product | Per operation | Per person | Per key | Default fee | Measured time |
|---|---|---|---|---|---|
| **Cash-in** (PIX → crypto) | R$ 1.00 to R$ 6,000.00 | payer (CPF/CNPJ): up to R$ 200/day instant · R$ 200 to R$ 6,000/day with a 24h hold · from the first operation | tier: R$ 5,000 → 25,000 → 100,000 per day (cash-in + payouts) | 2.5% + R$ 1.00 | USDT 2 s · USDC 7 s after the PIX lands (Polygon) |
| **Balance deposit** | same as cash-in | same; **above R$ 60 per deposit it is held 24h** (frozen) and then released as available; up to R$ 60 available on D+1 | same | 0% on deposit · withdrawal 1.8% + ~R$ 1.00 | PIX withdrawal: seconds, up to 24h |
| **Cash-out** (crypto → PIX) | R$ 6.00 to R$ 250,000.00 | recipient: R$ 100,000/day (CPF/CNPJ or key) | **no daily cap** | already inside the quote (`brl_amount` is net) | Polygon: 49–65 s from accept to PIX paid; other networks: the network's time |
| **Payout** (direct PIX) | R$ 1.00 to R$ 6,000.00 | — | tier | provider ~R$ 1.00 (+1.8% when it leaves the balance) | seconds, up to 24h |
| Requests | 60 per minute per key (configurable) · a cash-in QR is valid for 15 min · a cash-out quote is valid for 15 min on Polygon |  |  |  |  |

Every number here can change; `GET /keys/me` returns the ones that apply to your key. Specific commercial terms (fee, per-payer ceilings, holding period) are agreed with Lunium and reflected on the key.

## 10. Go-live checklist

1. Production key created (`POST /keys`) and stored in a vault; never in chat.

2. Operations group with @LuniumNotifyBot + @luniumB2B, `/vincular` done by an admin; `/configurar` shows everything ✅.

3. `settlement_address` (Polygon) and `webhook_url` configured via `PATCH /keys/me`; `POST /webhooks/test` answered 2xx.

4. Webhook verifies `X-Lunium-Signature` v2, deduplicates by `event_id`, ignores unknown events, answers in < 15 s.

5. Cash-in: the front end queries `GET /cashin/limits` before charging, shows `held`/`held_hours`, treats `delayed` as "paid, releases at …" and credits only on `cashin.settled` (or `paid` with `settlement_status: sent`). Tested with a real PIX of R$ 1 to R$ 5.

6. Cash-out: quote → show the holder (`/pix/keys/lookup`) and the amount → accept → show the exact `deposit_address` and `amount`; always sends `refund_address` and `external_id`; polls every 10–15 s or uses the webhook; handles `503 liquidante_indisponivel` by re-accepting the same order. Triggers `.01`–`.08` passed in the sandbox.

7. Errors branched by `acao`; `limits` read from the response instead of hard-coded; `expires_at` read instead of a fixed window.

8. Joined the partner channel (`https://t.me/+FerOZMRRP_g5MDk5`) and follows `docs.luniumpay.com/status`.

## 11. Links and channels

|  |  |
|---|---|
| API | `https://api.luniumpay.com` · OpenAPI `/openapi.json` · `/quickstart` · `/llms.txt` · MCP `/mcp` · health `/ping` |
| Docs | `https://docs.luniumpay.com/en/` · `/en/comecar` · `/status` |
| Code | `github.com/guilhermezanqueta-collab/lunium-examples` · `github.com/guilhermezanqueta-collab/lunium-agent-skills` |
| Operations | your project's group (Lunium <> YourProject) with @LuniumNotifyBot · human: @luniumB2B · partner channel `t.me/+FerOZMRRP_g5MDk5` · e-mail `contato@luniumpay.com` |

---

Lunium API · manual generated on 2026-09-02 from the production service (OpenAPI 1.12.0, `GET /keys/me`, live catalogs and measurements from the last 30 days). Limits and verification proportional to volume, as per Lunium's policy.
