Pular para o conteúdo
MOONIPdocs
IntegraçãoManual
PTEN
Ir para o painel

Saques

O termo de produto é Saque (payout). O endpoint atual é /v1/withdrawals — o nome de fio em uso hoje, ainda não renomeado para /v1/payouts.

Tira saldo para fora da sua carteira Moonip, rumo a um destino real — uma chave Pix ou uma conta bancária (TED). É o inverso do pagamento Pix: pagamento credita sua carteira, saque debita.

A disponibilidade dos trilhos é por conta. TED depende de o módulo correspondente estar habilitado para você. Confirme com seu contato na Moonip antes de construir em cima de um deles.

#O fluxo, de relance

text
1. POST /v1/withdrawals/validate-pix-key   (opcional — checagem prévia)
2. POST /v1/withdrawals                    (cria a solicitação e reserva o valor na hora)
3. A Moonip despacha — automaticamente ou após aprovação
4. O webhook payout.paid (ou payout.failed) chega no seu endpoint

Saque usa a mesma chave de API das cobranças — Authorization: Bearer <chave>, chamada do seu servidor, nunca do navegador ou do app do cliente final. (O cabeçalho apikey ainda é aceito.) A carteira é debitada na criação; se o saque também precisa de aprovação depende da configuração da sua conta.

Mande sempre o cabeçalho Idempotency-Key na criação — ele é o canal principal de idempotência (veja Criar o saque).

#1. Validar uma chave Pix (recomendado)

Antes de criar o saque, confira se a chave de destino está bem formada.

POST /v1/withdrawals/validate-pix-key

json
{
  "key": "12345678909",
  "type": "CPF"
}

Resposta 201 Created

json
{
  "isValid": true,
  "type": "CPF",
  "normalized": "12345678909"
}

Valores aceitos em type

TipoFormato
CPF11 dígitos, dígito verificador mod-11.
CNPJ14 dígitos, dígito verificador mod-11.
EMAILRFC 5322, até 77 caracteres.
PHONEE.164 com código do país +55 (+5511999999999).
RANDOMChave aleatória — UUID v4.
(omitido)Detectado automaticamente pelo valor da chave.

Declarar qualquer outro tipo (por exemplo EVP) é ignorado: a chave é detectada automaticamente e a resposta informa o tipo detectado. O endpoint responde 201 mesmo quando a chave é inválida — a falha mora no corpo, nunca no status HTTP:

Falha 201 Created

json
{
  "isValid": false,
  "type": "CPF",
  "normalized": "12345678900",
  "error": "CPF checksum invalid"
}

#2. Cotação, taxa e saldo sacável

Três ajudas de leitura, com a mesma chave de API da criação. currency é opcional em todas (padrão BRL).

#Cotação — quanto enviar para o recebedor receber um valor exato

POST /v1/withdrawals/quote — corpo { value, currency? }, value inteiro maior que zero. Precificado como Pix.

Resposta 201 Created

json
{
  "value": 50000,
  "fee": 100,
  "total": 50100
}

total é o bruto que você paga para o recebedor receber exatamente value — a taxa é gross-up, o destino nunca a absorve.

bash
curl -X POST https://api.moonip.io/v1/withdrawals/quote \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "value": 50000 }'
ts
import { MoonipClient } from '@moonip/node';

const client = new MoonipClient({ apiKey: process.env.MOONIP_API_KEY! });

const quote = await client.payouts.quote({ value: 50000 });

Python client.payouts.quote(value=50000), PHP $client->payouts->quote(50000), .NET client.Payouts.QuoteAsync(50000).

#Taxa — o que a criação cobraria

GET /v1/withdrawals/payout-fee

QueryObrigatórioDescrição
amountsimInteiro, 1 a 10.000.000.000 (menor unidade).
currencynãoPadrão BRL.
typenãoPIX (padrão) ou TED.

Resposta 200 OK

json
{
  "amount": 50000,
  "currency": "BRL",
  "fee": 100,
  "total": 50100
}

fee é o que o POST /v1/withdrawals cobraria por esse valor (já incluindo o complemento de pernas acima do teto do Pix); total = amount + fee.

bash
curl "https://api.moonip.io/v1/withdrawals/payout-fee?amount=50000&currency=BRL&type=PIX" \
  -H "Authorization: Bearer $MOONIP_API_KEY"
ts
const fee = await client.payouts.payoutFee({ amount: 50000 });

