saque.mjs
/**
* Saque (DePix → Pix) em Node, sem dependência nenhuma (Node 20+).
*
* P2D_KEY='p2d_test_...' node saque.mjs
*
* Cobre: criar o saque, ler o endereço e o valor exatos do depósito, provar
* que o mesmo `externalId` não vira dois saques e percorrer o ciclo no
* sandbox até o Pix pago — o mesmo que chega em `withdrawal.paid`.
*/
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 pela sua chave Pix e pelo documento do dono dela. Confira antes de
// enviar DePix em produção: Pix na chave errada não tem estorno.
const CHAVE_PIX = '6a4f5c1e-8b2d-4a7f-9c3e-1d2b3a4c5d6e';
const TIPO_DA_CHAVE = 'aleatoria';
const DOCUMENTO_DO_DONO = '529.982.247-25';
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 campo falhou.
throw new Error(
`[${res.status} ${code}] ${message} ${JSON.stringify(details ?? [])} (requestId: ${requestId})`,
);
}
return { dados, status: res.status };
}
const reais = (centavos) => `R$ ${(centavos / 100).toFixed(2)}`;
// ── 1. Os limites de agora ─────────────────────────────────────────────────
const { dados: conta } = await chamar('/v1/account');
console.log(
`conta ${conta.email} (${conta.environment}) —`,
`saque de ${reais(conta.limits.minWithdrawalInCents)} a ${reais(conta.limits.maxWithdrawalInCents)}`,
);
// ── 1b. Cotar antes de criar ───────────────────────────────────────────────
// O número para mostrar a quem saca. `merchantFeeBps` é a sua taxa (100 = 1%),
// que vai em DePix para o seu endereço padrão; para cobrá-la de verdade,
// mande o mesmo campo na criação.
const { dados: cotacao } = await chamar(
'/v1/withdrawals/quote?depixAmountInCents=100000&merchantFeeBps=100',
);
console.log(
`cotação: envia ${reais(cotacao.depixInCents)} →`,
`taxa nossa ${reais(cotacao.feeInCents)}, sua ${reais(cotacao.merchantFeeInCents)},`,
`Pix de ${reais(cotacao.payoutInCents)}`,
);
// ── 2. Criar o saque ───────────────────────────────────────────────────────
// `amountInCents` é o Pix que você quer receber; a taxa sai por fora. Para
// esvaziar a carteira, mande `depixAmountInCents` com o DePix que você tem.
// Com `merchantFeeBps` a sua taxa entra junto (exige endereço padrão).
const corpo = {
amountInCents: 100000,
pixKeyType: TIPO_DA_CHAVE,
pixKey: CHAVE_PIX,
payeeTaxNumber: DOCUMENTO_DO_DONO,
externalId: `saque-${Date.now()}`,
};
const { dados: saque, status } = await chamar('/v1/withdrawals', {
method: 'POST',
headers: { 'idempotency-key': crypto.randomUUID() },
corpo,
});
console.log(`saque ${saque.id} → ${saque.status} (HTTP ${status})`);
console.log(
`taxa ${reais(saque.amount.feeInCents)} →`,
`envie ${reais(saque.amount.depixInCents)} em DePix para receber ${reais(saque.amount.payoutInCents)}`,
);
console.log('endereço de depósito (só deste saque):', saque.deposit.address);
// ── 3. O mesmo externalId não cria um segundo saque ────────────────────────
const repetido = await chamar('/v1/withdrawals', { method: 'POST', corpo });
console.log(
repetido.status === 200 && repetido.dados.id === saque.id
? 'idempotência ok: veio o mesmo saque, sem duplicar.'
: 'ATENÇÃO: o saque duplicou — confira o externalId.',
);
// ── 4. Sandbox: receber o DePix e pagar o Pix sem mover nada ───────────────
// Em produção este é o passo em que você transfere o DePix para o endereço
// acima, e o resto chega pelos webhooks `withdrawal.processing` e
// `withdrawal.paid`.
if (saque.environment === 'test') {
for (const status of ['processing', 'paid']) {
await chamar(`/v1/test/withdrawals/${saque.id}/advance`, {
method: 'POST',
corpo: { status },
});
}
}
// ── 5. Conferir o fim ──────────────────────────────────────────────────────
const { dados: atual } = await chamar(`/v1/withdrawals/${saque.id}`);
console.log(
atual.status === 'paid'
? `Pix pago — comprovante ${atual.pix.endToEndId}`
: `saque em ${atual.status} — ainda não caiu.`,
);