# Saldo e saques

> Consultar saldo e pedir saque por Pix ou cripto pela API.

Fonte: https://app.thronecorporate.com/docs/saques

> [!WARNING]
> Não existe ambiente de testes para saque. Qualquer chave com a permissão de saque, inclusive `pk_test_`, transfere dinheiro de verdade.

## Saldo

`GET /balance` (permissão `balance:read`):

```bash
curl https://app.thronecorporate.com/api/v1/balance \
  -H "Authorization: Bearer $THRONE_PK:$THRONE_SK" \
  -H "Accept: application/json"
```

```json
{
  "balance_cents": 543210,
  "pending_balance_cents": 0,
  "reserved_cents": 50000,
  "available_cents": 543210,
  "currency": "BRL"
}
```

| Campo | O que é |
|---|---|
| `available_cents` | Saldo livre para saque pela API (igual a `balance_cents`) |
| `reserved_cents` | Valor preso em saques em andamento |
| `pending_balance_cents` | Saldo ainda pendente |
| `currency` | Sempre `BRL` |

O saldo da API é o **saldo Pix** da conta. O dinheiro de vendas no cartão (Sterling, Imperium, NovaCorp, Eminence, Apex, ShadowBank, Vortex) e no Internacional (Paycore) fica numa carteira separada, acompanhada pelo painel, e não é sacado pela API.

O saque por Pix independe do recebimento por Pix: a conta saca mesmo quando a criação de cobrança Pix responde `pix_not_enabled`.

## Antes do primeiro saque

Três exigências, todas obrigatórias:

1. **Permissão `withdrawals:write` na chave.** Ela não vem ligada; habilite em [Chaves de API](https://app.thronecorporate.com/painel/api-keys).
2. **IPs de saque.** Cadastre na chave o IP (ou faixa CIDR) do servidor que vai pedir o saque. Sem nenhum IP cadastrado, todo pedido volta `403 withdrawal_ip_not_authorized`. Uma chave de produção com saque também exige a lista geral de IPs da chave, que vale para todas as chamadas dela; por isso, use **uma chave só para saques**. Veja [as duas listas de IP](/docs/comecar#as-duas-listas-de-ip-da-chave).
3. **Código do autenticador.** Cada pedido leva no corpo o `totp_code`: os 6 dígitos atuais do aplicativo autenticador do dono da conta (o mesmo 2FA do login). Sem 2FA ativado nesse usuário, nenhum saque é aceito.

### Saque automático

Para o seu servidor pedir saque sem uma pessoa digitando o código, ele precisa gerar o `totp_code` sozinho, a partir da chave do autenticador do dono da conta (a chave mostrada, em texto ou QR Code, quando o 2FA é ativado). É o TOTP padrão: 6 dígitos, renovados a cada 30 segundos; o Throne aceita também o código anterior e o seguinte. Qualquer biblioteca de TOTP gera o código (por exemplo, `otplib` no Node.js, `pyotp` no Python, `spomky-labs/otphp` no PHP).

> [!WARNING]
> Essa chave dá acesso ao 2FA do login do dono da conta. Guarde-a como a secret key: só no servidor, fora do código e do repositório.

## Pedir saque

`POST /withdrawals`:

```bash cURL
curl -X POST https://app.thronecorporate.com/api/v1/withdrawals \
  -H "Authorization: Bearer $THRONE_PK:$THRONE_SK" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_cents": 50000,
    "totp_code": "123456",
    "destination": { "key": "12345678909", "key_type": "cpf" }
  }'
```
```js Node.js
import { randomUUID } from 'node:crypto';

const res = await fetch('https://app.thronecorporate.com/api/v1/withdrawals', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.THRONE_PK}:${process.env.THRONE_SK}`,
    'Content-Type': 'application/json',
    Accept: 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    amount_cents: 50000,          // R$ 500,00
    totp_code: codigoDoAutenticador,
    destination: { key: '12345678909', key_type: 'cpf' },
  }),
});
const saque = await res.json(); // 201, status "requested"
```
```python Python
import os, uuid, requests

