Skip to content

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


app/
api/
webhooks/
emitia/
route.ts ← Route Handler para recibir webhooks
actions/
invoice.ts ← Server Action para crear facturas

app/api/webhooks/emitia/route.ts
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;
}
}
}

app/actions/invoice.ts
'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();
}

Terminal window
# .env.local (nunca commitear)
EMITIA_API_KEY=sk_test_TU_CLAVE
EMITIA_WEBHOOK_SECRET=whsec_...