split.mjs
/**
* Split de pagamento em Node, sem dependência nenhuma (Node 20+).
*
* P2D_KEY='p2d_test_...' node split.mjs
*
* Cobre: criar a cobrança com split, conferir a taxa (plano + 1pp), empurrar
* no sandbox até `settled` e ler o que caiu em cada carteira — o mesmo que
* chega no evento `charge.split.completed`.
*/
const CHAVE = process.env.P2D_KEY;
const BASE = process.env.P2D_URL ?? 'https://apiv2.pix2depix.com';
if (!CHAVE) throw new Error('defina P2D_KEY com sua chave de API');
// Troque pelas suas carteiras Liquid. Estas passam na validação, mas não são
// de ninguém — use só no sandbox.
const CARTEIRA_A =
'lq1qqf8er278e6nyvuwtgf39e6ewvdcnjupn9a86rzpx655y5lhkt0walu3djf9cklkxd3ryld97hu8h3xepw7sh2rlu7q45dcew5';
const CARTEIRA_B = 'ex1qw508d6qejxtdg4y5r3zarvary0c5xw7kxw5fx4';
async function chamar(caminho, opcoes = {}) {
const res = await fetch(`${BASE}${caminho}`, {
...opcoes,
headers: {
authorization: `Bearer ${CHAVE}`,
'content-type': 'application/json',
...opcoes.headers,
},
body: opcoes.corpo ? JSON.stringify(opcoes.corpo) : undefined,
});
const dados = await res.json();
if (!res.ok) {
const { code, message, details, requestId } = dados.error;
// Em `invalid_request`, `details` diz qual destino ou percentual falhou.
throw new Error(
`[${res.status} ${code}] ${message} ${JSON.stringify(details ?? [])} (requestId: ${requestId})`,
);
}
return dados;
}
const reais = (centavos) => `R$ ${(centavos / 100).toFixed(2)}`;
// ── 1. Criar a cobrança com split ──────────────────────────────────────────
const cobranca = await chamar('/v1/split-charges', {
method: 'POST',
headers: { 'idempotency-key': crypto.randomUUID() },
corpo: {
amountInCents: 100000,
payerTaxNumber: '529.982.247-25',
payerName: 'Maria Silva',
externalId: `pedido-${Date.now()}`,
split: {
// Até duas casas, somando exatamente 100.
destinations: [
{ address: CARTEIRA_A, percent: 60 },
{ address: CARTEIRA_B, percent: 40 },
],
},
},
});
const { amount, split } = cobranca;
console.log(`cobrança ${cobranca.id} → ${cobranca.status}`);
console.log('copia-e-cola:', cobranca.pix.qrCode);
console.log(
`taxa: plano ${split.planFeeBps / 100}% + 1% = ${amount.feeBps / 100}%`,
`+ fixa ${reais(amount.feeFixedInCents)} → líquido ${reais(amount.netInCents)}`,
);
// ── 2. Sandbox: pagar e liquidar sem Pix ───────────────────────────────────
// Em produção quem faz isso é o seu cliente pagando, e a retenção de 24h.
if (cobranca.environment === 'test') {
for (const status of ['paid_pending_settlement', 'settled']) {
await chamar(`/v1/test/charges/${cobranca.id}/advance`, {
method: 'POST',
corpo: { status },
});
}
}
// ── 3. Ler o resultado ─────────────────────────────────────────────────────
// Em produção, reaja ao webhook `charge.split.completed` em vez de consultar:
// o corpo dele traz esta mesma cobrança em `data.charge`.
const atual = await chamar(`/v1/charges/${cobranca.id}`);
// Quem libera o produto é o status da cobrança, não o do split.
console.log(
atual.status === 'settled'
? 'cobrança liquidada — pode liberar o produto.'
: `cobrança em ${atual.status} — não libere.`,
);
console.log(`split: ${atual.split.status}`, atual.split.txId ?? '');
for (const destino of atual.split.destinations) {
console.log(
` ${destino.percent}% → ${destino.address.slice(0, 16)}…`,
destino.amountInCents === undefined ? '(ainda não enviado)' : reais(destino.amountInCents),
);
}