API SUNAT con Node.js: emite facturas electrónicas con fetch

Publicado el

Desde Node.js 18 tienes fetch nativo, así que integrar API SUNAT no requiere dependencias. En esta guía emites una factura en SUNAT beta y verificas el webhook con Express.

Lo que necesitas

Node.js 18 o superior.

Una cuenta de API SUNAT y una clave de prueba asu_test_: emite en SUNAT beta, con numeración propia y sin consumir tu plan.

Un RUC de prueba registrado con su certificado digital. La guía de inicio rápido de la documentación explica cómo hacerlo desde el panel.

La clave guardada en la variable de entorno APISUNAT_KEY de tu servidor, nunca en el código ni en el navegador.

Emite una factura

Envía la venta a POST /documents. Con un RUC en customer.documentNumber puedes omitir el nombre: la API lo busca.

JSemitir-factura.mjs
// Node 18+ (fetch nativo)
import { randomUUID } from "node:crypto";

const response = await fetch("https://api.apisunat.net/api/v1/issuer/documents", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.APISUNAT_KEY,
    "Idempotency-Key": randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    documentType: "01",
    currency: "PEN",
    issueDate: "2026-10-27",
    customer: { documentType: "6", documentNumber: "20100066603" },
    items: [{ description: "Consultoría", quantity: 1, unitPrice: 1180, unitCode: "ZZ" }],
  }),
});
const body = await response.json();
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
console.log(body.fullNumber, body.status);

Reintenta sin emitir dos veces

POST /documents exige el header Idempotency-Key. Guarda esa clave junto a tu pedido y reúsala si reintentas: la misma clave con el mismo cuerpo devuelve el mismo resultado y no emite un comprobante duplicado.

Si SUNAT rechaza el comprobante, la API no responde con un error HTTP: responde 202 con status "rejected" y el motivo en sunat.errorCode y sunat.errorMessage.

Recibe la respuesta de SUNAT por webhook

La emisión responde de inmediato y SUNAT puede seguir procesando. Registra un webhook para recibir document.accepted, document.rejected o document.failed.

Verifica la firma del header X-Apisunat-Signature (HMAC-SHA256 de "<t>.<cuerpo crudo>" con tu secreto whsec_) y rechaza las entregas con más de 5 minutos de antigüedad.

JSwebhook.mjs (Express)
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.APISUNAT_WEBHOOK_SECRET; // whsec_...

app.post("/webhooks/apisunat", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("x-apisunat-signature") ?? "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = createHmac("sha256", secret).update(`${t}.${req.body}`).digest("hex");
  const valid =
    v1 &&
    v1.length === expected.length &&
    timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - Number(t)) < 300;
  if (!valid) return res.status(400).end();

  const event = JSON.parse(req.body);
  // Guarda event.id para ignorar duplicados y procesa en segundo plano.
  res.status(200).end();
});

Siguientes pasos

Para emitir boletas cambia documentType a "03"; para notas de crédito, a "07". Todos los campos y códigos de error están en la documentación de la API.

Cuando todo funcione en beta, cambia a una clave asu_live_ y habilita producción en tu RUC.

Otras guías