r = requests.post(
    "https://app.thronecorporate.com/api/v1/withdrawals",
    headers={
        "Authorization": f"Bearer {os.environ['THRONE_PK']}:{os.environ['THRONE_SK']}",
        "Accept": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "amount_cents": 50000,
        "totp_code": codigo_do_autenticador,
        "destination": {"key": "12345678909", "key_type": "cpf"},
    },
    timeout=20,
)
saque = r.json()  # r.status_code == 201
```

| Campo | Obrigatório | Regra |
|---|---|---|
| `amount_cents` | Sim | Em centavos. Mínimo padrão R$ 50,00 (`5000`); a sua conta pode ter outro mínimo, nunca abaixo de R$ 10,00 (`1000`). Máximo R$ 500.000,00 por saque |
| `totp_code` | Sim | **Texto** com os 6 dígitos atuais do autenticador do dono da conta, entre aspas (`"012345"`). Enviado como número, volta erro de campo |
| `destination.key` e `destination.key_type` | Para Pix | `key_type`: `cpf` ou `cnpj` (só números), `email`, `phone` (formato internacional, `+5511999998888`) ou `evp` (a chave aleatória, com hífens) |
| `destination.address` e `destination.network` | Para cripto | `network`: `BTC`, `ETH`, `USDT_TRC20` ou `USDT_ERC20` |

Mande Pix **ou** cripto: com `address` presente, o saque é tratado como cripto. O saque cripto só é pago se a sua conta estiver configurada para pagar saques em cripto; se não estiver, termina `failed` e o valor volta.

### Taxa, limites e ritmo

- A taxa sai **de dentro** do valor pedido: o saldo é debitado em `amount_cents`, a taxa aparece em `fee_cents` e o destino recebe `net_amount_cents`. O valor pedido precisa ser maior que a taxa.
- Existe um limite diário por conta, que soma os saques do dia (os recusados não contam).
- O mínimo e a taxa da sua conta aparecem no painel, na tela [Saque](https://app.thronecorporate.com/painel/withdraw).
- No máximo **1 pedido de saque por minuto**. A janela conta mesmo quando o pedido é recusado por saldo ou por limite.

## Acompanhar o saque

A resposta `201` traz o saque com `status: "requested"`:

```json
{
  "id": "0b8f3a52-6c1d-4e2f-9a7b-3c4d5e6f7a8b",
  "type": "pix",
  "status": "requested",
  "amount_cents": 50000,
  "fee_cents": 500,
  "net_amount_cents": 49500,
  "requested_at": "2026-10-03T14:40:00Z",
  "completed_at": null,
  "failure_reason": null
}
```

| Status | O que significa |
|---|---|
| `requested` | Pedido aceito, na fila de pagamento |
| `processing` | Enviado ao banco. Pode voltar para `requested` enquanto espera a vez na fila |
| `completed` | Pago |
| `failed` | Não foi pago. O valor inteiro, taxa incluída, volta para o saldo |

Consulte com `GET /withdrawals/{id}` (permissão `withdrawals:read`). O campo `failure_reason` vem em linguagem pronta para mostrar ao seu cliente. A API não tem listagem de saques: guarde o `id`.

Os webhooks `withdrawal.completed` e `withdrawal.failed` avisam mais cedo, mas **nem todo saque gera webhook**. Consulte o `GET` até chegar em `completed` ou `failed`. Detalhes em [Webhooks](/docs/webhooks).

## Repetir um pedido de saque

Um saque repetido por engano é dinheiro saindo duas vezes. Siga esta ordem:

1. **A chamada falhou sem resposta (falha de conexão, timeout ou resposta sem o JSON da API).** Repita com a **mesma** `Idempotency-Key` e **exatamente o mesmo corpo, inclusive o mesmo `totp_code`**, mesmo que ele já tenha vencido. Se o primeiro pedido chegou, a API devolve o saque original sem conferir o código de novo e sem contar no limite de 1 por minuto.
2. **Veio `409 idempotency_key_processing`.** O primeiro pedido ainda está sendo registrado. Espere alguns segundos e repita igual ao passo 1. Se continuar por mais de 2 minutos, siga o passo 4.
3. **Veio um erro `4xx`, ou um `503` com o JSON da API.** O saque não foi criado. Corrija o motivo e peça de novo com uma `Idempotency-Key` nova e um código novo.
4. **Veio `500` (erro inesperado).** O saque pode ter sido criado. Não peça de novo antes de conferir o saldo (`GET /balance`) e a lista de saques no painel. Repetir com a mesma chave devolve `409 idempotency_key_processing` por até 24 horas, sem criar outro saque.

Nunca gere uma `Idempotency-Key` nova para um saque cujo resultado você não conhece. Na dúvida, confira o saldo (`GET /balance`) e a lista de saques no painel.

## Erros

| Status | `error` | O que fazer |
|---|---|---|
| `403` | `scope_required` | A chave precisa da permissão `withdrawals:write` |
| `403` | `withdrawal_ip_not_authorized` | Cadastre o IP do servidor nos IPs de saque da chave |
| `401` | `invalid_two_factor` | Código do autenticador errado ou vencido, ou o dono da conta não tem 2FA ativado. Gere um código novo; se continuar, confira o 2FA no painel |
| `422` | `insufficient_balance` | Saldo menor que `amount_cents` |
| `422` | `invalid_request` | Valor abaixo do mínimo da conta, acima do máximo ou menor que a taxa. Leia o `message` (abaixo de R$ 10,00 o erro vem como erro de campo) |
| `429` | `daily_limit_exceeded` | Passou do limite diário da conta |
| `429` | `rate_limited` | Mais de 1 pedido por minuto. Espere 60 segundos |
| `403` | `seller_not_approved` | A conta ainda não está liberada para operar |

Depois de um erro desta tabela, use uma `Idempotency-Key` nova. Depois de um `500`, ou sem resposta, siga [repetir um pedido de saque](#repetir-um-pedido-de-saque).
