SDK Node.js / TypeScript
@pitra/sdk é o cliente oficial da API Pitra para Node.js e TypeScript. Sem dependências de runtime, com tipagem completa, erros tipados, timeout, retry e idempotência automática.
Instalação
Requer Node.js 18 ou superior.
npm install @pitra/sdkConfiguração
import { PitraClient } from "@pitra/sdk";
const pitra = new PitraClient({
apiKey: process.env.PITRA_API_KEY!, // sk_live_* ou sk_test_*
// baseURL: "https://pitra.com.br/api/public/v1", // opcional
// timeout: 15000, // opcional (ms)
});sk_test_* em desenvolvimento: as cobranças são simuladas e não movimentam dinheiro.Primeira cobrança Pix
const cobranca = await pitra.pix.create({
amount: 19.9,
description: "Plano Pro",
external_id: "pedido-1042",
expires_in: 3600,
customer: { email: "cliente@exemplo.com" },
metadata: { origem: "checkout-proprio" },
});
console.log(cobranca.id); // pix_...
console.log(cobranca.status); // pending
console.log(cobranca.pix.copy_paste); // Pix Copia e Cola
console.log(cobranca.pix.qr_code_base64);Consultar cobrança
// Pelo id da Pitra
const cobranca = await pitra.pix.get("pix_123");
// Pelo identificador do seu sistema
const mesma = await pitra.pix.getByExternalId("pedido-1042");
if (cobranca.status === "paid") {
console.log("Pago em", cobranca.paid_at);
}Listar cobranças
const pagina = await pitra.pix.list({ limit: 20, status: "paid" });
for (const cobranca of pagina.data) {
console.log(cobranca.id, cobranca.amount);
}
if (pagina.has_more) {
const proxima = await pitra.pix.list({ limit: 20, starting_after: pagina.next_cursor! });
console.log(proxima.data.length);
}Métodos disponíveis
| Método | Endpoint | Descrição |
|---|---|---|
pitra.pix.create(data, options?) | POST /payments/pix | Cria uma cobrança Pix com QR Code e Copia e Cola. |
pitra.pix.get(id, options?) | GET /payments/pix/:id | Consulta uma cobrança pelo id da Pitra. |
pitra.pix.getByExternalId(externalId, options?) | GET /payments/pix/external/:externalId | Consulta pelo identificador do seu sistema. |
pitra.pix.list(params?, options?) | GET /payments/pix | Lista cobranças com paginação por cursor. |
verifyWebhook(input) | — | Verifica a assinatura Pitra-Signature de um webhook. |
cancel(). O método será publicado junto com o endpoint correspondente, em uma nova versão do SDK.Receber webhook
import express from "express";
import { verifyWebhook } from "@pitra/sdk";
const app = express();
// O corpo bruto é obrigatório: verifique a assinatura antes de qualquer parser JSON.
app.post("/webhooks/pitra", express.raw({ type: "*/*" }), async (req, res) => {
try {
const evento = await verifyWebhook({
headers: req.headers,
rawBody: req.body.toString("utf8"),
secret: process.env.PITRA_WEBHOOK_SECRET!,
});
if (evento.type === "pix.paid") {
// libere o pedido aqui
}
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});A assinatura usa o header Pitra-Signature: t=<unix>,v1=<hmac_sha256>, com comparação em tempo constante e tolerância padrão de 300 segundos contra replay.
Tratamento de erros
import {
AuthenticationError,
ValidationError,
RateLimitError,
NotFoundError,
ServerError,
NetworkError,
TimeoutError,
isApiError,
isTimeout,
} from "@pitra/sdk";
try {
await pitra.pix.create({ amount: 19.9 });
} catch (err) {
if (err instanceof ValidationError) {
console.error("Payload inválido:", err.code, err.message);
} else if (err instanceof AuthenticationError) {
console.error("Chave inválida ou revogada");
} else if (err instanceof RateLimitError) {
console.error("Aguarde", err.retryAfter, "segundos");
} else if (isTimeout(err)) {
console.error("Timeout — consulte pelo external_id antes de recriar");
} else if (isApiError(err)) {
console.error(err.status, err.code, err.requestId);
} else {
throw err;
}
}Todo erro traz status, code, message e requestId quando a API devolver. Informe o requestId ao suporte.
Timeout
// Timeout padrão do cliente
const pitra = new PitraClient({ apiKey, timeout: 15000 });
// Timeout de uma chamada específica
await pitra.pix.get("pix_123", { timeout: 5000 });Implementado com AbortController. Ao estourar, o SDK lança TimeoutError.
Retry
No máximo uma tentativa adicional, apenas em timeout, erro de rede, 429, 500, 502, 503 e 504. Nenhum outro erro 4xx é repetido automaticamente. A retentativa reenvia a mesma chave de idempotência, então não há risco de cobrança duplicada.
Idempotência
// Chave definida por você
await pitra.pix.create({ amount: 19.9 }, { idempotencyKey: "pedido-1042" });
// Sem informar nada, o SDK gera uma UUID v4 e envia em Idempotency-Key
await pitra.pix.create({ amount: 19.9, external_id: "pedido-1042" });Além da chave, reutilizar o mesmo external_id devolve a cobrança existente em vez de criar outra.
Helpers
| Helper | O que faz |
|---|---|
formatMoney(19.9) | Formata o valor no padrão brasileiro: R$ 19,90. |
generateIdempotencyKey() | Gera uma UUID v4 para usar como Idempotency-Key. |
isApiError(err) | Indica se o erro veio do SDK ou da API Pitra. |
isTimeout(err) | Indica se o erro foi causado por timeout. |
Exemplos por framework
Cada exemplo tem link direto — por exemplo /docs/sdk#express ou /docs/sdk#cloudflare-workers.
Express#
import express from "express";
import { PitraClient, verifyWebhook } from "@pitra/sdk";
const app = express();
const pitra = new PitraClient({ apiKey: process.env.PITRA_API_KEY! });
app.post("/pedidos", express.json(), async (req, res) => {
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
res.json({ id: cobranca.id, copy_paste: cobranca.pix.copy_paste });
});
// A verificação da assinatura exige o corpo bruto.
app.post("/webhooks/pitra", express.raw({ type: "*/*" }), async (req, res) => {
try {
const evento = await verifyWebhook({
headers: req.headers,
rawBody: req.body.toString("utf8"),
secret: process.env.PITRA_WEBHOOK_SECRET!,
});
if (evento.type === "pix.paid") { /* libere o pedido */ }
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});Fastify#
import Fastify from "fastify";
import { PitraClient, verifyWebhook } from "@pitra/sdk";
const app = Fastify();
const pitra = new PitraClient({ apiKey: process.env.PITRA_API_KEY! });
app.post("/pedidos", async () => {
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
return { id: cobranca.id, copy_paste: cobranca.pix.copy_paste };
});
app.addContentTypeParser("*", { parseAs: "buffer" }, (_req, body, done) => done(null, body));
app.post("/webhooks/pitra", async (req, reply) => {
const evento = await verifyWebhook({
headers: req.headers as Record<string, string>,
rawBody: (req.body as Buffer).toString("utf8"),
secret: process.env.PITRA_WEBHOOK_SECRET!,
});
if (evento.type === "pix.paid") { /* libere o pedido */ }
return reply.code(200).send();
});NestJS#
import { Controller, Post, Body, Headers, Req, HttpCode } from "@nestjs/common";
import { PitraClient, verifyWebhook } from "@pitra/sdk";
import type { Request } from "express";
const pitra = new PitraClient({ apiKey: process.env.PITRA_API_KEY! });
@Controller()
export class PagamentosController {
@Post("pedidos")
async criar(@Body() _body: unknown) {
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
return { id: cobranca.id, copy_paste: cobranca.pix.copy_paste };
}
// Registre o rawBody no main.ts: NestFactory.create(AppModule, { rawBody: true })
@Post("webhooks/pitra")
@HttpCode(200)
async webhook(@Req() req: Request & { rawBody: Buffer }, @Headers() headers: Record<string, string>) {
const evento = await verifyWebhook({
headers,
rawBody: req.rawBody.toString("utf8"),
secret: process.env.PITRA_WEBHOOK_SECRET!,
});
return { received: evento.id };
}
}Next.js API Route#
// app/api/pedidos/route.ts
import { PitraClient } from "@pitra/sdk";
const pitra = new PitraClient({ apiKey: process.env.PITRA_API_KEY! });
export async function POST() {
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
return Response.json({ id: cobranca.id, copy_paste: cobranca.pix.copy_paste });
}
// app/api/webhooks/pitra/route.ts
import { verifyWebhook } from "@pitra/sdk";
export async function POST(request: Request) {
const rawBody = await request.text(); // texto bruto, antes de qualquer parse
try {
const evento = await verifyWebhook({
headers: Object.fromEntries(request.headers),
rawBody,
secret: process.env.PITRA_WEBHOOK_SECRET!,
});
if (evento.type === "pix.paid") { /* libere o pedido */ }
return new Response("ok");
} catch {
return new Response("assinatura inválida", { status: 400 });
}
}Next.js Server Action#
"use server";
import { PitraClient } from "@pitra/sdk";
const pitra = new PitraClient({ apiKey: process.env.PITRA_API_KEY! });
export async function criarCobranca(pedidoId: string, valor: number) {
const cobranca = await pitra.pix.create(
{ amount: valor, external_id: pedidoId, expires_in: 1800, customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" } },
{ idempotencyKey: pedidoId },
);
return { id: cobranca.id, copyPaste: cobranca.pix.copy_paste, qr: cobranca.pix.qr_code_base64 };
}Bun#
import { PitraClient, verifyWebhook } from "@pitra/sdk";
const pitra = new PitraClient({ apiKey: Bun.env.PITRA_API_KEY! });
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/pedidos" && req.method === "POST") {
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
return Response.json({ id: cobranca.id, copy_paste: cobranca.pix.copy_paste });
}
if (url.pathname === "/webhooks/pitra" && req.method === "POST") {
const evento = await verifyWebhook({
headers: Object.fromEntries(req.headers),
rawBody: await req.text(),
secret: Bun.env.PITRA_WEBHOOK_SECRET!,
});
return Response.json({ received: evento.id });
}
return new Response("not found", { status: 404 });
},
});Cloudflare Workers#
import { PitraClient, verifyWebhook } from "@pitra/sdk";
type Env = { PITRA_API_KEY: string; PITRA_WEBHOOK_SECRET: string };
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const pitra = new PitraClient({ apiKey: env.PITRA_API_KEY });
const url = new URL(request.url);
if (url.pathname === "/pedidos") {
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
return Response.json({ id: cobranca.id, copy_paste: cobranca.pix.copy_paste });
}
if (url.pathname === "/webhooks/pitra") {
const evento = await verifyWebhook({
headers: Object.fromEntries(request.headers),
rawBody: await request.text(),
secret: env.PITRA_WEBHOOK_SECRET,
});
return Response.json({ received: evento.id });
}
return new Response("not found", { status: 404 });
},
};Vercel Functions#
// api/pedidos.ts — Vercel Function (runtime Node.js)
import { PitraClient } from "@pitra/sdk";
const pitra = new PitraClient({ apiKey: process.env.PITRA_API_KEY! });
export const config = { runtime: "nodejs" };
export default async function handler(request: Request) {
if (request.method !== "POST") return new Response("method not allowed", { status: 405 });
const cobranca = await pitra.pix.create({
amount: 49.9,
description: "Pedido 1042",
external_id: "pedido-1042",
expires_in: 1800,
customer: { email: "cliente@exemplo.com", whatsapp: "11999998888" },
});
return Response.json({ id: cobranca.id, copy_paste: cobranca.pix.copy_paste });
}