# Lexxen Hub API — LLM Reference

> Reference voltada para LLMs (Claude Code, GPT, Cursor, etc).
> Fonte de verdade: `routes/api.php`, FormRequests e controllers do repo.
> Versao: v1 | Atualizado: 2026-05-08

## Base

- Base URL: `https://api.lexxen.com`
- Prefixo: `/api/v1`
- Content-Type: `application/json`
- Charset: UTF-8
- Autenticacao: HMAC-SHA256 em todos os endpoints `/api/v1/*`
- Webhook publico (entrada Brasil Bitcoin): `POST /api/webhooks/brasil-bitcoin` (sem auth)

## Autenticacao (HMAC-SHA256)

### Headers obrigatorios

| Header | Valor |
|---|---|
| `X-API-Key` | API key (formato: `lxn_...`) |
| `X-Timestamp` | Unix epoch em **segundos** |
| `X-Signature` | HMAC-SHA256 hex do payload |
| `Content-Type` | `application/json` |
| `Accept` | `application/json` |

### Payload assinado

```
payload = timestamp + METHOD + path + raw_body
signature = hex( HMAC-SHA256(payload, api_secret) )
```

Regras:
- `timestamp`: exatamente o mesmo valor de `X-Timestamp`
- `METHOD`: UPPERCASE (`GET`, `POST`, `DELETE`)
- `path`: caminho **sem** barra inicial e **sem** query string. Exemplo: `v1/payouts`, `v1/balance`. Para o request real `https://api.lexxen.com/api/v1/payouts`, use `path = "v1/payouts"` (mas o backend tambem aceita `api/v1/payouts` — o que importa e usar exatamente a mesma string que o servidor recebe via Laravel `$request->path()`)
- `raw_body`: para `GET`/`DELETE` use string vazia (`""`); para `POST` use o **JSON literal** que sera enviado (mesma string, sem reformatar)

### Janela e replay

- `X-Timestamp` deve estar dentro de **±60 segundos** do tempo do servidor
- Cada combinacao `api_key + timestamp + signature` so pode ser usada uma vez (anti-replay 120s)
- Codigo de erro: `401 Authentication failed.`

### Snippet PHP

```php
$ts = (string) time();
$method = 'POST';
$path = 'v1/payouts';
$body = json_encode(['type' => 'PIX', 'amount' => 100, 'destination' => 'a@b.com']);
$signature = hash_hmac('sha256', $ts . $method . $path . $body, $apiSecret);
// Headers: X-API-Key, X-Timestamp=$ts, X-Signature=$signature
```

### Snippet Node

```js
const ts = String(Math.floor(Date.now() / 1000));
const body = JSON.stringify({ type: 'PIX', amount: 100, destination: 'a@b.com' });
const sig = crypto.createHmac('sha256', apiSecret).update(ts + 'POST' + 'v1/payouts' + body).digest('hex');
```

### IP whitelist

Se o tenant tiver IPs whitelistados, requests de IP fora da lista retornam `403`.

---

## Idempotency-Key

### Endpoints que **exigem** o header `Idempotency-Key` (POST)

Ausencia do header retorna `422 Header Idempotency-Key obrigatorio para operacoes financeiras.`

- `POST /v1/payouts`
- `POST /v1/payouts/{uuid}/approve`
- `POST /v1/crypto/send`
- `POST /v1/fiat/deposit`
- `POST /v1/fiat/withdraw`
- `POST /v1/otc/buy/execute`
- `POST /v1/otc/sell/execute`
- `POST /v1/pix-to-wallet`
- `POST /v1/pix-to-wallet/{uuid}/approve`
- `POST /v1/pix-to-wallet/{uuid}/reject`

### Comportamento

- Reenvio com mesma `Idempotency-Key` em ate **24h** retorna a resposta original (mesmo status, mesmo body), com header extra `X-Idempotency-Replayed: true`
- Persistido em DB + cache (sobrevive a flush de cache)
- Em outros endpoints o header e opcional mas respeitado se enviado

---

## Tipos e Enums

### Assets crypto suportados
`USDT`, `BTC`, `ETH`

### Networks crypto suportadas
`trx`, `eth`, `polygon` (alguns endpoints aceitam tambem `bitcoin`/`btc`)

### TransactionType
`PIX`, `CRYPTO`

### TransactionRequestStatus (fluxo)
`PENDING_APPROVAL → APPROVED → PROCESSING → COMPLETED`
Falha: `... → FAILED` | Rejeicao: `PENDING_APPROVAL → REJECTED`

> O enum tambem tem `REQUESTED`, mas a API sempre cria solicitacoes ja em `PENDING_APPROVAL`. Trate como o estado inicial efetivo.

