Ir al contenido

SDK de TypeScript

emitay es el cliente de la API para TypeScript y JavaScript. No tiene dependencias, sus tipos se generan del contrato de la API y trae la línea de comandos.

Ventana de terminal
npm install emitay

Corre en Node 18 o superior, Deno, Bun y workers de borde: solo usa fetch y Web Crypto. Se publica como módulo ES y como CommonJS, así que sirve igual con import que con require.

import { Emitay } from "emitay";
const emitay = new Emitay(); // toma la llave de EMITAY_API_KEY
const empresa = await emitay.company.get();
console.log(`${empresa.legal_name} (RUC ${empresa.ruc})`);

Sin argumentos toma la llave de la variable EMITAY_API_KEY. También se le puede dar, con sus opciones:

import { Emitay } from "emitay";
const emitay = new Emitay(process.env.EMITAY_API_KEY, {
maxRetries: 2, // reintentos de cada llamada; 0 los desactiva
timeoutMs: 60_000, // cuánto espera una respuesta
});
Recurso Métodos
emitay.invoices, receipts, creditNotes, debitNotes, despatchAdvices, retentions, perceptions create
emitay.documents get, list, pages, waitFor, void, pdf, xml, cdr, sendEmail, emails
emitay.events get, list, pages, stream
emitay.webhookEndpoints create, get, list, pages, update, delete, rotateSecret, deliveries, retryDelivery
emitay.layouts create, get, list, pages, update, preview
emitay.company get, series, requestLogs

Todo lo que emite devuelve el mismo recurso, Document.

import { outcomeOf } from "emitay";
const emitida = await emitay.receipts.create(
{
series: "B001",
items: [{ description: "Polo de algodón", quantity: 1, unit_price: "59.00", price_includes_tax: true }],
},
{ wait: 10 },
);
const boleta = await emitay.documents.waitFor(emitida);
const sunat = outcomeOf(boleta);
if (sunat.valid) console.log(boleta.links.pdf);
else console.error(sunat.code, sunat.description);
  • wait espera a SUNAT en la misma llamada, hasta 30 segundos.
  • documents.waitFor sigue consultando un comprobante pendiente; uno que ya tiene veredicto vuelve sin hacer ninguna petición.
  • outcomeOf resume la respuesta de SUNAT en un objeto cuyo tipo sigue al status; valid es true solo en accepted y observed.

El detalle está en El veredicto de SUNAT.

const controlador = new AbortController();
await emitay.invoices.create(body, {
idempotencyKey: "factura-pedido-8412", // tu propia llave de idempotencia
wait: 10, // segundos que la respuesta espera a SUNAT
signal: controlador.signal, // para cancelar
});

Si no das idempotencyKey, el SDK genera una por llamada y la repite en sus reintentos: ver Idempotencia.

list trae una página; pages las recorre todas, pidiendo cada una cuando el bucle la necesita.

const pagina = await emitay.documents.list({ series: "F001", number: 123 });
console.log(pagina.data.length, pagina.has_more);
for await (const cadaPagina of emitay.documents.pages({ status: "rejected" })) {
for (const documento of cadaPagina.data) console.log(documento.series, documento.number);
}

Los listados que cuelgan de otro recurso llevan el id delante: emitay.documents.emails.list(id) y emitay.webhookEndpoints.deliveries.list(id).

Todo lo que el cliente lanza extiende EmitayError, con mensajes en español:

Clase Cuándo Qué trae
EmitayApiError La API respondió con un problema status, code, detail, errors, fields, requestId
EmitayConnectionError No hubo respuesta tras los reintentos La causa
EmitayTimeoutError waitFor agotó su plazo El comprobante, aún pendiente, en document
EmitayWebhookError Una entrega no pasó la verificación El motivo

El manejo de cada una está en Errores.

verifyWebhook comprueba la firma de una entrega y devuelve el evento con su tipo; signWebhook firma una entrega de prueba, para los tests de tu receptor.

import { signWebhook, verifyWebhook } from "emitay";
const secret = "whsec_c2VjcmV0by1kZS1wcnVlYmE=";
const payload = JSON.stringify({ id: "evt_1", type: "document.accepted" });
const headers = await signWebhook({ payload, secret, id: "evt_1" });
const event = await verifyWebhook({ payload, headers, secret });
console.log(event.type);

Ver Webhooks.

Los tipos de cada petición y de cada recurso se exportan desde el paquete, con la descripción de cada campo en tu editor:

import type { CreateInvoice, Document, Event } from "emitay";
const cuerpo: CreateInvoice = {
series: "F001",
customer: { document_number: "20100066603", name: "CLIENTE S.A." },
items: [{ description: "Servicio de consultoría", quantity: 2, unit_price: "50.00" }],
};
function alRecibir(evento: Event): Document | undefined {
return evento.type === "document.accepted" ? evento.data.object : undefined;
}

Los códigos de los catálogos de SUNAT son string donde tu sistema ya los guarda como texto (el tipo de documento del cliente, el motivo de una nota): pasas tu valor sin convertirlo y la API lo valida.

No uses el SDK con tu llave en el navegador: una llave de API es un secreto de servidor, y la API no acepta llamadas desde páginas de otros orígenes. Desde el navegador, llama a tu propio servidor.