Skip to main content
Se você roda uma plataforma (marketplace, SaaS que cobra pelos clientes, rede de revendedores), uma pergunta aparece rápido: do dinheiro que entrou, quanto é de quem? Subconta é a resposta da API pra isso. Você cria uma subconta por cliente, aponta as cobranças pra ela, e o saldo de cada um passa a ser mantido pelo gateway: creditado quando o pagamento confirma, debitado quando você transfere, consultável a qualquer momento com extrato completo.

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.
Limite de 1000 subcontas por conta e por ambiente. Guarde o sacc_... junto do cadastro do seu cliente: é ele que você vai usar em todas as outras chamadas.

Cobrando para uma subconta

Adicione subaccountId na criação da cobrança. Funciona em POST /v1/charges e em POST /v1/payments, com PIX:
O resto do fluxo é o de sempre: o cliente final paga o PIX, o webhook chega. O passo novo é o seguinte: o líquido da venda é creditado no ledger da subconta no mesmo instante em que entra na sua carteira. O crédito é idempotente: reenvio de webhook não credita duas vezes. O webhook 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

Em /v1/payments e /v1/charges, onde o campo é aceito, subaccountId: "" devolve 400 em vez de criar a venda sem dono. É proposital: o padrão subaccountId: cliente.saccId || "" transformaria um id faltando em dinheiro caindo no pool geral sem aviso, e você só descobriria fechando o mês. Pra venda sem atribuição, omita o campo.

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):
As duas listas são separadas: /v1/payments não devolve cobranças psc_ e vice-versa. Se você usa os dois endpoints, feche o mês do cliente somando as duas, ou use o extrato da subconta, que junta tudo o que foi creditado, independente da origem.
?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

Os 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

Mande idempotencyKey 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 chave aceita [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 é um POST 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.
Este endpoint precisa do escopo subcontas.sacar, e ele nunca é concedido implicitamente. Chave sem lista de escopos, que por compatibilidade pode todo o resto, recebe 403 aqui até alguém marcar a caixinha no painel. É proposital: ligar um poder novo de mandar dinheiro pra fora não pode dar esse poder, de graça, a todas as chaves que você já emitiu. subcontas.gerenciar também não abre este, porque aquele mexe em atribuição e este mexe em dinheiro.
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.
POST /v1/payouts recusa subaccountId com 400 de propósito. Ali não há baixa no ledger: o dinheiro sairia da carteira e o extrato da subconta continuaria dizendo que o valor está lá. Pra sacar atribuído, use o endpoint desta seção.
O saque da conta principal não alcança o que é das subcontas. O sacável (withdrawable do GET /v1/wallet) desconta a soma dos saldos de todas as subcontas: se o dinheiro está atribuído a um cliente, ele sai pela subconta, não por cima dela. Saque acima do sacável leva 400 com o motivo detalhado. Pra liberar um valor, traga de volta com to_master primeiro.

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.
Com essa configuração, uma venda que rendeu R$ 100,00 líquidos credita R$ 94,00 na subconta (5% + R$ 1,00 ficam com você). O lançamento 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 feeCents da é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.
Com essa configuração, um saque de 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.
Onde ler o valor cobrado: no extrato da subconta, o lançamento 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 chave ps_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.