Python client.payouts.payout_fee(amount=50000), PHP $client->payouts->payoutFee(50000), .NET client.Payouts.PayoutFeeAsync(50000).

#Saldo sacável e parâmetros de taxa

GET /v1/withdrawals/payout-info — sem efeito colateral, feito para o botão "sacar tudo".

Resposta 200 OK

json
{
  "currency": "BRL",
  "available": 1000000,
  "pendingAdjustment": 0,
  "fee": { "fixed": 100, "variablePct": 0, "minimum": 100 },
  "maxWithdrawable": 999900
}

available é o saldo gastável. maxWithdrawable é o maior valor bruto pedível com a taxa já descontada. fee é o conjunto de parâmetros aplicado hoje (fixed, variablePct, minimum).

bash
curl "https://api.moonip.io/v1/withdrawals/payout-info?currency=BRL" \
  -H "Authorization: Bearer $MOONIP_API_KEY"
ts
const info = await client.payouts.payoutInfo();

Python client.payouts.payout_info(), PHP $client->payouts->payoutInfo(), .NET client.Payouts.PayoutInfoAsync().

#3. Criar o saque

POST /v1/withdrawals

Idempotência. Mande o cabeçalho Idempotency-Key — o canal principal, com espaços aparados e no máximo 255 caracteres. O campo idempotencyKey no corpo é um fallback para quem não usa SDK; quando os dois existem, vale o cabeçalho. A mesma chave devolve o mesmo saque: 201 de novo, sem segundo débito. Se a reserva falhar, a linha vira REJECTED e a chave é liberada — depois de depositar você pode repetir com a mesma chave.

Sem chave, a única proteção é um quase-duplicado: o mesmo type, amount e pixKey em 90 segundos responde 409 sem error.code estável. Mande uma chave de idempotência e você nunca depende disso.

CampoTipoObrigatórioDescrição
typeenumsimPIX ou TED.
amountinteirosimMenor unidade da moeda. No modo padrão gross é o que o recebedor recebe; a taxa é debitada por cima.
currencystringnãoPadrão BRL.
pixKeystringquando PIXChave Pix de destino.
pixKeyTypeenumnãoCPF, CNPJ, EMAIL, PHONE, RANDOM. Omitido ou desconhecido (por exemplo EVP): ignorado, a chave é detectada automaticamente.
bankCodestringquando TEDCódigo do banco (FEBRABAN).
bankAgencystringquando TEDAgência.
bankAccountstringquando TEDNúmero da conta.
walletIdstringnãoCarteira OPERATIONAL explícita e gastável em currency. Omitido → seleção automática. Desconhecida → WALLET_NOT_FOUND; não gastável → WALLET_NOT_SPENDABLE; nada cobre o débito → INSUFFICIENT_BALANCE.
amountTypeenumnãogross (padrão) ou net. net é legado e ainda aceito: o débito é amount e o recebedor recebe amount − taxa (fee >= amount → 400).
idempotencyKeystringnãoFallback — prefira o cabeçalho Idempotency-Key.

Chaves desconhecidas (description, cryptoAddress, cryptoNetwork, cryptoCurrency, qualquer outra) são removidas em silêncio — a API nunca guarda nem devolve esses campos. cryptoAddress, cryptoNetwork e cryptoCurrency ainda podem aparecer como campos de resposta legados, somente leitura; não são entrada de criação.

Exemplo — saque Pix

bash
curl -X POST https://api.moonip.io/v1/withdrawals \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Idempotency-Key: saque-2026-S17" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "PIX",
    "amount": 50000,
    "pixKey": "12345678909",
    "pixKeyType": "CPF"
  }'

Exemplo — saque TED (os dados bancários são três strings planas)

bash
curl -X POST https://api.moonip.io/v1/withdrawals \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Idempotency-Key: saque-2026-S17-ted" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "TED",
    "amount": 50000,
    "bankCode": "001",
    "bankAgency": "1234",
    "bankAccount": "56789012"
  }'
ts
const payout = await client.payouts.create(
  { type: 'PIX', amount: 50000, pixKey: '12345678909', pixKeyType: 'CPF' },
  { idempotencyKey: 'saque-2026-S17' },
);
// TED: { type: 'TED', amount: 50000, bankCode: '001', bankAgency: '1234', bankAccount: '56789012' }

Python client.payouts.create(input, idempotency_key=…), PHP $client->payouts->create($input, $idempotencyKey), .NET client.Payouts.CreateAsync(input, idempotencyKey).

