Skip to Content
ReceitasNext.js 15 (App Router)

Next.js 15 — App Router

Stack assumido: Next 15 + TypeScript + Prisma + zfiscoo-sdk.

1. Setup

pnpm add zfiscoo-sdk
.env.local
ZFISCOO_API_URL=https://api.zfiscoo.zek.app.br ZFISCOO_API_KEY=fk_live_xxxxxxxxxxxxxxxx ZFISCOO_WEBHOOK_SECRET=whsec_xxxxxxxx
lib/zfiscoo.ts
import { ZfiscooClient } from 'zfiscoo-sdk'; export const zfiscoo = new ZfiscooClient({ apiUrl: process.env.ZFISCOO_API_URL!, apiKey: process.env.ZFISCOO_API_KEY!, });

2. Server Action — emitindo NFC-e

app/pedidos/[id]/actions.ts
'use server'; import { revalidatePath } from 'next/cache'; import crypto from 'node:crypto'; import { prisma } from '@/lib/prisma'; import { zfiscoo } from '@/lib/zfiscoo'; export async function emitirNfce(pedidoId: string) { const pedido = await prisma.pedido.findUniqueOrThrow({ where: { id: pedidoId }, include: { items: true }, }); // Idempotency-Key determinístico por pedido — retry seguro const idemKey = `nfce:${pedidoId}`; const result = await zfiscoo.nfce.create({ body: { issuer_id: process.env.ZFISCOO_ISSUER_ID!, external_ref: pedidoId, items: pedido.items.map((i) => ({ description: i.nome, quantity: i.qty, unit_price: Number(i.preco), ncm: i.ncm, cfop: '5102', unit: 'UN', cst_icms: '00', })), payment: { method: pedido.metodoPagamento, amount: Number(pedido.total) }, }, idempotencyKey: idemKey, }); await prisma.pedido.update({ where: { id: pedidoId }, data: { nfceId: result.id, nfceStatus: result.status }, }); revalidatePath(`/pedidos/${pedidoId}`); return result; }

3. Webhook handler

Next.js 15 com Route Handlers. Crítico: precisa do raw body pra validar HMAC, NÃO use req.json().

app/api/zfiscoo/webhook/route.ts
import { NextResponse } from 'next/server'; import crypto from 'node:crypto'; import { prisma } from '@/lib/prisma'; export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; // crypto não funciona em edge export async function POST(req: Request) { const raw = await req.text(); const signature = req.headers.get('x-signature') ?? ''; const expected = crypto .createHmac('sha256', process.env.ZFISCOO_WEBHOOK_SECRET!) .update(raw, 'utf8') .digest('hex'); const received = signature.replace(/^sha256=/, ''); if ( expected.length !== received.length || !crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'), ) ) { return NextResponse.json({ error: 'invalid signature' }, { status: 401 }); } const event = JSON.parse(raw) as { id: string; type: string; data: { id: string; external_ref: string; access_key?: string }; }; // De-dupe por event.id — gateway pode reentregar const seen = await prisma.webhookEvent.findUnique({ where: { id: event.id } }); if (seen) return NextResponse.json({ ok: true, deduped: true }); await prisma.webhookEvent.create({ data: { id: event.id, type: event.type } }); if (event.type === 'nfce.authorized') { await prisma.pedido.update({ where: { id: event.data.external_ref }, data: { nfceStatus: 'authorized', chaveAcesso: event.data.access_key, }, }); } return NextResponse.json({ ok: true }); }

4. SSE no client (página de pedido)

app/pedidos/[id]/StatusStream.tsx
'use client'; import { useEffect, useState } from 'react'; export function StatusStream({ nfceId, sandboxKey }: { nfceId: string; sandboxKey: string }) { const [status, setStatus] = useState<string>('processing'); useEffect(() => { const es = new EventSource( `${process.env.NEXT_PUBLIC_ZFISCOO_API_URL}/v1/nfce/${nfceId}/events?key=${sandboxKey}`, ); es.addEventListener('status', (e) => { const evt = JSON.parse(e.data); setStatus(evt.status); if (['authorized', 'denied', 'cancelled'].includes(evt.status)) es.close(); }); return () => es.close(); }, [nfceId, sandboxKey]); return <span>Status: {status}</span>; }
🔒

Nunca exponha fk_live_... no client. Use o sandbox key público (fk_sandbox_public) pra SSE no browser durante testes, ou um endpoint proxy seu (/api/nfce/stream) que faz o fetch server-side.