Ir al contenido

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.

  1. 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.

  2. Ventana de terminal
    npm install emitay
    export EMITAY_API_KEY=sk_test_…
    npx emitay whoami

    whoami responde con tu empresa: la llave funciona.

  3. Guarda esto como factura.mjs:

    import { Emitay, outcomeOf } from "emitay";
    const emitay = new Emitay(); // toma la llave de EMITAY_API_KEY
    const 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.mjs
    F001-1 por PEN 118.00
    SUNAT: accepted (0) La Factura numero F001-1, ha sido aceptada
    https://files.emitay.com/files/…/F001-1.pdf

    Enviaste 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.

  4. 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 listen reenví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:3000
    Escuchando los eventos de MI EMPRESA S.A.C. (RUC 20…), en modo de prueba
    Reenviando 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.mjs

    Y en una tercera, emite otra factura:

    Ventana de terminal
    node factura.mjs

    Tu receptor la recibe, comprueba que la firmó Emitay y lo dice:

    Webhook verificado: F001-2 aceptada

    Y emitay listen muestra el evento y lo que respondió tu servidor:

    17:32:56 document.accepted Factura F001-2 (doc_…) — SUNAT 0
    → 204 No Content (16 ms)
    • 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.

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:

Ventana de terminal
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" }]
}'

La 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.