### Quote types (crypto)
`market` (preco do provider), `lexxen` (com markup Lexxen), `client_final` (com markup do tenant — default)

### PixToWalletStatus (fluxo)
`AWAITING_DEPOSIT → PENDING_APPROVAL → APPROVED → PROCESSING → COMPLETED`

Falhas e ramos terminais:
- `AWAITING_DEPOSIT → EXPIRED` (PIX nao caiu antes de `expires_at`)
- `PENDING_APPROVAL → REJECTED` (tenant rejeitou; BRL fica como saldo)
- `PROCESSING → FAILED_POST_DEPOSIT` (conversao OU envio falhou apos PIX cair; BRL fica como saldo, email + KPI disparados)

> Aprovacao humana e **sempre** exigida apos o PIX cair (nao ha auto-execucao). Cotacao BRL→cripto e tirada **no momento que o PIX cai**, nao no request inicial — tenant passa `min_amount_out` para se proteger de slippage.

---

## Endpoints

### Payouts (PIX/Crypto com dupla autorizacao)

#### `POST /v1/payouts` — Criar solicitacao

Headers extras: `Idempotency-Key` (obrigatorio).

Request body:
```json
{
  "type": "PIX",
  "amount": 100.00,
  "destination": "user@example.com",
  "asset": "USDT",
  "network": "trx",
  "external_id": "order-123",
  "description": "Pagamento fornecedor",
  "requires_approval": true
}
```

Validacao:
- `type` (enum `PIX|CRYPTO`, **required**)
- `amount` (numeric, > 0, **required**)
- `destination` (string, max 500, **required**)
- `asset` (string, **required se type=CRYPTO**)
- `network` (string, opcional)
- `external_id` (string, max 255, opcional)
- `description` (string, max 1000, opcional)
- `requires_approval` (boolean, opcional)

Response `201`:
```json
{
  "data": {
    "id": "uuid",
    "type": "PIX",
    "amount": "100.00",
    "fee": "0.50000000",
    "fee_currency": "BRL",
    "total": "100.50000000",
    "asset": null,
    "network": null,
    "destination": "user@example.com",
    "external_id": "order-123",
    "description": "Pagamento fornecedor",
    "amount_brl": null,
    "used_price": null,
    "markup_percent": null,
    "quote_type": null,
    "status": "PENDING_APPROVAL",
    "requires_approval": true,
    "created_by": "api",
    "approved_by": null,
    "created_at": "2026-05-04T12:00:00+00:00",
    "updated_at": "2026-05-04T12:00:00+00:00"
  }
}
```

Erros: `422` (validacao ou regra de negocio).

#### `GET /v1/payouts` — Listar

Query: `per_page=20` (default).

#### `GET /v1/payouts/{id}` — Detalhe

Response: mesmo shape de `data` acima. `404` se nao encontrado.

#### `POST /v1/payouts/{uuid}/approve` — Aprovar

Headers extras: `Idempotency-Key` (obrigatorio).
Body: vazio.
Response `200` com o mesmo shape de payout. Dispara execucao real da transacao.
Erros: `422` (saldo insuficiente, status invalido), `404` (nao encontrado).

---

### Crypto

#### `GET /v1/crypto/quote` — Cotacao

Query:
- `asset` (string `USDT|BTC|ETH`, **required**)
- `markup_percent` (numeric 0–100, opcional, default 0)
- `amount_brl` (numeric > 0.01, opcional)
- `network` (string `trx|eth|polygon`, opcional)
- `quote_type` (string `market|lexxen|client_final`, opcional, default `client_final`)

Response `200`:
```json
{ "data": { "asset": "USDT", "price": "5.45", "price_with_markup": "5.50", "markup_percent": 1, "amount_brl": 100, "amount_crypto": "18.18181818", "network": "trx", "quote_type": "client_final" } }
```

#### `POST /v1/crypto/send` — Enviar crypto

Headers extras: `Idempotency-Key` (obrigatorio).

Request body:
```json
{
  "amount": "10.5",
  "amount_brl": null,
  "asset": "USDT",
  "network": "trx",
  "destination": "TPxxxxxx...",
  "description": "Pagamento",
  "markup_percent": 0,
  "quote_type": "client_final",
  "auto_convert": false
}
```

Validacao:
- `amount` (numeric, min `0.00000001`) **OU** `amount_brl` (numeric, min `0.01`) — exatamente um dos dois
- `asset` (string, **required**, valores: `USDT|BTC|ETH`)
- `network` (string, **required**, valores: `trx|eth|polygon`)
- `destination` (string, **required**)
- `description` (string, max 255, opcional)
- `markup_percent` (numeric 0–100, opcional)
- `quote_type` (`market|lexxen|client_final`, opcional)
- `auto_convert` (boolean, opcional) — quando `true` e `amount_brl < R$ 80,00` retorna `422`

