Enviar a SUNAT
Manda a SUNAT un XML ya firmado y devuelve su resultado. Es la segunda mitad del flujo generar → enviar.
Autenticación: token Bearer de empresa (cpe:sign).
Consideraciones
- Cierra el flujo: generar firma y enviar manda y te devuelve el resultado.
- Puedes declarar el entorno en la ruta:
/api/pro/demo/cpe/enviaro/api/pro/produccion/cpe/enviar(y el aliasenviar-demo). Si no coincide con el de tu empresa respondemos409y no se envía nada. Ver generar. - Dos formas de identificar el comprobante: por
external_id(recomendado, usa el XML ya almacenado) o enviando elnombre_xml_firmado+contenido_xml_firmado(base64) directamente. - Espera unos segundos a que SUNAT responda y devuelve
200con el CDR, el código y el mensaje. Es el comportamiento por defecto. - Si SUNAT no contesta a tiempo responde
202, te dice que eso fue lo que pasó y el envío sigue su curso. Ahí recoges el resultado con consultar o por webhook. - Los resúmenes, reversiones y guías responden siempre
202: SUNAT los resuelve por ticket y no hay CDR que esperar. Se consultan después, como en cualquier plataforma. - Es idempotente: mandar dos veces el mismo comprobante produce un solo envío.
- El comprobante debe estar registrado (firmado por nosotros). Un XML que nunca pasó por la firma responde
404. - Si mandas en lote y no quieres esperar en cada comprobante, la empresa puede desactivarlo con response: immediate.
- Existe también
POST /api/cpe/enviar/external_idcomo variante explícita.
Este es el endpoint que cierra la migración. Firmas con generar, mandas aquí, y el resultado viene en la respuesta — igual que en la plataforma de la que vienes.
curl -X POST https://api.xmlperu.dev/api/cpe/enviar -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"external_id": "9c2f1b7e-…", "xml_filename": "20123456789-01-F001-777"}'
Los dos desenlaces
200 |
202 |
|
|---|---|---|
| Qué pasó | SUNAT respondió | SUNAT no contestó a tiempo |
Trae cdr |
sí | no |
| Qué haces | nada más | consultas o esperas el webhook |
El 202 no es un error y no hay que reenviar. El comprobante está firmado,
el envío sigue su curso y el mensaje te dice exactamente eso. Reenviarlo podría
duplicarlo.
Un 200 tampoco significa «aceptado»: significa que hay veredicto. Míralo en
state_type_id — 05 aceptado, 07 con observaciones, 09 rechazado. La tabla
completa está en Cómo leer la respuesta.
Resúmenes y guías
Responden siempre 202. SUNAT no devuelve un CDR al recibirlos, sino un ticket
que hay que consultar más tarde: es así en cualquier plataforma, y por eso ahí ya
consultabas.
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. |
Body
| Name | Type | Description |
|---|---|---|
| external_id | string | Identificador devuelto por generar. Si lo envías, se usa el XML firmado ya almacenado (no hace falta reenviar el base64). |
| xml_filename* | string | Nombre en formato RUC-TIPO-SERIE-NUMERO (sin extensión). Acepta también nombre_xml_firmado. |
| contenido_xml_firmado | string | XML ya firmado en base64. Solo necesario si no envías external_id. Acepta también xml_signed_base64. |
Ejemplo de solicitud
curl -X POST https://api.xmlperu.dev/api/cpe/enviar \
-H "Authorization: Bearer $TOKEN_EMPRESA" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"external_id": "162c9e5f-e001-4b31-a1fd-83a4e3a30a51",
"xml_filename": "20123456789-03-B001-777"
}'<?php
$token_empresa = 'pega-aqui-tu-token';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.xmlperu.dev/api/cpe/enviar');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token_empresa,
'Accept: application/json',
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, <<<JSON
{
"external_id": "162c9e5f-e001-4b31-a1fd-83a4e3a30a51",
"xml_filename": "20123456789-03-B001-777"
}
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/api/cpe/enviar', {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN_EMPRESA}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"external_id": "162c9e5f-e001-4b31-a1fd-83a4e3a30a51",
"xml_filename": "20123456789-03-B001-777"
}),
});
const data = await res.json();import requests
TOKEN_EMPRESA = "pega-aqui-tu-token"
headers = {
"Authorization": f"Bearer {TOKEN_EMPRESA}",
"Accept": "application/json",
}
payload = {
"external_id": "162c9e5f-e001-4b31-a1fd-83a4e3a30a51",
"xml_filename": "20123456789-03-B001-777"
}
res = requests.post("https://api.xmlperu.dev/api/cpe/enviar", json=payload, headers=headers)
data = res.json()Respuesta
state_type_id, no el HTTP: un rechazo también llega como 200.{
"success": true,
"connection": true,
"sunat_success": true,
"llego_a_sunat": true,
"resuelto": true,
"estado": 200,
"state_type_id": "05",
"mensaje": "La Factura numero F001-102, ha sido aceptada",
"message": "La Factura numero F001-102, ha sido aceptada",
"code": "0",
"external_id": "9c2f1b7e-…",
"cdr": "UEsDBBQAAAgIA…",
"time": 2.24
}{
"success": true,
"estado": "en_cola",
"mensaje": "SUNAT no respondió en 5 segundos. El envío quedó encolado y sigue su curso: consulta el estado con /api/cpe/consultar/20123456789-01-F001-777.",
"message": "SUNAT no respondió en 5 segundos. El envío quedó encolado y sigue su curso: consulta el estado con /api/cpe/consultar/20123456789-01-F001-777.",
"external_id": "9c2f1b7e-…",
"time": 5.02
}{
"success": false,
"message": "El comprobante no está registrado: fírmalo primero (/cpe/generar) y vuelve a enviarlo."
}