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
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 endpointSaque 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
{
"key": "12345678909",
"type": "CPF"
}Resposta 201 Created
{
"isValid": true,
"type": "CPF",
"normalized": "12345678909"
}Valores aceitos em type
| Tipo | Formato |
|---|---|
CPF | 11 dígitos, dígito verificador mod-11. |
CNPJ | 14 dígitos, dígito verificador mod-11. |
EMAIL | RFC 5322, até 77 caracteres. |
PHONE | E.164 com código do país +55 (+5511999999999). |
RANDOM | Chave 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
{
"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
{
"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.
curl -X POST https://api.moonip.io/v1/withdrawals/quote \
-H "Authorization: Bearer $MOONIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "value": 50000 }'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
| Query | Obrigatório | Descrição |
|---|---|---|
amount | sim | Inteiro, 1 a 10.000.000.000 (menor unidade). |
currency | não | Padrão BRL. |
type | não | PIX (padrão) ou TED. |
Resposta 200 OK
{
"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.
curl "https://api.moonip.io/v1/withdrawals/payout-fee?amount=50000¤cy=BRL&type=PIX" \
-H "Authorization: Bearer $MOONIP_API_KEY"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
{
"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).
curl "https://api.moonip.io/v1/withdrawals/payout-info?currency=BRL" \
-H "Authorization: Bearer $MOONIP_API_KEY"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.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
type | enum | sim | PIX ou TED. |
amount | inteiro | sim | Menor unidade da moeda. No modo padrão gross é o que o recebedor recebe; a taxa é debitada por cima. |
currency | string | não | Padrão BRL. |
pixKey | string | quando PIX | Chave Pix de destino. |
pixKeyType | enum | não | CPF, CNPJ, EMAIL, PHONE, RANDOM. Omitido ou desconhecido (por exemplo EVP): ignorado, a chave é detectada automaticamente. |
bankCode | string | quando TED | Código do banco (FEBRABAN). |
bankAgency | string | quando TED | Agência. |
bankAccount | string | quando TED | Número da conta. |
walletId | string | não | Carteira 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. |
amountType | enum | não | gross (padrão) ou net. net é legado e ainda aceito: o débito é amount e o recebedor recebe amount − taxa (fee >= amount → 400). |
idempotencyKey | string | não | Fallback — 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
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)
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"
}'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
{
"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.codeda 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
PENDING ─▶ APPROVED ─▶ PROCESSING ─▶ CONFIRMING ─▶ COMPLETED ✓ dinheiro entregue
└▶ PARTIALLY_COMPLETED entregue em partesDesfechos terminais incluem também FAILED, REJECTED, CANCELLED e REVERSED. Dez status no total.
| Status | Efeito na carteira |
|---|---|
PENDING | Debitado na criação (valor + taxa reservados) |
APPROVED | Reservado — aprovado, aguardando despacho |
PROCESSING | Reservado — enviado para liquidação |
CONFIRMING | Reservado — aguardando a confirmação da instituição de destino |
COMPLETED | Debitado (terminal) — entregue |
PARTIALLY_COMPLETED | Debitado (terminal) — entregue em partes (veja legs) |
FAILED | Estornado automaticamente — o despacho falhou (failure_code). O estorno pode ficar retido até a confirmação (veja Falhas de saque) |
REJECTED | Estornado — recusado na revisão, ou pela reserva (system:reserve-failed) |
CANCELLED | Estornado (valor + taxa) — cancelado por você antes do despacho |
REVERSED | Debitado 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:
| Campo | O 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. |
legSummary | Agregado das legs: { count, paidOut, returned, awaitingReturn, pendingDecision, inFlight, total }. |
recipientName | Nome casado no destino, quando disponível. |
reversalReason, reversalAmount, reversalStatus | Presentes 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 viraCANCELLED,valor + taxavoltam para a carteira na mesma transação, e o desfecho dispara os webhooks abaixo. Resposta200com o saque (formato do retrieve). - Já aprovado, despachado ou encerrado →
409,code=WITHDRAWAL_NOT_CANCELLABLE. Linhas que só o suporte cancela (sourceMANUAL, acima do piso de aprovação manual, fora de BRL) também respondem409: 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 o404nunca confirma que o id existe.
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" }'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
{
"contentType": "application/pdf",
"base64": "JVBERi0xLjQKJcTl8uXrp/Og0MTGCg=="
}Só o COMPLETED tem o título "Comprovante de Transferência PIX"; os demais documentos registram o desfecho.
curl https://api.moonip.io/v1/withdrawals/f6a7b8c9-d0e1-4234-9f01-234567890123/receipt \
-H "Authorization: Bearer $MOONIP_API_KEY"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.
{
"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.
| Query | Padrão | Descrição |
|---|---|---|
page | 1 | Número da página. |
limit | 20 | Teto 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. |
order | desc | asc 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úblico | Valores de fio internos |
|---|---|
pending | PENDING |
processing | APPROVED, PROCESSING, CONFIRMING |
paid | COMPLETED, PARTIALLY_COMPLETED |
failed | FAILED, REJECTED, CANCELLED |
| (nenhum) | REVERSED |
curl "https://api.moonip.io/v1/withdrawals?page=1&limit=20&status=paid&source=API&order=desc" \
-H "Authorization: Bearer $MOONIP_API_KEY"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
{
"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
legsna aprovação, cada perna até o teto. - Limites de saque por conta —
minWithdrawalAmount/maxWithdrawalAmountpor solicitação,dailyPayoutMaxAmount/dailyPayoutMaxCountpor dia (null = sem limite). Fora dos limites:AMOUNT_BELOW_MINIMUM,AMOUNT_ABOVE_MAXIMUMouDAILY_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.
| HTTP | error.code | Quando |
|---|---|---|
| 400 | INSUFFICIENT_BALANCE | Nenhuma carteira cobre o débito (details: available, requested, currency). 400 neste caminho — não é 422. |
| 400 | invalid_request | O walletId explícito é desconhecido, inativo, de outra moeda ou não gastável. Token em error.message: WALLET_NOT_FOUND / WALLET_NOT_SPENDABLE. |
| 400 | AMOUNT_BELOW_MINIMUM / AMOUNT_ABOVE_MAXIMUM | Fora dos limites de saque da conta. |
| 422 | DAILY_PAYOUT_LIMIT_EXCEEDED | O teto diário de valor ou de quantidade da conta foi atingido. |
| 403 | account_suspended | Conta suspensa ou bloqueada. |
| 403 | forbidden | O 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. |
| 403 | WITHDRAWAL_CANCEL_ROLE_REQUIRED | Cancelamento no painel por membro que não é dono nem admin. |
| 404 | not_found | Id desconhecido, ou saque de outra conta — o 404 nunca confirma que o id existe. |
| 409 | WITHDRAWAL_NOT_CANCELLABLE | Cancelamento 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.