Response `201`: mesmo shape do `payout` acima (TransactionRequestResource), com `type=CRYPTO` e campos de cotacao preenchidos quando `amount_brl` foi usado.

Notas:
- Enviar `amount` e `amount_brl` simultaneamente retorna `422`
- Auto-conversao usa OTC interno (BRL → crypto) com minimo R$ 80,00

#### `GET /v1/crypto/wallets` — Wallets do tenant

Response `200`:
```json
{ "data": { "wallets": [ { "asset": "USDT", "network": "trx", "address": "Txxx..." } ] } }
```

#### `POST /v1/crypto/wallets/address` — Garantir endereco de deposito

Request body:
```json
{ "coin": "USDT", "network": "trx" }
```

Validacao:
- `coin` (`USDT|BTC|ETH`, **required**)
- `network` (`trx|eth|polygon|bitcoin|btc`, **required**)

Response (existia): `200`
```json
{ "data": { "address": "Txxx...", "coin": "USDT", "network": "trx", "created": false } }
```

Response (criado agora): `201`
```json
{ "data": { "address": "Txxx...", "coin": "USDT", "network": "trx", "created": true } }
```

Erros: `422` (`PROVIDER_UNAVAILABLE`, `NOT_SUPPORTED`), `502` (`ADDRESS_GENERATION_FAILED`).

#### `GET /v1/crypto/balance` — Saldo crypto

Response `200`:
```json
{ "data": { "balances": [ { "asset": "USDT", "free": "1000.00", "pending_fees": "0.00000000", "available": "1000.00000000" } ] } }
```

Apenas assets com `free > 0` sao retornados. `pending_fees` desconta taxas de transacoes em PROCESSING.

#### `GET /v1/crypto/status/{id}` — Status de transacao crypto

`{id}` = uuid de TransactionRequest.

Response `200` (com hash):
```json
{ "data": { "transaction_request_id": "uuid", "provider_transaction_id": "12345", "status": "completed", "hash": "0xabc...", "amount": "10.5", "coin": "USDT", "network": "trx", "fee": "1" } }
```

Response `200` (cache local sem hash):
```json
{ "data": { "transaction_request_id": "uuid", "provider_transaction_id": "12345", "status": "PROCESSING", "hash": null, "metadata": {} } }
```

`404` se request ou transaction nao encontrado.

---

### Fiat (BRL via PIX, mesmo titular)

Todas as operacoes Fiat exigem que o documento informado bata com o `tenant.document` (mesma titularidade). Caso contrario: `422 SAME_HOLDER_REQUIRED`.

#### `POST /v1/fiat/deposit` — Gerar QR Code PIX para receber BRL

Headers extras: `Idempotency-Key` (obrigatorio).

Request body:
```json
{ "value": 100.00, "payer_document": "12345678900", "custom_id": "ord12345" }
```

Validacao:
- `value` (numeric, min 1, **required**)
- `payer_document` (string, 11–18 chars, **required**) — deve bater com `tenant.document`
- `custom_id` (string, max **11**, **alfanumerico apenas** — limite Brasil Bitcoin)

Response `201`:
```json
{
  "data": {
    "id": 1,
    "value": "100.00",
    "payer_document": "12345678900",
    "custom_id": "ord12345",
    "payment_string": "00020126...",
    "qr_code": "data:image/png;base64,...",
    "status": "pending",
    "created_at": "2026-05-04T12:00:00+00:00"
  }
}
```

Erros: `422` (`SAME_HOLDER_REQUIRED`, `PROVIDER_ERROR`, `FIAT_NOT_SUPPORTED`), `500` (`INTERNAL_ERROR`).

#### `GET /v1/fiat/deposits` — Listar

Query: `per_page=50` (default).

#### `GET /v1/fiat/deposits/{id}` — Detalhe

`404` se nao encontrado.

#### `POST /v1/fiat/withdraw` — Saque PIX

Headers extras: `Idempotency-Key` (obrigatorio).

Request body:
```json
{ "value": 100.00, "pix_key": "user@example.com", "holder_document": "12345678900" }
```

Validacao:
- `value` (numeric, min 1, **required**)
- `pix_key` (string, max 255, **required**)
- `holder_document` (string, 11–18, **required**) — deve bater com `tenant.document`

