Consultar comprobante
Estado de un comprobante. Es la vía para saber cómo terminó el envío, que corre de forma asíncrona.
Autenticación: token Bearer de empresa (cpe:sign).
Consideraciones
- Si el comprobante tiene una baja, viene en
voidcon suexternal_idy su estado.voidedes el hecho —SUNAT aceptó la baja—: una baja pedida y aún sin resolver dejavoided: false, porque puede ser rechazada y entonces el comprobante sigue declarado. - El
external_idlo devolvió la emisión. - Es solo lectura: no envía ni reenvía nada. El envío corre por su cuenta.
resolveddice si SUNAT ya se pronunció. Mientras seafalse, el comprobante sigue en camino:01es firmado y pendiente de salir,03es enviado esperando respuesta.- Sirve para todos los tipos: facturas, boletas, notas, resúmenes (RC/RA/RR) y guías (09/31).
- Los resúmenes y las guías tardan más. SUNAT los responde en dos tiempos, así que pueden pasar minutos en
resuelto: false. Es normal, no un fallo. Elticketque aparece es el identificador que SUNAT usa para esa espera — no tienes que hacer nada con él. resulttrae lo que respondió SUNAT en el último intento: código, mensaje y la clasificaciónorigin/action/reached_sunat. Esnullmientras no haya habido intento.reached_sunatviaja siempre dentro de él, también ennull— que significa «no se sabe», y es la señal de consultar antes de reintentar.- Tipos:
numberes un entero (no cadena, a diferencia deseriesystatus_code);voided,voided_atyvoidviajan en todos los comprobantes, connull/falsecuando no aplican. - Solo puedes consultar comprobantes de la empresa del token: los ajenos responden
403.
Si prefieres no preguntar: webhook
Consultar es barato (milisegundos), pero sigue siendo tu integración preguntando
en bucle. Con el webhook lo invertimos: cuando el
comprobante queda resuelto, te hacemos un POST con este mismo objeto document.
El webhook es aviso, no contrato — este endpoint sigue siendo la fuente de verdad.
Cómo saber si terminó
Mira resolved, no status_code: es un solo booleano en vez de una lista de
códigos que recordar.
status_code |
status |
resolved |
Qué significa |
|---|---|---|---|
01 |
registered |
false |
Firmado, pendiente de salir |
02 |
to_send |
false |
Firmado, esperando a que tú lo mandes |
03 |
sent |
false |
Enviado, esperando a SUNAT |
04 |
to_summarize |
false |
Boleta esperando el resumen del día |
05 |
accepted |
true |
Aceptado |
07 |
observed |
true |
Aceptado con observaciones |
09 |
rejected |
true |
Rechazado |
Un comprobante que se queda en 03 no está perdido: un proceso periódico
consulta su estado real ante SUNAT hasta resolverlo. No lo reenvíes.
Por qué falló, cuando falló
result trae lo que respondió SUNAT en el último intento. Con el envío fuera
del request, este es el único sitio donde leer el motivo de un rechazo:
"result": {
"code": "2335",
"message": "El XML no contiene información en el campo Total valor de venta",
"errors": ["2335: total del valor de venta descuadrado"],
"origin": "sunat",
"action": "correct",
"reached_sunat": true
}
action te dice qué hacer sin interpretar el texto: correct (arregla y emite
de nuevo), retry (vuelve a encolar), review (consulta, no reenvíes).
La clasificación completa está en Errores y respuestas.
Resúmenes y guías: el ticket no es asunto tuyo
Los resúmenes (RC/RA/RR) y las guías (09/31) no reciben el CDR al enviarse: SUNAT devuelve un ticket y procesa después. Alguien tiene que ir a preguntar el resultado — y ese alguien somos nosotros.
En cuanto el envío deja un ticket, arranca una consulta en segundo plano que
insiste hasta obtener un veredicto y guarda lo que aprende. Tú consultas este
endpoint, igual que para una factura, y ves resuelto: false hasta que haya
desenlace.
El campo ticket viene en la respuesta solo como dato de trazabilidad, por si
hay que cotejar una incidencia con SUNAT. No tienes que hacer nada con él.
Que la consulta salga de nuestra base y no de SUNAT tiene una consecuencia práctica: responde en milisegundos y puedes preguntar tan seguido como quieras. Si cada integración consultara a SUNAT por su cuenta, el servicio que ya es el cuello de botella recibiría muchísimo más tráfico del necesario.
Headers
| Name | Type | Description |
|---|---|---|
| Accept | string | application/json |
| Content-Type | string | application/json |
| Authorization | string | Bearer <token>. Genéralo desde tu panel, en Tokens de API. |
Parámetros de URL
| Name | Type | Description |
|---|---|---|
| external_id* | string | Identificador del comprobante devuelto al emitir. |
Ejemplo de solicitud
curl -X GET https://api.xmlperu.dev/v1/cpe/9c2f1b7e-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $TOKEN_EMPRESA" \
-H "Accept: application/json"<?php
$token_empresa = 'pega-aqui-tu-token';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.xmlperu.dev/v1/cpe/9c2f1b7e-0000-0000-0000-000000000000');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token_empresa,
'Accept: application/json',
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);const TOKEN_EMPRESA = 'pega-aqui-tu-token';
const res = await fetch('https://api.xmlperu.dev/v1/cpe/9c2f1b7e-0000-0000-0000-000000000000', {
method: 'GET',
headers: {
Authorization: `Bearer ${TOKEN_EMPRESA}`,
Accept: 'application/json',
},
});
const data = await res.json();import requests
TOKEN_EMPRESA = "pega-aqui-tu-token"
headers = {
"Authorization": f"Bearer {TOKEN_EMPRESA}",
"Accept": "application/json",
}
res = requests.get("https://api.xmlperu.dev/v1/cpe/9c2f1b7e-0000-0000-0000-000000000000", headers=headers)
data = res.json()Respuesta
{
"success": true,
"message": "Comprobante encontrado.",
"data": {
"document": {
"external_id": "9c2f1b7e-…",
"filename": "20123456789-01-F001-123",
"document_type_id": "01",
"series": "F001",
"number": 123,
"status_code": "05",
"status": "accepted",
"environment": "02",
"environment_name": "production",
"resolved": true,
"hash": "a3f1…9e2c",
"has_signed": true,
"has_cdr": true,
"ticket": null,
"date_of_issue": "2026-07-27",
"result": {
"code": "0",
"message": "La Factura numero F001-123, ha sido aceptada",
"reached_sunat": true
},
"voided": false,
"voided_at": null,
"void": null
}
}
}{
"success": true,
"message": "Comprobante encontrado.",
"data": {
"document": {
"external_id": "9c2f1b7e-…",
"filename": "20123456789-01-F001-123",
"document_type_id": "01",
"series": "F001",
"number": 123,
"status_code": "03",
"status": "sent",
"environment": "02",
"environment_name": "production",
"resolved": false,
"hash": "a3f1…9e2c",
"has_signed": true,
"has_cdr": false,
"ticket": null,
"date_of_issue": "2026-07-27",
"result": null,
"voided": false,
"voided_at": null,
"void": null
}
}
}resultado.{
"success": true,
"message": "Comprobante encontrado.",
"data": {
"document": {
"external_id": "9c2f1b7e-…",
"filename": "20123456789-01-F001-123",
"document_type_id": "01",
"series": "F001",
"number": 123,
"status_code": "09",
"status": "rejected",
"environment": "02",
"environment_name": "production",
"resolved": true,
"hash": "a3f1…9e2c",
"has_signed": true,
"has_cdr": false,
"ticket": null,
"date_of_issue": "2026-07-27",
"result": {
"code": "2335",
"message": "El XML no contiene información en el campo Total valor de venta",
"errors": ["2335: total del valor de venta descuadrado"],
"origin": "sunat",
"action": "correct",
"reached_sunat": true
},
"voided": false,
"voided_at": null,
"void": null
}
}
}{
"success": false,
"message": "El comprobante no existe."
}{
"success": false,
"message": "El comprobante no pertenece a esta empresa."
}