Ambiente de simulação — nenhuma cobrança real. Os pagamentos, Pix e cartões deste sandbox são sintéticos.
W Wolf Sandbox Docs Criar conta
Navegação

Quickstart de 5 minutos

Da conta nova a um pagamento Pix simulado com webhook assinado. Você precisa de curl ou Node.js 18+ (os exemplos em Node usam só a biblioteca padrão e fetch).

1. Crie sua conta

Abra https://v2.wolfpayteste.online/login, use Criar conta Seller (senha com 12+ caracteres), confirme o e-mail e conclua o onboarding com a razão social e o nome público que o comprador verá no checkout. No Wolf Sandbox público a conta é aprovada automaticamente e o painel abre em seguida.

2. Crie uma API key de teste

No painel, vá em API keys e crie uma chave com os escopos payments:read, payments:write e webhooks:write. O segredo sk_test_... aparece uma única vez: copie agora. Guarde-o só no servidor (nunca em navegador ou app). Detalhes em Autenticação.

Terminal shell
export WOLF_API_URL="https://api.v2.wolfpayteste.online"
export WOLF_API_KEY="sk_test_..."   # a chave copiada no passo 2

3. Prepare o receptor de webhooks

Rode o receptor abaixo e exponha-o numa URL HTTPS pública (por exemplo, um túnel HTTPS de sua preferência ou um servidor seu). Depois, no painel, em Webhooks, cadastre essa URL no modo live e copie o signing secret whsec_live_... — ele também aparece uma única vez.

receber-webhook.mjs Node.js
// Passo 6 — receber o webhook e verificar a assinatura (Node.js puro, sem dependências).
// Uso: WOLF_WEBHOOK_SECRET=whsec_live_... PORT=3000 node receber-webhook.mjs
import crypto from 'node:crypto';
import http from 'node:http';
import { fileURLToPath } from 'node:url';

const TOLERANCE_SECONDS = 300;

