Next.js
Integración con Next.js App Router. El webhook va en un Route Handler; la creación de factura puede ir en un Server Action o una API route.
Emitia-Version: 2026-01-01
Estructura de archivos
Section titled “Estructura de archivos”app/ api/ webhooks/ emitia/ route.ts ← Route Handler para recibir webhooks actions/ invoice.ts ← Server Action para crear facturasRoute Handler para webhooks
Section titled “Route Handler para webhooks”import crypto from 'crypto';import { NextRequest, NextResponse } from 'next/server';
function verifySignature(rawBody: string, signature: string, secret: string): boolean { const parts = Object.fromEntries(signature.split(',').map((s) => s.split('='))); const t = parseInt(parts['t'] ?? '', 10); const v1 = parts['v1']; if (!t || !v1) return false; if (Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));}
export async function POST(req: NextRequest) { // IMPORTANTE: leer el body como texto crudo ANTES de parsearlo const rawBody = await req.text(); const signature = req.headers.get('emitia-signature') ?? ''; const secret = process.env.EMITIA_WEBHOOK_SECRET ?? '';
if (!verifySignature(rawBody, signature, secret)) { return NextResponse.json({ error: 'Firma inválida' }, { status: 401 }); }
const event = JSON.parse(rawBody) as { id: string; type: string; data: { object: Record<string, unknown> }; };
// Procesar en background para responder rápido void processEvent(event);
return NextResponse.json({ received: true });}
async function processEvent(event: { id: string; type: string; data: { object: Record<string, unknown> } }) { switch (event.type) { case 'invoice.accepted_by_dian': { const invoiceId = event.data.object['id'] as string; const cufe = event.data.object['cufe'] as string; // Actualizar tu base de datos, enviar email al cliente, etc. console.log(`Factura aceptada: ${invoiceId} CUFE: ${cufe}`); break; } case 'invoice.rejected_by_dian': { const invoiceId = event.data.object['id'] as string; console.error(`Factura rechazada: ${invoiceId}`); break; } }}Server Action para crear facturas
Section titled “Server Action para crear facturas”'use server';
import crypto from 'crypto';
const API_BASE = 'https://api.emitia.co/v1';
interface CreateInvoiceParams { customerId: string; description: string; unitPrice: string; orderId: string;}
export async function createInvoiceAction(params: CreateInvoiceParams) { const idempotencyKey = crypto.randomUUID();
const res = await fetch(`${API_BASE}/invoices`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMITIA_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, 'Emitia-Version': '2026-01-01', }, body: JSON.stringify({ customer_id: params.customerId, currency: 'COP', operation_type: '10', payment_means: { code: '10', type: '1' }, lines: [ { description: params.description, quantity: '1.00', unit_code: '94', unit_price: params.unitPrice, line_extension_amount: params.unitPrice, taxes: [ { code: '01', rate: '19.00', base: params.unitPrice, value: (parseFloat(params.unitPrice) * 0.19).toFixed(2), }, ], }, ], metadata: { order_id: params.orderId }, }), });
if (!res.ok) { const error = await res.json(); throw new Error(`Error al crear factura: ${JSON.stringify(error)}`); }
return res.json();}Variables de entorno
Section titled “Variables de entorno”# .env.local (nunca commitear)EMITIA_API_KEY=sk_test_TU_CLAVEEMITIA_WEBHOOK_SECRET=whsec_...Qué sigue
Section titled “Qué sigue”- Conceptos: Webhooks — detalles de la verificación HMAC
- Conceptos: Idempotencia — por qué guardas la clave antes de llamar
- Guía de mapeo POS — cómo elegir los códigos correctos