Resposta 201 Created

json
{
  "id": "f6a7b8c9-d0e1-4234-9f01-234567890123",
  "type": "PIX",
  "status": "PENDING",
  "amount": 50000,
  "fee": 100,
  "netAmount": 50000,
  "currency": "BRL",
  "source": "API",
  "walletId": "2c9f8e7d-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
  "pixKey": "12345678909",
  "pixKeyType": "CPF",
  "idempotencyKey": "saque-2026-S17",
  "createdAt": "2026-04-25T16:10:00.000Z",
  "updatedAt": "2026-04-25T16:10:00.000Z"
}

A carteira é debitada na criação. No modo gross (o padrão) o débito é amount + fee (aqui 50.100) e o recebedor recebe exatamente amount — por isso netAmount é igual a amount. O valor fica reservado a partir daqui; a aprovação só decide quando o saque é despachado.

Um 201 já pode voltar APPROVED ou PROCESSING: o auto-payout aprova e despacha na hora quando a política da sua conta permite. "Sempre começa PENDING" não é garantido.

Quando o saque não vai. O contrato completo da recusa — o error.code da recusa imediata por saldo, os códigos de falha (failure_code) e quais deles estornam sozinhos — está em Falhas de saque.

#4. Ciclo de vida

text
PENDING ─▶ APPROVED ─▶ PROCESSING ─▶ CONFIRMING ─▶ COMPLETED            ✓ dinheiro entregue
                                                    └▶ PARTIALLY_COMPLETED   entregue em partes

Desfechos terminais incluem também FAILED, REJECTED, CANCELLED e REVERSED. Dez status no total.

StatusEfeito na carteira
PENDINGDebitado na criação (valor + taxa reservados)
APPROVEDReservado — aprovado, aguardando despacho
PROCESSINGReservado — enviado para liquidação
CONFIRMINGReservado — aguardando a confirmação da instituição de destino
COMPLETEDDebitado (terminal) — entregue
PARTIALLY_COMPLETEDDebitado (terminal) — entregue em partes (veja legs)
FAILEDEstornado automaticamente — o despacho falhou (failure_code). O estorno pode ficar retido até a confirmação (veja Falhas de saque)
REJECTEDEstornado — recusado na revisão, ou pela reserva (system:reserve-failed)
CANCELLEDEstornado (valor + taxa) — cancelado por você antes do despacho
REVERSEDDebitado e depois revertido (reversalReason, reversalAmount, reversalStatus)

REJECTED não é só "negado pelo admin": uma criação cuja reserva falha também vira REJECTED, com o valor devolvido.

#Campos de resposta além do corpo de criação

O GET /v1/withdrawals/{id}, a lista e a resposta do cancelamento acrescentam:

CampoO que é
legs[]{ id, legIndex, amount, status, endToEndId, failureCode, refunded }. Um saque Pix acima do teto por transação é dividido em N pernas na aprovação; legIndex começa em 1.
legSummaryAgregado das legs: { count, paidOut, returned, awaitingReturn, pendingDecision, inFlight, total }.
recipientNameNome casado no destino, quando disponível.
reversalReason, reversalAmount, reversalStatusPresentes quando o saque foi revertido.

#Cancelar um saque pendente

POST /v1/withdrawals/{id}/cancel — corpo opcional { "reason": "..." } (aparado, até 500 caracteres; padrão "Cancelado pelo lojista."). Mesma autenticação da criação.

  • Só enquanto o saque está PENDING (nada foi despachado). Ele vira CANCELLED, valor + taxa voltam para a carteira na mesma transação, e o desfecho dispara os webhooks abaixo. Resposta 200 com o saque (formato do retrieve).
  • Já aprovado, despachado ou encerrado → 409, code = WITHDRAWAL_NOT_CANCELLABLE. Linhas que só o suporte cancela (source MANUAL, acima do piso de aprovação manual, fora de BRL) também respondem 409: cancele pelo suporte.
  • No painel, só o dono ou um admin da conta cancela (membro comum → 403, code = WITHDRAWAL_CANCEL_ROLE_REQUIRED). Pela API, vale a chave da conta. A conta recebe um e-mail "Saque cancelado".
  • Saque de outra conta → 404 — e o 404 nunca confirma que o id existe.
bash
curl -X POST https://api.moonip.io/v1/withdrawals/f6a7b8c9-d0e1-4234-9f01-234567890123/cancel \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Pedido duplicado" }'
ts
await client.payouts.cancel('f6a7b8c9-d0e1-4234-9f01-234567890123', { reason: 'Pedido duplicado' });