/** true se `x-wolf-signature` confere com HMAC-SHA256(segredo, `${timestamp}.${corpo bruto}`) e o timestamp é recente. */
export function verifyWolfSignature(rawBody, timestamp, signature, secret, nowSeconds = Date.now() / 1000) {
  if (!/^\d+$/.test(timestamp ?? '')) return false;
  if (Math.abs(nowSeconds - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(signature ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

/** Servidor HTTP que aceita só entregas assinadas e chama `onEvent` uma vez por `event.id`. */
export function createWebhookServer({ secret, onEvent }) {
  const seen = new Set();
  return http.createServer((request, response) => {
    if (request.method !== 'POST') { response.writeHead(405).end(); return; }
    const chunks = [];
    request.on('data', (chunk) => chunks.push(chunk));
    request.on('end', () => {
      const rawBody = Buffer.concat(chunks); // bytes exatos: não reserialize o JSON antes de verificar
      if (!verifyWolfSignature(rawBody, request.headers['x-wolf-timestamp'], request.headers['x-wolf-signature'], secret)) {
        response.writeHead(400).end();
        return;
      }
      const event = JSON.parse(rawBody.toString('utf8'));
      // Idempotência: a mesma entrega pode chegar mais de uma vez (retries, replays).
      if (!seen.has(event.id)) { seen.add(event.id); onEvent(event); }
      response.writeHead(204).end(); // responda 2xx rápido; processe em segundo plano
    });
  });
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const port = Number(process.env.PORT ?? 3000);
  createWebhookServer({
    secret: process.env.WOLF_WEBHOOK_SECRET,
    onEvent: (event) => console.log(event.type, event.resource_id, event.data?.status ?? ''),
  }).listen(port, () => console.log(`Ouvindo webhooks em http://localhost:${port}`));
}

O receptor recusa (400) qualquer entrega sem assinatura válida ou com timestamp fora da janela de 5 minutos e processa cada event.id uma única vez. O formato completo está em Webhooks → assinatura.

4. Crie um PaymentIntent Pix com o pagador

O valor vai em centavos, como string. O documento do pagador é opcional, validado pelo dígito verificador e nunca volta em claro: a resposta traz só o tipo e os dois últimos dígitos.

criar-payment-intent.sh curl
# Passo 3 — criar um PaymentIntent Pix com pagador (amount_minor em centavos, como string).
# Use uma Idempotency-Key única por operação (por exemplo, derivada do id do seu pedido).
curl -sS -X POST "$WOLF_API_URL/v1/payment-intents" \
  -H "Authorization: Bearer $WOLF_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-A-1001-criar" \
  -d '{
    "amount_minor": "9900",
    "currency": "BRL",
    "payment_method_types": ["pix"],
    "description": "Curso de fotografia",
    "metadata": { "pedido_interno": "A-1001" },
    "payer": {
      "name": "Ana Compradora",
      "email": "ana@example.com",
      "document": { "type": "cpf", "number": "529.982.247-25" }
    }
  }'
criar-payment-intent.mjs Node.js
// Passo 3 — criar um PaymentIntent Pix com pagador.
// Uso: WOLF_API_URL=... WOLF_API_KEY=sk_test_... node criar-payment-intent.mjs
import { randomUUID } from 'node:crypto';
import { fileURLToPath } from 'node:url';

export async function createPixPaymentIntent({ apiUrl = process.env.WOLF_API_URL, apiKey = process.env.WOLF_API_KEY } = {}) {
  const response = await fetch(`${apiUrl}/v1/payment-intents`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      // Uma chave por operação. Repetir a mesma chave com o mesmo corpo devolve o mesmo PaymentIntent.
      'Idempotency-Key': randomUUID(),
    },
    body: JSON.stringify({
      amount_minor: '9900', // R$ 99,00 em centavos, sempre como string
      currency: 'BRL',
      payment_method_types: ['pix'],
      description: 'Curso de fotografia',
      metadata: { pedido_interno: 'A-1001' },
      payer: {
        name: 'Ana Compradora',
        email: 'ana@example.com',
        document: { type: 'cpf', number: '529.982.247-25' }, // opcional; nunca volta em claro
      },
    }),
  });
  const body = await response.json();
  if (!response.ok) throw new Error(`Wolf API ${response.status}: ${body.error?.code} (request_id ${body.error?.request_id})`);
  return body; // status "requires_payment_method"
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const paymentIntent = await createPixPaymentIntent();
  console.log(paymentIntent.id, paymentIntent.status);
}

A resposta é 201 com status: "requires_payment_method". Guarde o id: export PAYMENT_INTENT_ID=pi_....

5. Confirme com Pix

confirmar-pix.sh curl
# Passo 4 — confirmar com Pix. Resposta 202: status "processing" e next_action.pix_qr (QR sintético SIMULADO).
curl -sS -X POST "$WOLF_API_URL/v1/payment-intents/$PAYMENT_INTENT_ID/confirm" \
  -H "Authorization: Bearer $WOLF_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-A-1001-confirmar" \
  -d '{ "payment_method": { "type": "pix" } }'
confirmar-pix.mjs Node.js
// Passo 4 — confirmar o PaymentIntent com Pix e obter o QR Code sintético.
// Uso: WOLF_API_URL=... WOLF_API_KEY=sk_test_... PAYMENT_INTENT_ID=pi_... node confirmar-pix.mjs
import { randomUUID } from 'node:crypto';
import { fileURLToPath } from 'node:url';

export async function confirmPix({ apiUrl = process.env.WOLF_API_URL, apiKey = process.env.WOLF_API_KEY, paymentIntentId = process.env.PAYMENT_INTENT_ID } = {}) {
  const response = await fetch(`${apiUrl}/v1/payment-intents/${encodeURIComponent(paymentIntentId)}/confirm`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': randomUUID(),
    },
    body: JSON.stringify({ payment_method: { type: 'pix' } }),
  });
  const body = await response.json();
  if (!response.ok) throw new Error(`Wolf API ${response.status}: ${body.error?.code} (request_id ${body.error?.request_id})`);
  // 202: status "processing" e next_action.type "pix_qr" com o copia-e-cola SIMULADO e o vencimento.
  return body;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const paymentIntent = await confirmPix();
  console.log(paymentIntent.status, paymentIntent.next_action?.expires_at);
  console.log(paymentIntent.next_action?.qr_code_payload);
}

A resposta é 202 com status: "processing" e next_action.type: "pix_qr": o copia-e-cola é sintético (marcado SIMULADO, não pagável em nenhum banco) e vence em 30 minutos. O receptor recebe payment.processing (se o endpoint escuta todos os eventos ou esse tipo).

6. Simule o pagamento

