Ir al contenido

El veredicto de SUNAT

Un comprobante nace pending: ya tiene número, está firmado y existe para Emitay. SUNAT responde después: puede tardar segundos o minutos. La API no te hace esperar esa respuesta salvo que se lo pidas.

status Qué significa ¿Vale el comprobante?
pending SUNAT aún no responde. Emitay lo sigue enviando Todavía no se sabe
accepted SUNAT lo aceptó Sí
observed SUNAT lo aceptó con observaciones, que vienen en sunat.notes Sí. Conviene corregir la causa en los siguientes
rejected SUNAT lo rechazó: no es un comprobante válido No. Su número queda usado
voided Fue aceptado y luego anulado No

sunat.code y sunat.description son el código y las palabras de SUNAT, tal cual.

Prefer: wait=10 retiene la respuesta hasta 10 segundos (el máximo es 30) mientras Emitay espera a 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" }]
}'

Es una espera acotada, no otro camino: si SUNAT no contesta a tiempo, la respuesta llega igual, con status: "pending", y el comprobante sigue su curso.

GET /v1/documents/{id} devuelve el comprobante como está ahora. El SDK lo hace por ti, sin escribir un bucle:

import { EmitayTimeoutError, outcomeOf } from "emitay";
const emitida = await emitay.invoices.create(body, { wait: 10 });
try {
const factura = await emitay.documents.waitFor(emitida, { timeoutMs: 60_000 });
const sunat = outcomeOf(factura);
if (sunat.valid) {
console.log(`Aceptada: ${factura.links.pdf}`); // sunat.notes trae las observaciones
} else {
console.error(`Rechazada (${sunat.code}): ${sunat.description}`);
}
} catch (error) {
if (!(error instanceof EmitayTimeoutError)) throw error;
// Nada falló: SUNAT tarda. El comprobante sigue en camino, en error.document.
}

waitFor pregunta cada segundo y va espaciando hasta 5. Si agota su plazo lanza EmitayTimeoutError. No es un fallo y no hay que emitir de nuevo: emitir otra vez crearía otro comprobante.

Es lo que usa un backend: Emitay llama a tu servidor cuando hay veredicto, aunque tarde minutos y aunque tu proceso se haya reiniciado entretanto.

Evento Cuándo
document.accepted SUNAT aceptó
document.observed SUNAT aceptó con observaciones
document.rejected SUNAT rechazó, o venció el plazo sin respuesta

Cómo recibirlos y verificarlos está en Webhooks.

  • Emitay reintenta solo. Si SUNAT no responde o falla, se reenvía el mismo archivo con una espera que crece de 15 segundos a una hora. No tienes que hacer nada.
  • last_error dice por qué falló el último intento. Es informativo: mientras el estado sea pending, el comprobante sigue en camino.
  • Cada tipo tiene una ventana de envío que fija SUNAT: 3 días desde la emisión para una factura y sus notas, 5 para una boleta y las suyas, 1 para una guía. Si la ventana se cierra sin que SUNAT haya dado un veredicto, el comprobante termina rejected, con sunat: null y el motivo en last_error.

Un rechazo es definitivo para ese número: Emitay nunca regenera el XML de un número ya enviado. Lee sunat.code y sunat.description, corrige la causa y emite un comprobante nuevo, que tomará el siguiente número.

Lo que se puede saber antes, Emitay lo niega antes: la mayoría de los rechazos posibles se responden como 422 en la petición, sin consumir número.

const porId = await emitay.documents.get("doc_…");
const porNumero = await emitay.documents.list({ series: "F001", number: 123 });
for await (const pagina of emitay.documents.pages({ status: "rejected" })) {
for (const documento of pagina.data) console.log(documento.series, documento.number);
}

El listado filtra por status, type, series, number, issue_date_from e issue_date_to. En la terminal: npx emitay document F001-123.