Response `201`:
```json
{
  "data": {
    "id": 1,
    "value": "100.00",
    "pix_key": "user@example.com",
    "pix_key_type": "EMAIL",
    "holder_document": "12345678900",
    "withdraw_fee": "0.50",
    "status": "processing",
    "bank": "Banco X",
    "provider_id": "abc123",
    "processed_at": "2026-05-04T12:00:01+00:00",
    "created_at": "2026-05-04T12:00:00+00:00"
  }
}
```

Erros: `422` (`SAME_HOLDER_REQUIRED`, `INSUFFICIENT_BALANCE`, `FIAT_NOT_SUPPORTED`), `502` (`PROVIDER_ERROR`).

Dispara webhook `withdraw.fiat.completed` apos sucesso.

#### `GET /v1/fiat/withdrawals` — Listar
#### `GET /v1/fiat/withdrawals/{id}` — Detalhe

---

### OTC (compra/venda direta, sem envio)

Cotacoes OTC tem TTL de **5 segundos** e valor minimo de **R$ 80,00**.

#### `POST /v1/otc/buy/quote` — Cotar compra (BRL → crypto)

Request body:
```json
{ "asset": "USDT", "amount_brl": 100.00, "markup_percent": 0 }
```

Validacao:
- `asset` (`USDT|BTC|ETH`, **required**)
- `amount_brl` (numeric, min **80**, **required**)
- `markup_percent` (0–100, opcional)

Response `201`:
```json
{
  "data": {
    "id": "uuid",
    "side": "buy",
    "asset": "USDT",
    "pair": "USDTBRL",
    "amount_brl": "100",
    "amount_crypto": "18.18181818",
    "price": "5.45",
    "price_with_markup": "5.45",
    "markup_percent": "0",
    "markup_amount": "0.00000000",
    "net_amount": "18.18181818",
    "destination_currency": "USDT",
    "status": "pending",
    "provider_transaction_id": null,
    "expires_at": "2026-05-04T12:00:05+00:00",
    "executed_at": null,
    "created_at": "2026-05-04T12:00:00+00:00"
  }
}
```

#### `POST /v1/otc/buy/execute` — Executar compra

Headers extras: `Idempotency-Key` (obrigatorio).

Request body:
```json
{ "quote_id": "uuid" }
```

Response `200`: mesmo shape, com `status="executed"`, `provider_transaction_id`, `executed_at` preenchidos.

Erros possiveis (todos com `error` + `message`):
- `404 QUOTE_NOT_FOUND`
- `422 WRONG_QUOTE_SIDE` (cotacao e de venda)
- `422 QUOTE_ALREADY_PROCESSED` (status diferente de `pending`)
- `422 QUOTE_EXPIRED` (passou de 5s)
- `422 INSUFFICIENT_BALANCE` (BRL insuficiente)
- `502 OTC_EXECUTE_FAILED` (provider)

Dispara webhook `otc.buy.completed` em sucesso, `otc.failed` em falha de provider.

#### `POST /v1/otc/sell/quote` — Cotar venda (crypto → BRL)

Request body:
```json
{ "asset": "USDT", "amount": "20", "markup_percent": 0 }
```

Validacao:
- `asset` (`USDT|BTC|ETH`, **required**)
- `amount` (numeric, min `0.00000001`, **required**) — em **crypto**
- `markup_percent` (0–100, opcional)

Response: mesmo shape de buy quote (com `side="sell"`, `destination_currency="BRL"`).
Erro `422 OTC_MIN_BRL` se a cotacao gerar menos que R$ 80,00.

#### `POST /v1/otc/sell/execute` — Executar venda

Headers extras: `Idempotency-Key` (obrigatorio).
Body: `{ "quote_id": "uuid" }`. Mesmos erros do buy/execute, mas exige saldo crypto.
Dispara `otc.sell.completed` em sucesso.

#### `GET /v1/otc/quotes/{id}` — Detalhe da cotacao

---

### PIX-to-Wallet (BRL → cripto → wallet externa, com aprovacao humana)

Rota agregadora que orquestra `fiat/deposit` (gera QR PIX) + `otc/buy` (BRL→cripto) + `crypto/send` (envia para wallet) num unico fluxo. Sempre exige aprovacao apos o PIX cair. Idempotente.

**Maquina de estados:** `AWAITING_DEPOSIT → PENDING_APPROVAL → APPROVED → PROCESSING → COMPLETED`. Ramos: `EXPIRED`, `REJECTED`, `FAILED_POST_DEPOSIT`.

#### `POST /v1/pix-to-wallet` — Criar ordem (gera QR Code PIX)

Headers extras: `Idempotency-Key` (obrigatorio).

