Inicio rápido
De cero a una factura aceptada por el ambiente de pruebas de SUNAT y un webhook verificado
en tu máquina. Cada bloque se puede pegar tal cual. Necesitas Node 18 o superior; si solo
quieres ver la API responder, con curl basta.
-
Consigue tu llave de prueba
Sección titulada «Consigue tu llave de prueba»Crea tu cuenta en app.emitay.com y confirma tu correo. El panel te pide tres datos de tu empresa —RUC, razón social y dirección fiscal— y te entrega una llave de prueba (
sk_test_…). Se muestra una sola vez: cópiala.No hay nada más que configurar: con ella los comprobantes se firman con un certificado de prueba y van al ambiente beta de SUNAT, sin valor tributario.
-
Instala el paquete
Sección titulada «Instala el paquete»Ventana de terminal npm install emitayexport EMITAY_API_KEY=sk_test_…npx emitay whoamiwhoamiresponde con tu empresa: la llave funciona. -
Emite una factura
Sección titulada «Emite una factura»Guarda esto como
factura.mjs:import { Emitay, outcomeOf } from "emitay";const emitay = new Emitay(); // toma la llave de EMITAY_API_KEYconst emitida = await emitay.invoices.create({series: "F001",customer: { document_number: "20100066603", name: "CLIENTE S.A." },items: [{ description: "Servicio de consultoría", quantity: 2, unit_price: "50.00" }],},{ wait: 10 },);const factura = await emitay.documents.waitFor(emitida);const sunat = outcomeOf(factura);console.log(`${factura.series}-${factura.number} por ${factura.currency} ${factura.total}`);console.log(`SUNAT: ${sunat.status} (${sunat.code}) ${sunat.description}`);console.log(factura.links.pdf);Ventana de terminal node factura.mjsF001-1 por PEN 118.00SUNAT: accepted (0) La Factura numero F001-1, ha sido aceptadahttps://files.emitay.com/files/…/F001-1.pdfEnviaste una serie, un cliente y lo que vendiste. Emitay calculó el IGV y el total, tomó el siguiente número de la serie, firmó el XML y lo envió. El enlace abre el PDF.
-
Recibe el resultado por webhook
Sección titulada «Recibe el resultado por webhook»Un backend no espera a SUNAT: recibe un webhook cuando hay veredicto.
Guarda esto como
webhook.mjs:import { createServer } from "node:http";import { EmitayWebhookError, verifyWebhook } from "emitay";createServer(async (request, response) => {const chunks = [];for await (const chunk of request) chunks.push(chunk);try {const event = await verifyWebhook({payload: Buffer.concat(chunks), // el cuerpo tal como llegóheaders: request.headers,secret: process.env.EMITAY_WEBHOOK_SECRET,});if (event.type === "document.accepted") {const factura = event.data.object;console.log(`Webhook verificado: ${factura.series}-${factura.number} aceptada`);}response.writeHead(204).end();} catch (error) {if (!(error instanceof EmitayWebhookError)) throw error;response.writeHead(400).end(error.message);}}).listen(process.env.PORT ?? 3000, () => console.log("Esperando webhooks"));En una terminal,
emitay listenreenvía cada evento de tu cuenta a tu máquina, firmado igual que una entrega real. Sin túnel y sin URL pública:Ventana de terminal npx emitay listen --forward-to http://localhost:3000Escuchando los eventos de MI EMPRESA S.A.C. (RUC 20…), en modo de pruebaReenviando a http://localhost:3000/Secreto de firma: whsec_…En otra terminal, arranca tu receptor con ese secreto:
Ventana de terminal export EMITAY_WEBHOOK_SECRET=whsec_…node webhook.mjsY en una tercera, emite otra factura:
Ventana de terminal node factura.mjsTu receptor la recibe, comprueba que la firmó Emitay y lo dice:
Webhook verificado: F001-2 aceptadaY
emitay listenmuestra el evento y lo que respondió tu servidor:17:32:56 document.accepted Factura F001-2 (doc_…) — SUNAT 0→ 204 No Content (16 ms) -
A producción
Sección titulada «A producción»-
En el panel, carga el certificado digital y el usuario SOL de tu empresa y crea una llave de producción (
sk_live_…). El código no cambia: cambia la llave. -
Registra la URL pública de tu receptor. El secreto solo se muestra aquí:
const endpoint = await emitay.webhookEndpoints.create({url: "https://erp.miempresa.pe/webhooks/emitay",events: ["document.accepted", "document.observed", "document.rejected"],});console.log(endpoint.secret); // whsec_… → EMITAY_WEBHOOK_SECRET en producción
El detalle de cada paso está en Pasar a producción.
-
Sin Node: con curl
Sección titulada «Sin Node: con curl»La API es HTTP y JSON: cualquier lenguaje la llama igual. Con tu llave en EMITAY_API_KEY
(export EMITAY_API_KEY=sk_test_…; en PowerShell, $env:EMITAY_API_KEY = "sk_test_…"), la
misma factura, esperando hasta 10 segundos la respuesta de SUNAT:
curl https://api.emitay.com/v1/invoices \ -H "Authorization: Bearer $EMITAY_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=10" \ -d '{ "series": "F001", "customer": { "document_number": "20100066603", "name": "CLIENTE S.A." }, "items": [{ "description": "Servicio de consultoría", "quantity": 2, "unit_price": "50.00" }] }'$cabeceras = @{ Authorization = "Bearer $env:EMITAY_API_KEY" Prefer = "wait=10"}$cuerpo = @'{ "series": "F001", "customer": { "document_number": "20100066603", "name": "CLIENTE S.A." }, "items": [{ "description": "Servicio de consultoría", "quantity": 2, "unit_price": "50.00" }]}'@Invoke-RestMethod -Method Post ` -Uri https://api.emitay.com/v1/invoices ` -Headers $cabeceras ` -ContentType "application/json; charset=utf-8" ` -Body $cuerpoLa respuesta es el comprobante. Si SUNAT contestó dentro de esos 10 segundos, ya trae su veredicto:
{ "id": "doc_…", "object": "document", "livemode": false, "type": "01", "series": "F001", "number": 1, "status": "accepted", "issue_date": "2026-10-08", "currency": "PEN", "total": "118.00", "amounts": { "taxed": "100.00", "exempt": "0.00", "unaffected": "0.00", "igv": "18.00" }, "sunat": { "code": "0", "description": "La Factura numero F001-1, ha sido aceptada", "notes": [], "simulated": false }, "links": { "pdf": "https://files.emitay.com/files/…/F001-1.pdf", "xml": "https://files.emitay.com/files/…/F001-1.xml", "cdr": "https://files.emitay.com/files/…/R-F001-1.zip" }}Se muestran los campos principales; la lista completa está en Recursos.
Qué sigue
Sección titulada «Qué sigue»- Emitir cada tipo de comprobante: boletas, notas, guías, retenciones.
- El veredicto de SUNAT: qué significa cada estado y cómo esperarlo.
- Webhooks: la firma, los reintentos y cómo no procesar dos veces.
- Errores: qué responde la API cuando algo no es válido.