Obtener el PDF de una Factura [BETA]
Funcionalidad en desarrollo y pruebas: este servicio está disponible en el ambiente de pruebas y todavía no debe usarse en producción. El funcionamiento, los campos y los mensajes de error pueden cambiar antes del despliegue definitivo.
Este servicio devuelve el enlace seguro de impresión del PDF de una factura de venta. El enlace lo genera nuby: apunta al visor público del documento y lleva un hash cifrado que lo protege. La ruta interna del archivo nunca se expone.
El enlace tiene una vigencia de 30 días. Después de esa fecha deja de funcionar, así que pídelo de nuevo cuando lo necesites en lugar de guardarlo como definitivo.
¿Para qué sirve este servicio?
Úsalo cuando necesites entregar el comprobante sin construir rutas ni manipular archivos. Los casos de uso más frecuentes son:
- Enviar la factura al cliente. Adjunta el enlace en un correo, un mensaje o tu portal de autogestión.
- Archivar el comprobante en tu sistema documental. Guarda el enlace junto al
factura_idy renuévalo cuando venza. - Imprimir o descargar el PDF desde el navegador del usuario final, sin instalar nada.
- Mostrar un botón "Ver factura" en tu aplicación, generando el enlace en el momento de la consulta.
- Evitar descargas masivas de archivos. Tu integración nunca necesita credenciales de almacenamiento: nuby resuelve el documento.
1. El Endpoint (La dirección web)
Método y dirección
| Dato | Valor |
|---|---|
| Método | GET |
| Recurso | /invoices/{factura_id}/pdf |
| Cuerpo de la petición | No se envía cuerpo |
GET https://{{instancia}}/service/v2/public/invoices/{factura_id}/pdf
¿Qué debo colocar en {{instancia}}?
Es la dirección web principal con la que ingresas a tu plataforma, incluyendo la terminación .nuby.app o .arrendasoft.co. Si entras a nuby escribiendo inmobiliaria.nuby.app en el navegador, esa es tu instancia.
Importante: no incluyas https:// ni barras diagonales (/) al final. La dirección se compone así: https:// + tu instancia + /service/v2/public/invoices/ + el identificador + /pdf.
Encabezados requeridos
| Encabezado | ¿Obligatorio? | Valor |
|---|---|---|
Authorization |
Sí | Bearer <token>, con el token obtenido en el servicio Login. |
Content-Type |
No | application/json si tu cliente lo envía. Como la petición no lleva cuerpo, el servicio no lo necesita. |
Permiso requerido (scope): catalogs:read
Este servicio es de solo lectura y exige el permiso catalogs:read en el cliente OAuth. Si el token es válido pero al cliente le falta el permiso, la respuesta es 403 con el código RECURSO_NO_AUTORIZADO.
¿Por qué catalogs:read y no un permiso de facturas? El libro publicado de la API documenta el permiso genérico read para los endpoints de consulta anteriores (los que ya existían). Los endpoints nuevos del ciclo administrativo usan el permiso por recurso, y en tu cliente OAuth los permisos de consulta disponibles son catalogs:read, invoices:create, invoices:update, notes:create y third-parties:create. Como no existe un permiso de lectura propio de facturas, esta consulta se autoriza con catalogs:read.
Si aún no tienes un token, consulta la página Login del libro API nuby V2.
Requisitos previos de esta operación
- La factura debe existir en nuby: si no, la respuesta es
404 FACTURA_NO_ENCONTRADA. - La factura debe ser de venta. Si se generó desde un contrato de arrendamiento, se rechaza con
409 FACTURA_CONTRATO_NO_SOPORTADAywarnings[]. - El cliente OAuth del token debe tener el permiso
catalogs:read.
2. La Petición (¿Qué debes enviarnos?)
Este servicio no requiere cuerpo. Solo necesitas el encabezado de autorización y el identificador de la factura en la dirección.
| Dato | Valor |
|---|---|
| Método | GET |
Content-Type |
application/json (opcional: la petición no tiene cuerpo) |
Authorization |
Bearer <token>, token obtenido al consumir el servicio Login |
Parámetros de la ruta (path)
| Parámetro | Tipo | Requerido | Por defecto | Descripción |
|---|---|---|---|---|
factura_id |
integer | Sí | — | Identificador interno de la factura. Debe ser un entero mayor que 0. Va en la dirección, antes del segmento /pdf. |
Si el identificador no es un entero positivo (por ejemplo /invoices/abc/pdf o /invoices/0/pdf), la respuesta es 400 con el mensaje "Debe indicar un factura_id entero mayor que 0 para operar sobre la factura." y el detalle del campo en errors.factura_id:
{
"success": false,
"status": 400,
"message": "Debe indicar un factura_id entero mayor que 0 para operar sobre la factura.",
"error_code": "VALIDATION_ERROR",
"errors": {
"factura_id": [
"Debe indicar un factura_id entero mayor que 0 para operar sobre la factura."
]
}
}
¿De dónde saco el factura_id?
- De la creación de la factura (
POST /invoices): la respuesta lo devuelve enbody.factura_id. - Del listado (
GET /invoices/sales/list): cada fila delbodylo trae enfactura_id. - Del detalle (
GET /invoices/{factura_id}): lo devuelve enbody.factura_id, junto con unpdf_linkequivalente al de este servicio.
No es el número de factura. factura_id es el identificador interno de nuby; no es el consecutivo ni el número que aparece impreso. Para encontrar el factura_id correcto, usa el listado de facturas de venta.
Ejemplos de petición
GET https://mi-inmobiliaria.nuby.app/service/v2/public/invoices/10231/pdf
GET https://mi-inmobiliaria.nuby.app/service/v2/public/invoices/10230/pdf
Encabezados de una petición completa
GET /service/v2/public/invoices/10231/pdf HTTP/1.1
Host: mi-inmobiliaria.nuby.app
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
Orden de respuesta: nuby primero revisa si la factura es de contrato (409), después si existe (404) y solo entonces genera el enlace (200). Por eso una factura de contrato nunca produce un enlace.
3. La Respuesta (¿Qué te entregaremos?)
Si el token es válido y la factura existe, el sistema devuelve un JSON con el identificador, el enlace seguro y la fecha en que el enlace deja de ser válido:
{
"status": 200,
"message": "",
"body": {
"factura_id": 10231,
"pdf_link": "https://<dominio-publico-pdf>?hash=...",
"expira_en": "2026-11-20"
},
"pagination": [],
"filters_applied": []
}
Estructura principal de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
status |
integer | Código del estado de la respuesta. 200 cuando la consulta fue exitosa. |
message |
string | Cadena vacía cuando la consulta fue exitosa. |
body |
object | Enlace y vigencia del PDF. En un error, este campo no existe. |
pagination |
array | Siempre una lista vacía []: esta consulta no pagina. |
filters_applied |
array | Siempre una lista vacía []: esta consulta no tiene filtros. |
Campos del cuerpo (body)
| Campo | Tipo | Descripción |
|---|---|---|
factura_id |
integer | Identificador de la factura consultada. Es el mismo valor que enviaste en la dirección. |
pdf_link |
string | Enlace seguro de impresión, listo para abrir en el navegador. Se sirve desde el dominio público de PDF de nuby e incluye un hash cifrado que valida el visor público. Contiene el identificador de la factura y una fecha límite; no contiene rutas internas de archivos. |
expira_en |
string | Fecha (AAAA-MM-DD) en la que el enlace deja de ser válido. Se calcula como la fecha de la consulta más 30 días. |
El enlace lo genera nuby
Tu integración no construye la dirección del PDF ni conoce la ubicación del archivo: nuby cifra el identificador de la factura y una fecha límite, y devuelve el enlace listo para abrir en el navegador. La ruta interna nunca se expone: el visor público valida el hash y sirve el documento.
No intentes armar el enlace por tu cuenta. El hash es una firma que solo genera nuby; cualquier enlace construido a mano será rechazado por el visor. Tampoco reutilices un hash de otra factura: el identificador va dentro de la firma.
Vigencia de 30 días
nuby calcula el enlace con una vigencia de 30 días contados desde la fecha de la consulta. expira_en te dice el último día en que funciona; a partir del día siguiente, el visor deja de servir el documento.
No guardes el enlace como definitivo. Si lo almacenas más de 30 días, dejará de funcionar. Guarda el factura_id y vuelve a pedir el enlace cuando lo necesites: es una llamada barata y siempre devuelve un enlace vigente.
Cómo se usa el enlace
4. Seguridad y Posibles Errores
Tu pase tiene fecha de caducidad
Por medidas de seguridad, el token que te entregamos solo dura 1 hora. Una vez transcurrido ese tiempo, el sistema te bloquea el acceso con un error 401 y tu sistema debe volver a consumir el servicio de Login para pedir un pase nuevo.
Esta respuesta la produce el middleware de autenticación heredado (el mismo de toda la API) y no el contrato de la operación. Así se ve:
{
"statusCode": 401,
"error": {
"type": "SERVER_ERROR",
"description": "JWT Token expired."
}
}
Ojo con la forma: los errores del middleware usan statusCode y error.description, no success ni error_code. El valor de type depende del manejador de errores de la versión desplegada (el libro publicado muestra SERVER_ERROR y el manejador actual responde UNAUTHENTICATED); lo estable es statusCode y la description.
| Situación | HTTP | error.description exacta |
|---|---|---|
No se envió el encabezado Authorization |
401 |
JWT Token required. |
El encabezado no tiene el formato Bearer {token} |
400 |
JWT Token not send. |
| El token no fue encontrado en el sistema | 401 |
JWT Token not found. |
| El token expiró | 401 |
JWT Token expired. |
Errores del contrato de la operación
Los errores que produce la operación misma traen success: false, el código HTTP, un message legible y un error_code estable:
{
"success": false,
"status": 404,
"message": "La factura 990 no existe.",
"error_code": "FACTURA_NO_ENCONTRADA"
}
| Código HTTP | error_code |
Cuándo ocurre y cómo resolverlo |
|---|---|---|
400 |
VALIDATION_ERROR |
El factura_id de la dirección no es un entero positivo. Mensaje: Debe indicar un factura_id entero mayor que 0 para operar sobre la factura. Corrige el identificador. |
401 |
— | No enviaste el encabezado Authorization, o el token no existe o expiró. Vuelve a consumir el servicio Login. La respuesta tiene la forma del middleware (statusCode + error.description). |
403 |
CONSUMIDOR_NO_AUTENTICADO |
El token es válido, pero no corresponde a un cliente OAuth habilitado para consumir la API. Mensaje: El cliente autenticado no está autorizado para consumir este endpoint. |
403 |
RECURSO_NO_AUTORIZADO |
Al cliente OAuth le falta el permiso catalogs:read. Mensaje: El consumidor no tiene habilitado el recurso solicitado. Solicita el ajuste a tu administrador en nuby. |
404 |
FACTURA_NO_ENCONTRADA |
No existe una factura con ese identificador. Mensaje: La factura {factura_id} no existe. Verifica el factura_id o búscalo en el listado. La respuesta no incluye warnings. |
409 |
FACTURA_CONTRATO_NO_SOPORTADA |
La factura se generó desde un contrato de arrendamiento. Incluye warnings[] con el factura_id y el alcance. No se genera ningún enlace para esas facturas. |
500 |
INTERNAL_ERROR |
Fallo técnico al leer la factura. Mensaje: No fue posible leer la factura de venta. Intente nuevamente o contacte al administrador del
sistema. Reintenta; si persiste, contacta a soporte. |
Forma exacta del rechazo 409
Una factura de contrato se rechaza con el código FACTURA_CONTRATO_NO_SOPORTADA, un mensaje que explica el motivo y una advertencia que identifica la factura y el alcance del endpoint:
{
"success": false,
"status": 409,
"message": "La factura 990 fue generada desde un contrato de arrendamiento. Esta API solo admite facturas de venta; las facturas de contrato se gestionan en el flujo de contratos de nuby.",
"error_code": "FACTURA_CONTRATO_NO_SOPORTADA",
"warnings": [
{
"code": "FACTURA_CONTRATO_NO_SOPORTADA",
"factura_id": 990,
"scope": "solo_facturas_de_venta"
}
]
}
Campo de warnings[] |
Tipo | Significado |
|---|---|---|
code |
string | Siempre FACTURA_CONTRATO_NO_SOPORTADA. Es el valor que debes evaluar en tu código. |
factura_id |
integer | Identificador de la factura rechazada. |
scope |
string | Siempre solo_facturas_de_venta: confirma que el rechazo es por alcance, no por permisos. |
El enlace es la credencial del documento. Quien tenga el enlace puede abrir el PDF durante su vigencia, así que trátalo como información sensible: no lo publiques en sitios abiertos ni lo registres en logs de acceso. Si se filtra, deja de ser válido a los 30 días sin que tengas que hacer nada.
Trata el 404 como un caso de negocio, no como una falla técnica: significa que el identificador no existe en nuby. No lo reintentes con el mismo valor.
5. Ejemplos de integración
Aquí tienes ejemplos listos para que tus desarrolladores los adapten a tu plataforma. Todos obtienen el enlace y tratan los dos rechazos de negocio más frecuentes (404 y 409).
cURL
TOKEN="TU_TOKEN_AQUI"
INSTANCIA="mi-inmobiliaria.nuby.app"
FACTURA_ID=10231
curl "https://${INSTANCIA}/service/v2/public/invoices/${FACTURA_ID}/pdf" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${TOKEN}"
PHP
<?php
$instancia = 'mi-inmobiliaria.nuby.app'; // Tu dirección web completa
$token = 'TU_TOKEN_AQUI'; // Token obtenido del servicio Login
$facturaId = 10231;
$url = "https://{$instancia}/service/v2/public/invoices/{$facturaId}/pdf";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: Bearer {$token}",
]);
$respuesta = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$info = json_decode($respuesta, true);
if ($httpCode === 200) {
$enlace = $info['body']['pdf_link'];
echo "Enlace del PDF: {$enlace}\n";
echo "Válido hasta: {$info['body']['expira_en']}\n";
// Guarda el identificador, no el enlace: el enlace vence a los 30 días.
// file_put_contents('factura_pdf.txt', $enlace);
} elseif ($httpCode === 404) {
echo "La factura no existe en nuby: {$info['message']}\n";
} elseif ($httpCode === 409) {
echo "Factura de contrato, no soportada por esta API: {$info['message']}\n";
} else {
echo "Error HTTP {$httpCode}: {$info['message']}\n";
}
Python
import requests
# 1. Configura tus datos de acceso
instancia = "mi-inmobiliaria.nuby.app" # Reemplaza con tu dirección web completa
token = "TU_TOKEN_AQUI" # Token obtenido del servicio Login
factura_id = 10231
# 2. Envía la petición
url = f"https://{instancia}/service/v2/public/invoices/{factura_id}/pdf"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
}
respuesta = requests.get(url, headers=headers, timeout=30)
# 3. Procesa la respuesta
if respuesta.status_code == 200:
body = respuesta.json()["body"]
print(f"Enlace del PDF: {body['pdf_link']}")
print(f"Válido hasta: {body['expira_en']}")
elif respuesta.status_code in (404, 409):
error = respuesta.json()
print(f"Rechazo de negocio {error['error_code']}: {error['message']}")
else:
print(f"Error HTTP {respuesta.status_code}: {respuesta.text}")
JavaScript (Fetch API)
// 1. Configura tus datos de acceso
const instancia = 'mi-inmobiliaria.nuby.app'; // Reemplaza con tu dirección web completa
const token = 'TU_TOKEN_AQUI'; // Token obtenido del servicio Login
// 2. Función que obtiene el enlace del PDF
async function obtenerEnlacePdf(facturaId) {
const url = `https://${instancia}/service/v2/public/invoices/${facturaId}/pdf`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
const info = await response.json().catch(() => ({}));
if (!response.ok) {
console.error(`Error HTTP ${response.status} ${info.error_code ?? ''}: ${info.message ?? ''}`);
return null;
}
return info.body;
}
// 3. Consume el enlace y ábrelo en una pestaña nueva
obtenerEnlacePdf(10231).then((body) => {
if (!body) {
return;
}
console.log(`Enlace: ${body.pdf_link}`);
console.log(`Válido hasta: ${body.expira_en}`);
// window.open(body.pdf_link, '_blank', 'noopener');
});
Power Query M (Excel / Power BI)
let
// 1. Configura tus datos de acceso
instancia = "mi-inmobiliaria.nuby.app", // Reemplaza con tu dirección web completa
token = "TU_TOKEN_AQUI", // Token obtenido del servicio Login
facturaId = "10231", // Identificador interno de la factura
// 2. Prepara la dirección de la petición
url = "https://" & instancia & "/service/v2/public/invoices/" & facturaId & "/pdf",
// 3. Envía la petición GET
response = Web.Contents(url, [
Headers = [
#"Content-Type" = "application/json",
#"Authorization" = "Bearer " & token
]
]),
// 4. Decodifica la respuesta JSON y toma el cuerpo
jsonResponse = Json.Document(response),
body = jsonResponse[body],
// 5. Convierte el enlace y su vigencia en una tabla de una fila
tabla = Table.FromRecords({[
FacturaID = body[factura_id],
PdfLink = body[pdf_link],
ExpiraEn = Date.FromText(body[expira_en])
]})
in
tabla
Sobre Power Query M: la columna PdfLink queda como texto; Excel y Power BI no descargan el PDF al actualizar la consulta, solo guardan el enlace. Si necesitas el archivo, abre el enlace desde el navegador mientras esté vigente.
6. Buenas prácticas y límites conocidos
Recomendaciones
- Pide el enlace justo cuando lo necesites. Así siempre está vigente y no tienes que renovarlo.
- Guarda el
factura_id, no el enlace. El identificador no expira; el enlace sí. - Revisa
expira_enantes de enviar el enlace a un tercero. Un enlace vencido se ve como un error del visor, no de tu integración. - Genera el enlace en el momento del clic si lo vas a mostrar en un portal: evitas enlaces muertos en la interfaz.
- Trata el enlace como información sensible. Quien lo tenga puede abrir el documento mientras esté vigente.
- No registres datos personales en tus logs. Para trazar la consulta te basta con el
factura_id.
Límites de este servicio
- Solo devuelve el enlace. No devuelve el archivo ni el contenido del PDF.
- No expone la ruta interna del archivo. Solo el enlace público con su hash cifrado.
- Solo facturas de venta. Las de contrato se rechazan con
409 FACTURA_CONTRATO_NO_SOPORTADAy no producen enlace. - El enlace vence a los 30 días. No hay renovación automática: se pide de nuevo.
- No permite elegir formato ni plantilla. El PDF es el comprobante que nuby genera para la factura.
- No pagina. Es una consulta de un solo documento:
paginationyfilters_appliedvienen como listas vacías. - No requiere que la factura esté emitida. Un borrador también produce enlace, pero el documento refleja su estado actual (sin CUFE mientras no se haya enviado a la DIAN).
¿Necesitas más datos que el enlace? El detalle de la factura devuelve estado, valores, CUFE y el detalle de ítems, e incluye el mismo pdf_link. Si buscas el identificador de otra factura, usa el listado de facturas de venta.