Request body:
```json
{
  "amount_brl": 1500.00,
  "asset": "USDT",
  "network": "trx",
  "destination_address": "TXY...wallet...",
  "min_amount_out": "499.50",
  "markup_percent": 0.5,
  "description": "Order #4231",
  "expires_in_seconds": 86400
}
```

Validacao:
- `amount_brl` (numeric, min `R$ 80,00`, **required**)
- `asset` (`USDT|BTC|ETH`, **required**)
- `network` (`trx|eth|polygon`, **required**)
- `destination_address` (string sem espacos, max 255, **required**)
- `min_amount_out` (numeric > 0, **required**) — piso de slippage. Cotacao retornando abaixo disso falha com `failure_reason=SLIPPAGE`.
- `markup_percent` (numeric 0–100, opcional)
- `description` (string max 255, opcional)
- `expires_in_seconds` (integer 300–259200, opcional, default 86400 = 24h)

Response `201`:
```json
{
  "data": {
    "id": "01j2k3...",
    "status": "AWAITING_DEPOSIT",
    "amount_brl_expected": "1500.00",
    "amount_brl_actual": null,
    "asset": "USDT",
    "network": "trx",
    "destination_address": "TXY...",
    "min_amount_out": "499.50000000",
    "amount_out": null,
    "pix": {
      "qr_code": "data:image/png;base64,...",
      "payment_string": "00020126...",
      "custom_id": "p2wAbCdEfGh"
    },
    "expires_at": "2026-05-09T15:00:00+00:00",
    "created_at": "2026-05-08T15:00:00+00:00"
  }
}
```

Erros:
- `422` — validacao, `IDEMPOTENCY_KEY_REQUIRED`, `PROVIDER_NOT_SUPPORTED`
- `502` — `PROVIDER_QR_FAILED`

#### `POST /v1/pix-to-wallet/{uuid}/approve` — Aprovar conversao+envio

Headers extras: `Idempotency-Key` (obrigatorio).
Body: vazio ou `{ "reason": "..." }` (opcional).

Pre-condicao: ordem precisa estar em `PENDING_APPROVAL` (PIX ja caiu).

Response `200`: mesmo shape, `status="APPROVED"`. Enfileira `ProcessPixToWalletJob`.

Erros: `404` (`ORDER_NOT_FOUND`), `409` (`INVALID_STATE`).

#### `POST /v1/pix-to-wallet/{uuid}/reject` — Rejeitar

Headers extras: `Idempotency-Key` (obrigatorio).
Body: `{ "reason": "..." }` (opcional).

Pre-condicao: `PENDING_APPROVAL`. BRL fica como saldo do tenant (nao ha refund automatico).

Response `200`: `status="REJECTED"`.

#### `GET /v1/pix-to-wallet/{id}` — Detalhe

Response: shape completo, incluindo `fiat_deposit_id`, `otc_quote_id`, `transaction_request_id`, `provider_send_id`, `blockchain_hash`, `failure_reason`, `failure_detail`, timestamps de cada transicao.

#### `GET /v1/pix-to-wallet` — Listar

Query: `status`, `asset`, `from`, `to`, `per_page` (default 15, max 100).

Response paginado padrao Laravel.

---

### Balance

#### `GET /v1/balance` — Saldo unificado

Response `200`:
```json
{
  "data": {
    "pix": { "balance": "1000.00", "pending_fees": 0.5, "available": 999.5 },
    "crypto": { "balances": [ { "asset": "USDT", "free": "100.00", "pending_fees": "0.00000000", "available": "100.00000000" } ] }
  }
}
```

Cada bloco pode ser `null` se o provider correspondente nao estiver disponivel.

---

### Webhooks (gerenciamento de assinaturas)

#### `GET /v1/webhooks` — Listar webhooks
#### `POST /v1/webhooks` — Criar webhook

Request body:
```json
{ "url": "https://meu-app.com/webhook", "allowed_ips": ["1.2.3.4"] }
```

Validacao:
- `url` (url, max 500, **required**) — URL bloqueada se apontar para enderecos privados/internos (SSRF guard)
- `allowed_ips` (array de IPs, opcional)

> **Nota:** o campo `events` **nao** existe — todo webhook recebe **todos** os eventos do tenant. Para filtrar, faca isso no seu lado pelo header `X-Webhook-Event`.

Response `201`:
```json
{
  "data": { "id": 1, "url": "https://meu-app.com/webhook", "allowed_ips": ["1.2.3.4"], "is_active": true, "created_at": "...", "updated_at": "..." },
  "secret": "abc123..."
}
```

> **Importante:** o `secret` so e retornado **uma unica vez** na criacao. Guarde-o para validar a assinatura HMAC.

#### `GET /v1/webhooks/{id}` — Detalhe (sem secret)
#### `DELETE /v1/webhooks/{id}` — Deletar (`204`)

