Pix2DePix

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.`,
);