No sandbox, quem “paga” o Pix é você: a rota do simulador gera o mesmo evento de provedor que um banco geraria. O comprador também pode usar o botão Simular pagamento do checkout hospedado.

simular-pagamento.sh curl
# Passo 5 — simular o pagamento do Pix (só existe no Wolf Sandbox). O PaymentIntent vira "succeeded".
curl -sS -X POST "$WOLF_API_URL/v1/simulator/payment-intents/$PAYMENT_INTENT_ID/pix/pay" \
  -H "Authorization: Bearer $WOLF_API_KEY"

# Conferir o estado a qualquer momento:
curl -sS "$WOLF_API_URL/v1/payment-intents/$PAYMENT_INTENT_ID" \
  -H "Authorization: Bearer $WOLF_API_KEY"
simular-pagamento.mjs Node.js
// Passo 5 — simular o pagamento do Pix (só existe no Wolf Sandbox) e conferir o resultado.
// Uso: WOLF_API_URL=... WOLF_API_KEY=sk_test_... PAYMENT_INTENT_ID=pi_... node simular-pagamento.mjs
import { fileURLToPath } from 'node:url';

async function wolf(path, { apiUrl, apiKey, method = 'GET' }) {
  const response = await fetch(`${apiUrl}${path}`, { method, headers: { Authorization: `Bearer ${apiKey}` } });
  const body = await response.json();
  if (!response.ok) throw new Error(`Wolf API ${response.status}: ${body.error?.code} (request_id ${body.error?.request_id})`);
  return body;
}

export async function simulatePixPayment({ apiUrl = process.env.WOLF_API_URL, apiKey = process.env.WOLF_API_KEY, paymentIntentId = process.env.PAYMENT_INTENT_ID } = {}) {
  const id = encodeURIComponent(paymentIntentId);
  // Gera o evento de provedor pix.paid. Repetir não cobra de novo: o PaymentIntent continua succeeded.
  await wolf(`/v1/simulator/payment-intents/${id}/pix/pay`, { apiUrl, apiKey, method: 'POST' });
  return wolf(`/v1/payment-intents/${id}`, { apiUrl, apiKey }); // status "succeeded"
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const paymentIntent = await simulatePixPayment();
  console.log(paymentIntent.id, paymentIntent.status);
}

O PaymentIntent vira succeeded: a Charge é criada e o ledger é lançado na mesma transação. Repetir a simulação não cobra de novo.

7. Receba o webhook e confira a assinatura

Em até cerca de dois minutos o receptor imprime payment.paid com o id do PaymentIntent. Cada entrega traz x-wolf-event-id, x-wolf-timestamp e x-wolf-signature (v1= + HMAC-SHA256 do timestamp.corpo). Para testar só o receptor, peça um evento webhook.test:

enviar-evento-de-teste.sh curl
# Passo 6 (opcional) — pedir um evento webhook.test para o seu endpoint (resposta 202, entrega em até ~2 min).
curl -sS -X POST "$WOLF_API_URL/v1/webhooks/endpoints/$WEBHOOK_ENDPOINT_ID/test" \
  -H "Authorization: Bearer $WOLF_API_KEY" \
  -H "Idempotency-Key: quickstart-teste-webhook-1"
enviar-evento-de-teste.mjs Node.js
// Passo 6 (opcional) — pedir um evento webhook.test para o seu endpoint.
// Uso: WOLF_API_URL=... WOLF_API_KEY=sk_test_... WEBHOOK_ENDPOINT_ID=whe_... node enviar-evento-de-teste.mjs
import { randomUUID } from 'node:crypto';
import { fileURLToPath } from 'node:url';

export async function sendTestEvent({ apiUrl = process.env.WOLF_API_URL, apiKey = process.env.WOLF_API_KEY, endpointId = process.env.WEBHOOK_ENDPOINT_ID } = {}) {
  const response = await fetch(`${apiUrl}/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}/test`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${apiKey}`, 'Idempotency-Key': randomUUID() },
  });
  const body = await response.json();
  if (!response.ok) throw new Error(`Wolf API ${response.status}: ${body.error?.code} (request_id ${body.error?.request_id})`);
  return body; // 202: entrega pendente; chega ao endpoint em até ~2 minutos
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const delivery = await sendTestEvent();
  console.log(delivery.id, delivery.event_type, delivery.status);
}

Próximos passos