#### `GET /v1/webhooks/{id}/deliveries` — Ultimas 100 entregas

Response `200`:
```json
{
  "data": [
    { "id": 1, "event": "transaction.completed", "response_code": 200, "response_body": "ok", "attempts": 1, "delivered_at": "...", "payload": { ... }, "created_at": "..." }
  ]
}
```

#### `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry` — Reenviar entrega

Response `200`:
```json
{ "message": "Webhook redisparado com sucesso.", "delivery_id": 1, "event": "transaction.completed" }
```

---

## Webhook Delivery (eventos enviados ao seu servidor)

Quando um evento ocorre, a Lexxen Hub faz `POST` para a `url` cadastrada com:

### Headers

| Header | Valor |
|---|---|
| `Content-Type` | `application/json` |
| `X-Webhook-Signature` | `HMAC-SHA256(body, webhook.secret)` em hex |
| `X-Webhook-Event` | nome do evento (ex: `transaction.completed`) |
| `X-Webhook-Idempotency-Key` | hash deterministico (use para deduplicar) |
| `User-Agent` | `LexxenHub/1.0` |

### Body envelope

```json
{
  "event": "transaction.completed",
  "data": { /* shape varia por evento — veja abaixo */ },
  "timestamp": "2026-05-04T12:00:00+00:00"
}
```

### Validacao da assinatura (PHP)

```php
$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expected, $request->header('X-Webhook-Signature'))) abort(401);
```

### Retry policy

- 5 tentativas com backoff: `10s, 30s, 60s, 5min, 15min`
- Sucesso = HTTP 2xx do seu endpoint
- Sem retry se URL falhar SSRF guard (privado/interno) — a entrega e marcada com erro e descartada
- Timeout: 15s, sem follow de redirects

### Eventos e payloads (`data`)

#### `transaction.created` — solicitacao criada
```json
{
  "request_id": "uuid",
  "type": "PIX",
  "amount": "100.00",
  "status": "PENDING_APPROVAL",
  "destination": "user@example.com",
  "created_by": "api",
  "fee_amount": 0.5,
  "description": "Pagamento",
  "pix_key_type": "EMAIL"
}
```
Para `type=CRYPTO`, em vez de `pix_key_type` traz `asset` e `network`.

#### `transaction.approved` — aprovada (mesma forma + `approved_by`)
```json
{ "request_id": "uuid", "type": "PIX", "amount": "100.00", "status": "APPROVED", "destination": "...", "approved_by": "api", "fee_amount": 0.5, "pix_key_type": "EMAIL" }
```

#### `transaction.completed` — PIX completed (imediato)
```json
{
  "request_id": "uuid",
  "type": "PIX",
  "amount": "100.00",
  "fee_amount": 0.5,
  "net_amount": 99.5,
  "status": "COMPLETED",
  "destination": "user@example.com",
  "provider_transaction_id": "abc123",
  "e2e_id": "E12345678202605041200a1b2c3d4e5f",
  "pix_key_type": "EMAIL"
}
```

#### `transaction.completed` — Crypto completed (apos polling de hash)
```json
{
  "request_id": "uuid",
  "type": "CRYPTO",
  "amount": "10.5",
  "fee_amount": "1",
  "network_fee": "0.1",
  "net_amount": "10.5",
  "status": "COMPLETED",
  "destination": "Txxx...",
  "provider_transaction_id": "12345",
  "asset": "USDT",
  "network": "trx",
  "blockchain_hash": "0xabc...",
  "blockchain_explorer": "https://tronscan.org/#/transaction/0xabc...",
  "auto_conversion": null
}
```

Ha tambem uma variante simplificada quando vem do webhook do provider Brasil Bitcoin (`provider: "BRASIL_BITCOIN"`):
```json
{ "transaction_id": "uuid", "status": "completed", "hash": "0xabc...", "provider": "BRASIL_BITCOIN" }
```

#### `transaction.failed`
```json
{
  "request_id": "uuid",
  "type": "PIX",
  "amount": "100.00",
  "fee_amount": 0.5,
  "status": "FAILED",
  "destination": "user@example.com",
  "error": "mensagem do provider",
  "pix_key_type": "EMAIL"
}
```

#### `otc.buy.completed` / `otc.sell.completed`
```json
{
  "quote_id": "uuid",
  "side": "buy",
  "asset": "USDT",
  "pair": "USDTBRL",
  "amount_brl": "100",
  "amount_crypto": "18.18181818",
  "price": "5.45",
  "price_with_markup": "5.50",
  "markup_percent": "1",
  "markup_amount": "0.18181818",
  "net_amount": "18.00000000",
  "destination_currency": "USDT",
  "provider_transaction_id": "12345",
  "executed_at": "2026-05-04T12:00:01+00:00"
}
```

