Manual de integración
Consulta de RUC y DNI contra los padrones de SUNAT, tipo de cambio oficial y envío por WhatsApp. Todo con un token, sobre HTTPS y devolviendo JSON.
Tu token
Todo va con un token que saca el titular de la cuenta desde el portal. No hay registro público: la cuenta la crea un administrador y entrega el correo y la contraseña de acceso.
Entrar en el portal con el correo y la contraseña que entregó el administrador.
Generar el token en APIs de consulta. Se enseña una sola vez.
Mandarlo en la cabecera Authorization de cada
petición.
Authorization: Bearer 12|aBcD3fGhIjKlMnOpQrStUvWxYz... Accept: application/json
Si generas uno nuevo, el anterior deja de valer al instante. Actualiza el sistema que lo estuviera usando antes de generarlo, no después.
La primera llamada
Todas las rutas cuelgan de esta base. Es HTTPS y solo HTTPS: una petición por HTTP no se atiende.
https://portal.api.facturantes.com/api/v1
Prueba con un RUC cualquiera. Si responde 200, ya estás
integrado. Elige tu lenguaje:
curl https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013 \ -H 'Accept: application/json' \ -H 'Authorization: Bearer TU_TOKEN'
$respuesta = Http::withToken('TU_TOKEN')
->acceptJson()
->get('https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013');
if ($respuesta->successful()) {
$datos = $respuesta->json('data');
}
const respuesta = await fetch('https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer TU_TOKEN',
},
});
const { data } = await respuesta.json();
import requests
r = requests.get(
"https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013",
headers={
"Accept": "application/json",
"Authorization": "Bearer TU_TOKEN",
},
)
datos = r.json()["data"]
Qué hacer con cada error
No todos los fallos se tratan igual, y confundirlos es lo que rompe una integración en producción. Hay tres familias:
| Familia | Códigos | Qué hacer |
|---|---|---|
| Culpa de la petición | 401 · 403 · 422 | No reintentes. Repetir da exactamente lo mismo. Corrige el dato o la credencial. |
| Sin datos | 404 | No es un fallo. El documento es válido pero no está en el padrón. Trátalo como «sin resultados», no como avería. |
| Temporal | 429 · 503 |
Reintenta con espera. En el 429, la que diga
Retry-After; en el 503, sube el intervalo en cada
intento.
|
Comunes a todas las llamadas
La respuesta siempre tiene la misma forma
Lo que sale bien viene dentro de data; lo que sale mal,
dentro de error. Nunca los dos, así que puedes decidir
mirando cuál llegó.
{
"data": {
"razon_social": "REXTIE S.A.C.",
"estado": "ACTIVO"
}
}
{
"error": {
"code": "datos_invalidos",
"message": "El RUC no supera la comprobación
del dígito verificador.",
"details": { }
}
}
Decide por code, no por el mensaje
error.code es estable y está pensado para que tu programa
lo compare. error.message está escrito para una persona y
puede cambiar de redacción sin previo aviso — no lo uses en un
if.
Las fechas van en ISO 8601
Siempre AAAA-MM-DD, y las que llevan hora con su huso
horario (2026-08-28T09:14:02-05:00). Da igual en qué
formato lo publique SUNAT: aquí llega normalizado.
Los campos que no aplican vienen a null
No desaparecen del JSON ni llegan como cadena vacía. Un campo que
existe siempre existe; si no hay dato, vale null. Puedes
mapear la respuesta sin comprobar si la clave está.
Límites de ritmo
Se cuentan por integración, no por IP. Al pasarte recibes un
429 con la cabecera Retry-After diciendo
cuántos segundos esperar. Reintentar en bucle mantiene el contador
arriba y alarga el bloqueo en vez de acortarlo.
| Qué | Límite | Por qué ese |
|---|---|---|
|
Consultas al padrón
RUC, DNI, tipo de cambio
|
120 / minuto | Es el de una cuenta nueva. El tuyo lo ves en el panel y se puede subir si lo necesitas. |
| Ficha web de SUNAT | 20 / minuto | Cada llamada son varias peticiones contra SUNAT. |
| Envío por WhatsApp | 30 / minuto | Lo que quema un número no es el total del día: es la ráfaga. |
Consulta de RUC
Razón social, estado, condición y la dirección desglosada: ubigeo, tipo y nombre de vía, número, interior, distrito, provincia y departamento. Sale de los padrones ya importados, sin salir a la red.
| Parámetro | Obligatorio | Formato |
|---|---|---|
| ruc | Sí | Once dígitos. Se comprueba el dígito verificador antes de consultar. |
curl https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013 \ -H 'Accept: application/json' \ -H 'Authorization: Bearer TU_TOKEN'
$respuesta = Http::withToken('TU_TOKEN')
->acceptJson()
->get('https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013');
if ($respuesta->successful()) {
$datos = $respuesta->json('data');
}
const respuesta = await fetch('https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer TU_TOKEN',
},
});
const { data } = await respuesta.json();
import requests
r = requests.get(
"https://portal.api.facturantes.com/api/v1/padron/ruc/20601030013",
headers={
"Accept": "application/json",
"Authorization": "Bearer TU_TOKEN",
},
)
datos = r.json()["data"]
{
"data": {
"razon_social": "REXTIE S.A.C.",
"numero_documento": "20601030013",
"nombre_comercial": null,
"estado": "ACTIVO",
"condicion": "HABIDO",
"es_facturable": true,
"direccion": "CAL. LAS CAMELIAS NRO. 256 INT. 701A URB. JARDIN",
"ubigeo": "150131",
"via_tipo": "CAL.",
"via_nombre": "LAS CAMELIAS",
"zona_codigo": "URB.",
"zona_tipo": "JARDIN",
"numero": "256",
"interior": "701A",
"lote": "-",
"dpto": "LIMA",
"manzana": "-",
"kilometro": "-",
"distrito": "SAN ISIDRO",
"provincia": "LIMA",
"departamento": "LIMA",
"es_agente_retencion": false,
"es_agente_percepcion": false,
"es_buen_contribuyente": false,
"es_exceptuado_percepcion": false,
"renuncio_exoneracion": false,
"es_exportador_servicios": false,
"locales_anexos": null,
"tipo": null,
"actividad_economica": null,
"actividades_economicas": [],
"numero_trabajadores": null,
"numero_prestadores_servicio": null,
"periodo_trabajadores": null,
"tipo_facturacion": null,
"tipo_contabilidad": null,
"comercio_exterior": null,
"es_emisor_electronico": true,
"emisor_electronico_desde": "2018-07-30",
"comprobantes_electronicos": [
{
"comprobante": "FACTURA",
"desde": "2018-07-30"
},
{
"comprobante": "BOLETA DE VENTA",
"desde": "2019-07-09"
}
],
"comprobantes_autorizados": [],
"afiliado_ple_desde": null,
"fecha_inscripcion": null,
"fecha_inicio_actividades": null,
"padrones": [],
"fuente": "padron",
"consultado_en": "2026-08-27T20:02:18-05:00",
"actualizado_en": "2026-08-23T02:15:01-05:00"
}
}
Consulta de DNI
SUNAT construye el RUC de una persona natural como 10 + DNI + dígito verificador, así que la búsqueda es la misma de siempre y resuelve al instante a los 15,4 millones con RUC. A quien no lo tiene se le busca después en una fuente de respaldo, que además devuelve el nombre partido en `nombres`, `apellido_paterno` y `apellido_materno`. Mira `fuente` para saber de dónde salió la respuesta.
| Parámetro | Obligatorio | Formato |
|---|---|---|
| dni | Sí | Ocho dígitos. |
curl https://portal.api.facturantes.com/api/v1/padron/dni/00000006 \ -H 'Accept: application/json' \ -H 'Authorization: Bearer TU_TOKEN'
$respuesta = Http::withToken('TU_TOKEN')
->acceptJson()
->get('https://portal.api.facturantes.com/api/v1/padron/dni/00000006');
if ($respuesta->successful()) {
$datos = $respuesta->json('data');
}
const respuesta = await fetch('https://portal.api.facturantes.com/api/v1/padron/dni/00000006', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer TU_TOKEN',
},
});
const { data } = await respuesta.json();
import requests
r = requests.get(
"https://portal.api.facturantes.com/api/v1/padron/dni/00000006",
headers={
"Accept": "application/json",
"Authorization": "Bearer TU_TOKEN",
},
)
datos = r.json()["data"]
{
"data": {
"tipo_documento": "DNI",
"numero_documento": "00000006",
"ruc": "10000000065",
"nombre_completo": "SILVA RIVERA DE RIOS ALICIA",
"nombres": null,
"apellido_paterno": null,
"apellido_materno": null,
"estado": "ACTIVO",
"condicion": "HABIDO",
"es_facturable": true,
"direccion": null,
"ubigeo": null,
"via_tipo": "-",
"via_nombre": "-",
"zona_codigo": "-",
"zona_tipo": "-",
"numero": "-",
"interior": "-",
"lote": "-",
"dpto": "-",
"manzana": "-",
"kilometro": "-",
"distrito": null,
"provincia": null,
"departamento": null,
"es_agente_retencion": false,
"es_agente_percepcion": false,
"es_buen_contribuyente": false,
"es_exceptuado_percepcion": false,
"renuncio_exoneracion": false,
"es_exportador_servicios": false,
"locales_anexos": null,
"actividad_economica": null,
"actividades_economicas": [],
"numero_trabajadores": null,
"numero_prestadores_servicio": null,
"periodo_trabajadores": null,
"tipo_facturacion": null,
"tipo_contabilidad": null,
"comercio_exterior": null,
"es_emisor_electronico": false,
"emisor_electronico_desde": null,
"comprobantes_electronicos": [],
"comprobantes_autorizados": [],
"afiliado_ple_desde": null,
"fecha_inscripcion": null,
"fecha_inicio_actividades": null,
"padrones": [],
"fuente": "padron",
"consultado_en": "2026-08-27T22:35:16-05:00",
"actualizado_en": "2026-08-23T00:23:33-05:00"
}
}
Tipo de cambio
El que hay que usar al emitir en dólares: SUNAT exige el de la fecha del comprobante, no el del banco ni el del día en que se envía. Sale del fichero que SUNAT publica cada día.
curl https://portal.api.facturantes.com/api/v1/tipo-cambio \ -H 'Accept: application/json' \ -H 'Authorization: Bearer TU_TOKEN'
$respuesta = Http::withToken('TU_TOKEN')
->acceptJson()
->get('https://portal.api.facturantes.com/api/v1/tipo-cambio');
if ($respuesta->successful()) {
$datos = $respuesta->json('data');
}
const respuesta = await fetch('https://portal.api.facturantes.com/api/v1/tipo-cambio', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer TU_TOKEN',
},
});
const { data } = await respuesta.json();
import requests
r = requests.get(
"https://portal.api.facturantes.com/api/v1/tipo-cambio",
headers={
"Accept": "application/json",
"Authorization": "Bearer TU_TOKEN",
},
)
datos = r.json()["data"]
{
"data": {
"moneda": "USD",
"fecha": "2026-08-28",
"compra": 3.34,
"venta": 3.348,
"es_de_hoy": true,
"consultado_en": "2026-08-28T09:14:02-05:00",
"fuente": "sunat"
}
}
Ficha web de SUNAT
Añade lo que el padrón no publica: tipo de contribuyente, actividad económica (CIIU), si es emisor electrónico, padrones especiales y número de trabajadores.
| Parámetro | Obligatorio | Formato |
|---|---|---|
| ruc | Sí | Once dígitos. |
curl https://portal.api.facturantes.com/api/v1/sunat/ruc/20601030013 \ -H 'Accept: application/json' \ -H 'Authorization: Bearer TU_TOKEN'
$respuesta = Http::withToken('TU_TOKEN')
->acceptJson()
->get('https://portal.api.facturantes.com/api/v1/sunat/ruc/20601030013');
if ($respuesta->successful()) {
$datos = $respuesta->json('data');
}
const respuesta = await fetch('https://portal.api.facturantes.com/api/v1/sunat/ruc/20601030013', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer TU_TOKEN',
},
});
const { data } = await respuesta.json();
import requests
r = requests.get(
"https://portal.api.facturantes.com/api/v1/sunat/ruc/20601030013",
headers={
"Accept": "application/json",
"Authorization": "Bearer TU_TOKEN",
},
)
datos = r.json()["data"]
{
"data": {
"razon_social": "REXTIE S.A.C.",
"numero_documento": "20601030013",
"nombre_comercial": "REXTIE",
"estado": "ACTIVO",
"condicion": "HABIDO",
"es_facturable": true,
"direccion": "CAL.LAS CAMELIAS NRO. 256 INT. 701A URB. JARDIN",
"ubigeo": null,
"via_tipo": "-",
"via_nombre": "-",
"zona_codigo": "-",
"zona_tipo": "-",
"numero": "-",
"interior": "-",
"lote": "-",
"dpto": "LIMA",
"manzana": "-",
"kilometro": "-",
"distrito": "SAN ISIDRO",
"provincia": "LIMA",
"departamento": "LIMA",
"es_agente_retencion": false,
"es_agente_percepcion": false,
"es_buen_contribuyente": false,
"es_exceptuado_percepcion": false,
"renuncio_exoneracion": false,
"es_exportador_servicios": false,
"locales_anexos": null,
"tipo": "SOCIEDAD ANONIMA CERRADA",
"actividad_economica": "OTROS TIPOS DE INTERMEDIACIÓN MONETARIA",
"actividades_economicas": [
{
"tipo": "Principal",
"codigo": "6419",
"descripcion": "OTROS TIPOS DE INTERMEDIACIÓN MONETARIA"
}
],
"numero_trabajadores": 42,
"numero_prestadores_servicio": 7,
"periodo_trabajadores": "2026-06",
"tipo_facturacion": "MANUAL/COMPUTARIZADO",
"tipo_contabilidad": "MANUAL/COMPUTARIZADO",
"comercio_exterior": "SIN ACTIVIDAD",
"es_emisor_electronico": true,
"emisor_electronico_desde": "2018-07-30",
"comprobantes_electronicos": [
{
"comprobante": "FACTURA",
"desde": "2018-07-30"
},
{
"comprobante": "BOLETA",
"desde": "2019-07-09"
}
],
"comprobantes_autorizados": [
"FACTURA",
"BOLETA DE VENTA"
],
"afiliado_ple_desde": null,
"fecha_inscripcion": "2016-02-22",
"fecha_inicio_actividades": "2016-02-22",
"padrones": [
"NINGUNO"
],
"fuente": "sunat",
"consultado_en": "2026-08-23T00:02:35-05:00",
"actualizado_en": null
}
}
Cómo funciona el envío
Se contrata aparte de las consultas. Si tu integración no lo tiene,
estos dos endpoints responden 403 acceso_no_contratado.
Cada número es un dispositivo con su propio token. El token es el emisor: la petición no lleva ningún campo para elegir número, así que no hay nada que equivocar.
En el portal, APIs de WhatsApp → Agregar dispositivo. Nace con su token, visible en su fila.
Vincular saca un QR. Se escanea desde el móvil de ese número; el número no se teclea en ningún sitio.
Mandas con el token de ese dispositivo, no con el de las consultas.
Los dos envíos
Las rutas y los campos van en inglés, al revés que el resto de esta API. Son los del servicio que ya se usaba: un cliente escrito contra aquél cambia la URL y el token y sigue funcionando.
Texto
curl -X POST https://portal.api.facturantes.com/api/v1/send/text \ -H 'Accept: application/json' \ -H 'Authorization: Bearer TU_TOKEN'
$respuesta = Http::withToken('TU_TOKEN')
->acceptJson()
->post('https://portal.api.facturantes.com/api/v1/send/text');
if ($respuesta->successful()) {
$datos = $respuesta->json('data');
}
const respuesta = await fetch('https://portal.api.facturantes.com/api/v1/send/text', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer TU_TOKEN',
},
});
const { data } = await respuesta.json();
import requests
r = requests.post(
"https://portal.api.facturantes.com/api/v1/send/text",
headers={
"Accept": "application/json",
"Authorization": "Bearer TU_TOKEN",
},
)
datos = r.json()["data"]
{
"number": "51987654321",
"text": "Tu comprobante ya está disponible."
}
Archivos
El contenido va en base64 dentro del JSON. Uno solo
con file; varios con files, hasta cinco.
{
"number": "51987654321",
"file": "JVBERi0xLjQK...",
"filename": "F001-123.pdf",
"caption": "Adjuntamos tu comprobante."
}
{
"number": "51987654321",
"caption": "Tu comprobante F001-123",
"files": [
{ "file": "JVBERi0xLjQK...",
"filename": "F001-123.pdf" },
{ "file": "PD94bWwgdmVy...",
"filename": "F001-123.xml" }
]
}
| Campo | Obligatorio | Formato |
|---|---|---|
| number | Sí |
Destino con código de país y solo dígitos:
51987654321. Sin espacios, guiones ni paréntesis.
|
| file | Uno de los dos |
El contenido en base64, hasta 8 MB ya
descodificado. Un data: URI también vale: se le
quita el prefijo.
|
| files | Uno de los dos |
Lista de {file, filename}, hasta cinco. No se
puede mandar junto con file.
|
| filename | Sí |
Con su extensión: de ahí sale el tipo real del archivo. Un
.xml sin extensión llegaría al móvil con icono de
PDF y sin abrirse.
|
| filetype | No |
document (por defecto) o image. Una
imagen se ve en el chat; un documento sale como tarjeta con su
nombre.
|
| caption | No | Pie del adjunto, hasta 1000 caracteres. |
caption va en el
primero para que el cliente no lea dos veces lo mismo.
{
"data": {
"estado": "encolado",
"number": "51987654321",
"desde": "51984342396",
"mensajes": 2
}
}
Códigos de error
Decide por error.code, que es estable, y no por el
mensaje, que está escrito para una persona y puede cambiar de
redacción.
| HTTP | code | Qué pasó y qué hacer |
|---|---|---|
| 401 | no_autenticado | Falta el token o ya no vale. Comprueba la cabecera Authorization; si lo revocaste, genera otro. |
| 403 | cliente_inactivo | La integración está pendiente de aprobación o suspendida. No se arregla desde el código. |
| 403 | acceso_no_contratado | Tu integración no tiene contratada esa familia de API. Escríbenos. |
| 404 | ruc_no_encontrado | El RUC es válido pero no está en el padrón. No es un fallo: trátalo como «sin datos». |
| 404 | dni_no_encontrado | El DNI es válido pero no está en el padrón. Un tercio de los adultos no tiene RUC y no aparece en él, así que esto significa «no lo tenemos», no «no existe». |
| 403 | token_sin_dispositivo | Estás mandando con el token de las consultas. Usa el de la fila del dispositivo. |
| 409 | whatsapp_desconectado | Ese número está de alta pero no vinculado. No sirve reintentar: hay que escanear el QR desde el portal. |
| 422 | datos_invalidos | El documento no pasa el dígito verificador, falta un parámetro o el número de destino no lleva código de país. Revisa antes de reintentar: repetir da lo mismo. |
| 429 | demasiadas_peticiones | Te pasaste del ritmo. Espera los segundos que dice Retry-After en vez de reintentar en bucle. |
| 404 | no_encontrado | Esa ruta no existe. No es lo mismo que un documento que no está en el padrón: revisa la URL. |
| 503 | fuentes_no_disponibles | Ninguna fuente respondió. No es lo mismo que un 404: aquí no sabemos si existe, así que conviene reintentar más tarde. |
| 500 | error_interno | Algo se rompió de nuestro lado. Reintenta más tarde y avísanos si sigue pasando. |
Lo que más confunde
¿Cada cuánto se actualizan los padrones?
A diario, con lo que publica SUNAT. El campo
actualizado_en de cada respuesta dice exactamente cuándo
cambió por última vez esa ficha concreta.
¿Sirve el mismo token para consultas y WhatsApp?
No. El de consultas no pertenece a ningún número, así que en WhatsApp
da 403 token_sin_dispositivo. Cada dispositivo tiene el
suyo y es lo que decide por qué número sale el mensaje.
¿Puedo cambiar el móvil de un dispositivo sin tocar mi código?
Sí. Desvincular suelta el teléfono y deja el token intacto; escaneas el QR con otro móvil y sigues mandando con la misma credencial de siempre.
¿Por qué mi consulta de DNI no encuentra a alguien que existe?
Porque va contra los padrones de SUNAT, y quien no tiene RUC no está en ellos. No es un registro de identidad: es un registro de contribuyentes. Un 404 aquí significa «no lo tenemos», no «no existe».
¿Dónde veo cuánto llevo consumido?
En el portal, dentro de APIs de consulta, con el desglose por endpoint del mes en curso.