O que uma subconta é (e o que não é)
É
Um ledger com saldo próprio dentro da sua conta.Criada por API, na hora, sem documento nem aprovação.Extrato permanente de tudo que entrou e saiu.
Não é
Uma conta PurinCash: não tem login, painel nem chave de API própria.Um destino de dinheiro real: o valor continua na carteira da sua conta.Um cadastro com KYC: a relação com o cliente final é sua.
O dinheiro real fica sempre na sua carteira, e o saldo da subconta diz que fatia dele
pertence a cada cliente. É por isso que criar subconta é instantâneo. E é também por
isso que um bug seu de atribuição nunca move dinheiro de verdade: a camada é contábil.
Criando uma subconta
Resposta 201
string
required
Nome do cliente. Até 100 caracteres, contados como o JavaScript conta (unidades
UTF-16): acentos e cedilha valem 1, mas emoji e outros caracteres fora do plano
básico valem 2. Na prática só aparece com nome cheio de emoji.
string
O ID do cliente no seu sistema. Vale usar sempre: ele é único por ambiente, e criar
de novo com o mesmo
externalId devolve 409 com o subaccountId da existente. Ou
seja: a criação vira idempotente, e o retry do seu código nunca duplica cliente.string
Contato do cliente. Informativo, não autentica nada.
string
String JSON de até 2 KB, devolvida sem alteração. Omitir é o mesmo que mandar
"{}": o campo sempre volta preenchido, nunca ausente nem null.sacc_... junto do cadastro
do seu cliente: é ele que você vai usar em todas as outras chamadas.
Cobrando para uma subconta
AdicionesubaccountId na criação da cobrança. Funciona em POST /v1/charges e em
POST /v1/payments, com PIX:
charge.paid / payment.paid vem com o campo subaccountId quando a
cobrança tem subconta, então o seu handler sabe de quem é a venda sem consultar nada:
O valor creditado no ledger é o líquido, o mesmo que entrou na sua carteira, já
sem a taxa do gateway. Assim a soma dos saldos das subcontas nunca promete mais
dinheiro do que existe.
Onde subaccountId não entra
Reconciliando depois
Cada venda vive na listagem do endpoint que a criou, e as duas aceitam?subaccountId= e devolvem o subaccountId de cada item (null quando não há
atribuição):
?subaccountId= malformado devolve 400 e um sacc_ que não existe na sua conta
devolve 404, nunca uma lista vazia que pareceria “cliente sem vendas”.
Consultando saldo e extrato
amountCents vêm com sinal: crédito positivo, débito negativo. Somar a coluna
reproduz o saldo. Cada lançamento tem um id próprio (estável, serve pra dedupe) e
o refId amarra à origem: psc_/psa_ é uma venda, tr_/idem_ é transferência,
saq_ é saque feito pelo painel e adj_ é correção contábil do suporte. O extrato
aceita ?from= e ?to= (ISO 8601) pra fechar o mês de um cliente sem baixar tudo,
e ?type= com qualquer um dos tipos: sale, transfer_in, transfer_out,
payout, payout_reversal, adjust.
GET /v1/subaccounts lista todas com saldo, e aceita ?externalId= pra achar um
cliente pelo seu ID sem guardar o sacc_ do lado de lá.
O GET de uma subconta traz também medBlockedCents e availableCents: MED
(disputa) atribuído a uma venda daquela subconta bloqueia o valor nela: não
debita, espelhando a carteira mãe. O availableCents é o saldo menos esse bloqueio,
e é ele que limita repasse enquanto a disputa vive.
Transferindo saldo
POST /v1/subaccounts/{id}/transfer move valor entre a subconta e a sua conta, nos
dois sentidos. É uma operação contábil, nenhum PIX é disparado:
to_master exige saldo suficiente na subconta, e a checagem é atômica: duas
transferências concorrentes nunca deixam o saldo negativo. A description aparece no
extrato. Capriche nela: é o que o seu financeiro vai ler depois.
to_subaccount exige lastro: o valor precisa caber no que a sua carteira tem de
sacável e ainda não pertence a nenhuma subconta (o unattributedCents do
GET /v1/wallet). Aporte atribui dinheiro que existe: não cria.
Sem lastro, 400 Insufficient unattributed master balance com o disponível no corpo.
Se duas transferências concorrentes disputarem o mesmo lastro, a que estourar o
limite volta atrás com 409: repita a chamada.
Repetindo sem duplicar
MandeidempotencyKey no corpo (não é header) e o retry de rede deixa de ser um
risco: a repetição devolve 200 com a transferência original e idempotentReplay: true, em vez de mover o valor de novo.
[A-Za-z0-9_-], até 64 caracteres, e é escopada à subconta. Ela vira
o refId do lançamento com o prefixo idem_, e é por isso que o extrato mostra
idem_fechamento-2026-07-cliente-42 no lugar de um tr_ aleatório. Sem a chave,
cada requisição é uma transferência nova.
As regras do amountCents
amountCents é número JSON inteiro, de 1 a 100.000.000.000 (1 bilhão de reais).
Fora disso é 400, sem conserto silencioso:
O mesmo vale para os campos de texto:
name acima de 100, externalId acima de 200,
description acima de 200 e metadata acima de 2048 caracteres são 400: nada é
truncado em silêncio. metadata precisa ser string (serialize com
JSON.stringify antes); active precisa ser boolean de verdade ("false" entre
aspas é 400). externalId é imutável (o PUT recusa) e case-sensitive:
cli_1 e CLI_1 são clientes diferentes.
Pagando o cliente de verdade
Duas formas. A direta é umPOST só; a de dois passos continua valendo e é a que
funciona também em sandbox.
Saque direto da subconta
POST /v1/subaccounts/{id}/payout debita o ledger do cliente e manda o PIX na mesma
operação.
Se a subconta tiver markup de saque, o
amountCents continua
sendo o que é debitado dela por inteiro, e o saque nasce já sem a sua parte.
Resposta 200 (ou 202, quando o banco ainda não confirmou), no mesmo formato do saque
turbo. Chamar de novo com a mesma idempotencyKey devolve o saque original com
idempotentReplay: true, sem pagar nada. Se aquele saque tiver terminado em estorno, a
resposta é 409 com reversed: true: nada saiu, e a instrução é repetir com uma chave
nova, para você não marcar como pago um repasse que não aconteceu.
Live-only. Chave de sandbox recebe 400, porque subconta de teste não tem lastro.
Recusa também deixa rastro: a tentativa fica registrada com status falhou e o motivo,
e aparece em GET /v1/payouts junto com os saques normais. O dinheiro
volta pro ledger da subconta no mesmo instante, com um lançamento de estorno que cita o
motivo. Isso existe pra que um valor que sumiu e voltou no extrato sempre tenha uma
contrapartida procurável, em vez de virar um par de linhas sem explicação.
Ou em dois passos
1
Traga o saldo pra conta principal
POST /v1/subaccounts/{id}/transfer com direction: "to_master" e o valor do
repasse. Isso debita o ledger do cliente e documenta o repasse no extrato dele.2
Saque com a chave PIX do cliente
POST /v1/payouts com method: "pix" e o walletAddress sendo a
chave PIX do cliente. Repare que aqui o valor é amount, em reais, e não em
centavos.O que os dois caminhos têm em comum, e é o que importa: o dinheiro sai pelo mesmo
fluxo de saque de sempre (chave verificada, limites, MED e aprovação). No caminho
direto, o débito no ledger acontece antes de qualquer dinheiro andar e é condicionado
ao saldo da subconta menos o que estiver retido por MED dela. Se o banco negar depois,
o valor volta pro ledger da subconta de origem, e não pro caixa geral.O mesmo saque existe no painel, em Subcontas na aba Sacar.
Taxa da plataforma (markup)
Cada subconta pode ter um markup: percentual (feePercent, 0 a 100 com até 2 casas)
e/ou fixo em centavos (feeFixedCents). A cada venda confirmada, o markup sai do
líquido ANTES do crédito: a subconta recebe liquido - markup, e a sua fatia fica na
conta principal sem nenhum movimento extra: você não precisa mais varrer as vendas e
cobrar comissão com to_master uma a uma.
sale do extrato grava o
feeCents cobrado, então a reconciliação fecha: liquido = amountCents + feeCents.
Regras que valem a pena saber:
- O percentual arredonda pra baixo (na dúvida, o centavo é do cliente), e o markup nunca passa do líquido: taxa fixa maior que a venda credita zero, não gera dívida.
- Mudar a taxa vale só pra vendas FUTURAS: cada lançamento carrega o
feeCentsda época, nada é reescrito. - Transferências não têm markup. Saques têm o seu próprio, logo abaixo.
Markup no saque
O par acima só vale na venda. Pra cobrar também quando o cliente saca, existem dois campos separados:payoutFeePercent (0 a 100, até 2 casas) e
payoutFeeFixedCents.
São campos próprios de propósito. Reaproveitar os de venda faria toda subconta que
já cobra pra receber passar a cobrar o mesmo pra sacar, sem ninguém ter pedido: quem
cobra 5% de comissão não necessariamente quer cobrar 5% de repasse. O padrão é zero
nos dois, e zero é exatamente o comportamento de quem nunca configurou nada.
amountCents: 15000 nessa subconta:
O dinheiro não se move em lugar nenhum. A subconta é debitada o cheio e o saque
nasce pelo líquido, então o seu markup simplesmente fica na carteira da conta
principal: sem crédito extra, sem lançamento paralelo, sem uma segunda escrita que
possa falhar no meio de um saque.
payout grava o
feeCents do markup ao lado do débito cheio. Vale reparar que a resposta do saque
já vem líquida do seu markup: o amount dela é o valor que virou saque
(R$ 146,50 no exemplo) e o fee é a taxa da plataforma, não a sua.
- Markup que consome o valor inteiro é
400, com a taxa citada na mensagem. Saque que só cobra taxa é cobrança disfarçada, não saque. - Se o banco recusar depois, o estorno devolve à subconta o valor cheio, markup incluído: você não fica com a taxa de um saque que não aconteceu.
- Mudar a taxa vale só pra saques FUTUROS, igual à de venda.
- Vale nos dois caminhos: pelo endpoint e pelo painel, em Subcontas na aba Sacar. É o mesmo motor.
No painel as duas taxas ficam no mesmo lugar: ao criar a subconta, e depois em
Suas taxas neste cliente, dentro do cliente. Só o dono da conta edita; membro
de equipe vê e não mexe.
Webhooks de movimentação
Todo movimento do ledger que não nasce de uma cobrança avisa a URLs da conta:subaccount.transfer (API e painel, com
origin dizendo quem foi), subaccount.payout e subaccount.payout_reversal.
A venda continua avisando pelo charge.paid/payment.paid com subaccountId.
Formato completo em Webhooks → Subcontas.
Desativando
PUT /v1/subaccounts/{id} com active: false. A subconta some das novas cobranças e
não aceita mais entradas novas, mas o saldo continua consultável e o to_master
continua funcionando. Desativar não prende dinheiro. Excluir não existe: o extrato é
histórico financeiro, e histórico financeiro não se apaga.
Cobrança criada antes da desativação continua creditando a subconta quando for
paga. A desativação fecha a porta pra cobranças novas; o que já estava emitido com o
subaccountId daquele cliente pertence a ele: desviar esse crédito pro pool geral é
que seria erro contábil.Em sandbox
Tudo funciona com chaveps_test_, com o mesmo isolamento do resto da API: subcontas
de teste são separadas das reais. O simulate-paid credita o ledger aplicando o
mesmo markup configurado na subconta (feePercent/feeFixedCents), igual à
produção: o lançamento sale sai com o feeCents cobrado, então dá pra testar o ciclo
inteiro, taxa incluída, sem mover um centavo: criar subconta, cobrar, simular
pagamento, ver o saldo e o markup, transferir.
Referência completa
Os 6 endpoints de subcontas com playground, na seção Subcontas da Referência.