#### `otc.failed`
```json
{ "quote_id": "uuid", "side": "buy", "asset": "USDT", "amount_brl": "100", "amount_crypto": "18.18", "error": "mensagem do provider" }
```

#### `deposit.fiat.received` — PIX recebido
```json
{
  "deposit_id": 1,
  "value": "100.00",
  "status": "credited",
  "bank": "Banco X",
  "payer_document": "12345678900",
  "custom_id": "ord12345",
  "provider_id": "abc123",
  "credited_at": "2026-05-04T12:00:00+00:00"
}
```

#### `withdraw.fiat.completed` — Saque PIX concluido
```json
{
  "withdrawal_id": 1,
  "value": "100.00",
  "pix_key": "user@example.com",
  "pix_key_type": "EMAIL",
  "holder_document": "12345678900",
  "withdraw_fee": "0.50",
  "status": "processing",
  "bank": "Banco X",
  "provider_id": "abc123",
  "processed_at": "2026-05-04T12:00:01+00:00"
}
```

#### `deposit.crypto.received` — Crypto recebido (deposito on-chain)
```json
{
  "provider_id": "12345",
  "amount": "10.5",
  "asset": "USDT",
  "network": "trx",
  "network_name": "Tron [TRC-20]",
  "address": "Txxx...",
  "blockchain_hash": "0xabc...",
  "status": "credited",
  "received_at": "2026-05-04T12:00:00+00:00"
}
```

#### `pix_to_wallet.created` / `.deposit_received` / `.approved` / `.rejected` / `.completed` / `.failed_post_deposit` / `.expired`

Todos os eventos `pix_to_wallet.*` compartilham o mesmo envelope `data`. Campos preenchidos progressivamente conforme a ordem avanca.

```json
{
  "order_id": "01j2k3...",
  "status": "PENDING_APPROVAL",
  "asset": "USDT",
  "network": "trx",
  "destination_address": "TXY...",
  "amount_brl_expected": "1500.00",
  "amount_brl_actual": "1500.00",
  "amount_out": null,
  "min_amount_out": "499.50000000",
  "blockchain_hash": null,
  "failure_reason": null,
  "description": "Order #4231",
  "custom_id": "p2wAbCdEfGh",
  "expires_at": "2026-05-09T15:00:00+00:00",
  "deposit_received_at": "2026-05-08T15:10:00+00:00",
  "approved_at": null,
  "completed_at": null,
  "created_at": "2026-05-08T15:00:00+00:00"
}
```

Quando assinar:
- `pix_to_wallet.created` — voce gerou a ordem; QR pronto para pagamento.
- `pix_to_wallet.deposit_received` — PIX caiu, ordem aguarda aprovacao manual.
- `pix_to_wallet.approved` — voce aprovou; conversao + envio em andamento.
- `pix_to_wallet.rejected` — voce rejeitou; BRL fica como saldo.
- `pix_to_wallet.completed` — envio crypto concluido; `blockchain_hash` populado.
- `pix_to_wallet.failed_post_deposit` — falhou apos PIX cair; BRL fica como saldo (`failure_reason`: `SLIPPAGE`, `OTC_FAILED`, `SEND_FAILED`, `PROVIDER_UNAVAILABLE`, `JOB_EXCEPTION`).
- `pix_to_wallet.expired` — PIX nao caiu antes de `expires_at`.

---

## Codigos de erro

| HTTP | Significado |
|---|---|
| `200` | OK |
| `201` | Criado |
| `204` | Sem conteudo (delete) |
| `400` | Argumento invalido (raro — usado em `crypto/quote` para erros estruturais do provider) |
| `401` | Authentication failed (HMAC invalido, timestamp fora da janela, replay) |
| `403` | Authentication failed (tenant inativo ou IP fora da whitelist) |
| `404` | Recurso nao encontrado |
| `422` | Validacao falhou ou regra de negocio (saldo, status invalido, mesmo titular, idempotency-key ausente, etc) |
| `500` | Erro interno (fallback) |
| `502` | Erro do provider (`OTC_EXECUTE_FAILED`, `PROVIDER_ERROR`, `ADDRESS_GENERATION_FAILED`) |

### Codigos de erro de negocio (string em `error`)