Python client.payouts.cancel(id, reason=…), PHP $client->payouts->cancel($id, $reason), .NET client.Payouts.CancelAsync(id, reason).

#Comprovante

GET /v1/withdrawals/{id}/receipt — não é só para COMPLETED: o documento existe para todo status terminal (COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED, CANCELLED, REVERSED). Não terminal → 400.

Resposta 200 OK

json
{
  "contentType": "application/pdf",
  "base64": "JVBERi0xLjQKJcTl8uXrp/Og0MTGCg=="
}

Só o COMPLETED tem o título "Comprovante de Transferência PIX"; os demais documentos registram o desfecho.

bash
curl https://api.moonip.io/v1/withdrawals/f6a7b8c9-d0e1-4234-9f01-234567890123/receipt \
  -H "Authorization: Bearer $MOONIP_API_KEY"
ts
const receipt = await client.payouts.receipt('f6a7b8c9-d0e1-4234-9f01-234567890123');

Python client.payouts.receipt(id), PHP $client->payouts->receipt($id), .NET client.Payouts.ReceiptAsync(id).

#5. Webhooks

Assine payout.created, payout.approved, payout.paid e payout.failed. Todos usam o envelope versionado evt_ e a assinatura com timestamp. A aprovação indica processing, antes da liquidação. Veja Webhooks.

json
{
  "id": "evt_5f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "payout.paid",
  "created_at": "2026-04-25T16:11:42.000Z",
  "data": {
    "object": {
      "id": "po_f6a7b8c9-d0e1-4234-9f01-234567890123",
      "object": "payout",
      "status": "paid"
    }
  }
}

data.object.status usa o vocabulário público: pending, processing, paid, failed. FAILED, REJECTED e CANCELLED chegam todos como payout.failed com status: "failed".

payout.failed contém data.object.failure_code e data.object.failure_message quando disponíveis. Use o código estável para decidir como agir; veja Falhas de saque.

#6. Listar seus saques

GET /v1/withdrawals — mesma chave de API do endpoint de criação.

QueryPadrãoDescrição
page1Número da página.
limit20Teto rígido em 100 — valor maior é reduzido em silêncio, sem erro.
status—CSV de qualquer um dos vocabulários (abaixo). Token desconhecido → 400.
source—API, PANEL, MANUAL ou INDEFINIDO (linhas sem source). Qualquer outro valor é ignorado em silêncio e a lista não é filtrada.
orderdescasc para o mais antigo primeiro; qualquer outra coisa é desc.

Os dois vocabulários valem em status, e os valores de fio na resposta são os internos em maiúsculas:

Filtro públicoValores de fio internos
pendingPENDING
processingAPPROVED, PROCESSING, CONFIRMING
paidCOMPLETED, PARTIALLY_COMPLETED
failedFAILED, REJECTED, CANCELLED
(nenhum)REVERSED
bash
curl "https://api.moonip.io/v1/withdrawals?page=1&limit=20&status=paid&source=API&order=desc" \
  -H "Authorization: Bearer $MOONIP_API_KEY"
ts
const page = await client.payouts.list({ limit: 20, status: 'paid', page: 1, source: 'API', order: 'desc' });

Python client.payouts.list(limit=…, status=…, page=…, source=…, order=…), PHP $client->payouts->list([...]), .NET client.Payouts.ListAsync(...).

Resposta 200 OK

json
{
  "data": [
    {
      "id": "f6a7b8c9-d0e1-4234-9f01-234567890123",
      "type": "PIX",
      "status": "COMPLETED",
      "amount": 50000,
      "fee": 100,
      "netAmount": 50000,
      "currency": "BRL",
      "source": "API",
      "completedAt": "2026-04-25T16:11:42.000Z",
      "recipientName": "Maria Silva",
      "legs": [
        {
          "id": "c1d2e3f4-0000-0000-0000-000000000001",
          "legIndex": 1,
          "amount": 50000,
          "status": "COMPLETED",
          "endToEndId": "E12345678202604251611abcdef123456"
        }
      ],
      "legSummary": { "count": 1, "paidOut": 50000, "returned": 0, "awaitingReturn": 0, "pendingDecision": 0, "inFlight": 0, "total": 50000 },
      "...": "..."
    }
  ],
  "total": 17,
  "page": 1,
  "limit": 20
}