- `SAME_HOLDER_REQUIRED` — documento informado != tenant.document
- `INSUFFICIENT_BALANCE` — saldo insuficiente (resposta inclui `available` na message)
- `PROVIDER_UNAVAILABLE` / `PROVIDER_ERROR` — provider externo
- `NOT_SUPPORTED` / `FIAT_NOT_SUPPORTED` / `OTC_NOT_SUPPORTED` — feature indisponivel para o provider configurado
- `QUOTE_NOT_FOUND` / `QUOTE_EXPIRED` / `QUOTE_ALREADY_PROCESSED` / `WRONG_QUOTE_SIDE` — fluxo OTC
- `OTC_QUOTE_FAILED` / `OTC_EXECUTE_FAILED` / `OTC_MIN_BRL` — fluxo OTC
- `ADDRESS_GENERATION_FAILED` — geracao de endereco crypto
- `INTERNAL_ERROR` — fallback de exception nao tratada
- `NOT_FOUND` — recurso ausente em endpoints fiat

---

## Fluxos comuns

### 1. Pagamento PIX com aprovacao
1. `POST /v1/payouts` `{type:"PIX", amount, destination}` → `201` com `status="PENDING_APPROVAL"`
2. `POST /v1/payouts/{id}/approve` → `200` com `status="APPROVED"`
3. Voce recebe webhook `transaction.created` (1), depois `transaction.approved` (2), depois `transaction.completed` ou `transaction.failed`

### 2. Envio crypto pagando em BRL (auto-OTC)
1. `POST /v1/crypto/send` `{amount_brl, asset, network, destination, auto_convert: true}` (min R$ 80) → `201`
2. `POST /v1/payouts/{id}/approve` → executa OTC interno BRL→crypto, depois envia
3. Webhook `transaction.completed` traz `blockchain_hash` apos confirmacao on-chain

### 3. Compra OTC pura (sem envio)
1. `POST /v1/otc/buy/quote` → `data.id` valida por **5s**
2. `POST /v1/otc/buy/execute` `{quote_id}` antes de expirar
3. Webhook `otc.buy.completed`

### 4. Receber PIX
1. `POST /v1/fiat/deposit` `{value, payer_document}` → QR Code
2. Pagador faz o PIX
3. Voce recebe `deposit.fiat.received`

### 5. Receber crypto
1. `POST /v1/crypto/wallets/address` `{coin, network}` → endereco
2. Cliente envia para o endereco
3. Voce recebe `deposit.crypto.received` com `blockchain_hash`

### 6. PIX-to-Wallet (BRL → cripto → wallet externa em 4 passos)

```bash
# 1. Criar ordem (gera QR PIX)
curl -X POST https://api.lexxen.com/api/v1/pix-to-wallet \
  -H "X-API-Key: lxn_..." -H "X-Timestamp: ..." -H "X-Signature: ..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount_brl":1500,"asset":"USDT","network":"trx","destination_address":"TXY...","min_amount_out":"499.50"}'
# → 201 com data.pix.qr_code e data.pix.payment_string

# 2. Pagador faz o PIX usando o copia-e-cola

# 3. Voce recebe webhook pix_to_wallet.deposit_received → aprovar
curl -X POST https://api.lexxen.com/api/v1/pix-to-wallet/{order_id}/approve \
  -H "X-API-Key: ..." -H "X-Timestamp: ..." -H "X-Signature: ..." \
  -H "Idempotency-Key: $(uuidgen)"
# → 200 com status="APPROVED"

# 4. Voce recebe pix_to_wallet.completed com blockchain_hash
curl https://api.lexxen.com/api/v1/pix-to-wallet/{order_id} \
  -H "X-API-Key: ..." -H "X-Timestamp: ..." -H "X-Signature: ..."
# → status="COMPLETED", blockchain_hash="0x..."
```

Eventos webhook nesta ordem: `pix_to_wallet.created` → `pix_to_wallet.deposit_received` → `pix_to_wallet.approved` → `pix_to_wallet.completed`. Em qualquer falha apos o PIX cair, o BRL fica como saldo do tenant (consultavel via `GET /v1/balance`).

---

## Checklist de implementacao

- [ ] Armazenar `api_secret` apenas no servidor (nunca expor no client)
- [ ] Sincronizar relogio do servidor (NTP) — janela e ±60s
- [ ] Gerar UUID/hash unico para `Idempotency-Key` por intencao de transacao
- [ ] Reenviar com **mesma** `Idempotency-Key` em caso de timeout/erro de rede
- [ ] No webhook receiver: validar `X-Webhook-Signature` com HMAC do **raw body**
- [ ] No webhook receiver: deduplicar pelo `X-Webhook-Idempotency-Key`
- [ ] No webhook receiver: responder `2xx` rapido — processar de forma assincrona se precisar
- [ ] Tratar `X-Idempotency-Replayed: true` como sinal de retry bem-sucedido (nao processar de novo)