#7. Limites e regras

  • Teto por transação no Pix — R$ 15.000,00 por padrão, por conta (seu contrato pode ampliar). Acima do teto o saque é dividido em N legs na aprovação, cada perna até o teto.
  • Limites de saque por conta — minWithdrawalAmount / maxWithdrawalAmount por solicitação, dailyPayoutMaxAmount / dailyPayoutMaxCount por dia (null = sem limite). Fora dos limites: AMOUNT_BELOW_MINIMUM, AMOUNT_ABOVE_MAXIMUM ou DAILY_PAYOUT_LIMIT_EXCEEDED.
  • A carteira precisa cobrir valor + taxa — débito parcial nunca acontece; a solicitação é recusada de uma vez.
  • Aprovação — saques a partir de manualApprovalThresholdCents (padrão R$ 15.001,00 — um centavo acima do teto do Pix, então qualquer Pix de perna única até o teto aprova automaticamente), todo saque fora de BRL e linhas enviadas para revisão manual precisam de aprovação. O auto-payout é política de conta (autoPayoutEnabled, autoWithdrawPct, AUTO_PAYOUT_TYPES, AUTO_PAYOUT_MAX_AMOUNT_CENTS, PILOT_MANUAL_WITHDRAWALS) — peça ao suporte para revisar a sua.
  • TED — restrito a bancos brasileiros (lista de códigos FEBRABAN). Liquidação no mesmo dia útil se aprovado até 16:30 (horário de Brasília).

#Códigos de erro

Ramifique em error.code. A message é para log; o código é o contrato. Exceção: três tokens (WALLET_NOT_FOUND, WALLET_NOT_SPENDABLE, TENANT_KYC_REQUIRED) são texto puro de mensagem — as linhas deles mostram o error.code de fallback, e você casa o token em error.message. Mapa completo: Erros.

HTTPerror.codeQuando
400INSUFFICIENT_BALANCENenhuma carteira cobre o débito (details: available, requested, currency). 400 neste caminho — não é 422.
400invalid_requestO walletId explícito é desconhecido, inativo, de outra moeda ou não gastável. Token em error.message: WALLET_NOT_FOUND / WALLET_NOT_SPENDABLE.
400AMOUNT_BELOW_MINIMUM / AMOUNT_ABOVE_MAXIMUMFora dos limites de saque da conta.
422DAILY_PAYOUT_LIMIT_EXCEEDEDO teto diário de valor ou de quantidade da conta foi atingido.
403account_suspendedConta suspensa ou bloqueada.
403forbiddenO KYC da conta não está ativo (quando o gate de KYC está em modo de exigência). Token em error.message: TENANT_KYC_REQUIRED.
403WITHDRAWAL_CANCEL_ROLE_REQUIREDCancelamento no painel por membro que não é dono nem admin.
404not_foundId desconhecido, ou saque de outra conta — o 404 nunca confirma que o id existe.
409WITHDRAWAL_NOT_CANCELLABLECancelamento de saque que não está PENDING, ou linha que só o suporte cancela.
409(sem código estável)Quase-duplicado de criação sem chave em 90 s. Mande uma Idempotency-Key e repita.

#Dúvidas

P: Criei um saque e ele está preso em PENDING. R: Ele está aguardando aprovação — o despacho imediato só cobre o que a política da sua conta permite (manualApprovalThresholdCents, padrão R$ 15.001,00). Aprove pelo painel ou fale com o suporte sobre auto-payout.

P: Meu saque deu FAILED — a taxa foi cobrada? R: Não. O FAILED dispara um estorno automático e atômico de valor + taxa na carteira. O estorno cai assim que o lançamento na carteira acontece; quando a failure_message é "Estamos confirmando com o banco; você será avisado.", o estorno está retido (legs[].refunded é false) — o dinheiro ainda não voltou, aguarde o crédito e o aviso.

P: Dá para cancelar um saque? R: Só enquanto estiver PENDING, com POST /v1/withdrawals/{id}/cancel ou o botão Cancelar saque no painel. Depois de aprovado, o dinheiro já está em trânsito.

P: Minha chave CNPJ foi recusada como inválida, mas meu banco aceita. R: O dígito verificador mod-11 é exigido e não existe parâmetro de bypass. Se a sua chave reprova na checagem, fale com o suporte.

P: Meu banco não aparece na lista de TED. R: Usamos a lista de códigos FEBRABAN do Bacen. Abra um chamado se faltar algum — normalmente resolvemos em 24h.