# Obtener el PDF de una Factura [BETA]

<p class="callout warning">**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.</p>

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.

<p class="callout info">**¿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:</p>

<div class="page-content" dir="auto" id="bkmrk-enviar-la-factura-al"><div dir="auto">- **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_id` y 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.

---

  </div></div>#### **1. El Endpoint (La dirección web)**

##### **Método y dirección**

<div class="page-content" dir="auto" id="bkmrk-dato-valor-m%C3%A9todo-ge"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 23%;"></col><col style="width: 77%;"></col></colgroup><thead><tr><th>Dato</th><th>Valor</th></tr></thead><tbody><tr><td>Método</td><td>`GET`</td></tr><tr><td>Recurso</td><td>`/invoices/{factura_id}/pdf`</td></tr><tr><td>Cuerpo de la petición</td><td>No se envía cuerpo</td></tr></tbody></table>

</div></div>```http
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.

<p class="callout warning">**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`.</p>

##### **Encabezados requeridos**

<div class="page-content" dir="auto" id="bkmrk-encabezado-%C2%BFobligato"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 14%;"></col><col style="width: 64%;"></col></colgroup><thead><tr><th>Encabezado</th><th>¿Obligatorio?</th><th>Valor</th></tr></thead><tbody><tr><td>`Authorization`</td><td>Sí</td><td>`Bearer <token>`, con el token obtenido en el servicio **Login**.</td></tr><tr><td>`Content-Type`</td><td>No</td><td>`application/json` si tu cliente lo envía. Como la petición no lleva cuerpo, el servicio no lo necesita.</td></tr></tbody></table>

</div></div>##### **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`.

<p class="callout info">**¿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`.</p>

<p class="callout info">Si aún no tienes un token, consulta la página [*<span class="tw-font-bold tw-text-arrBlue hover:tw-text-nubyBlue tw-cursor-pointer">Login</span>*](https://docs.nuby.ai/books/api-nuby-v2/page/login) del libro **API nuby V2**.</p>

##### **Requisitos previos de esta operación**

<div class="page-content" dir="auto" id="bkmrk-la-factura-debe-exis"><div dir="auto">- 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_SOPORTADA` y `warnings[]`.
- El cliente OAuth del token debe tener el permiso `catalogs:read`.

  </div></div>#### **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.

<div class="page-content" dir="auto" id="bkmrk-dato-valor-m%C3%A9todo-ge-1"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 78%;"></col></colgroup><thead><tr><th>Dato</th><th>Valor</th></tr></thead><tbody><tr><td>Método</td><td>`GET`</td></tr><tr><td>`Content-Type`</td><td>`application/json` (opcional: la petición no tiene cuerpo)</td></tr><tr><td>`Authorization`</td><td>`Bearer <token>`, token obtenido al consumir el servicio **Login**</td></tr></tbody></table>

</div></div>##### **Parámetros de la ruta (path)**

<div class="page-content" dir="auto" id="bkmrk-par%C3%A1metro-tipo-reque"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 8%;"></col><col style="width: 9%;"></col><col style="width: 10%;"></col><col style="width: 51%;"></col></colgroup><thead><tr><th>Parámetro</th><th>Tipo</th><th>Requerido</th><th>Por defecto</th><th>Descripción</th></tr></thead><tbody><tr><td>`factura_id`</td><td>integer</td><td>Sí</td><td>—</td><td>Identificador interno de la factura. Debe ser un entero **mayor que 0**. Va en la dirección, antes del segmento `/pdf`.</td></tr></tbody></table>

</div></div>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`:

```json
{
  "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`?**

<div class="page-content" dir="auto" id="bkmrk-de-la-creaci%C3%B3n-de-la"><div dir="auto">- **De la creación de la factura** (`POST /invoices`): la respuesta lo devuelve en `body.factura_id`.
- **Del listado** (`GET /invoices/sales/list`): cada fila del `body` lo trae en `factura_id`.
- **Del detalle** (`GET /invoices/{factura_id}`): lo devuelve en `body.factura_id`, junto con un `pdf_link` equivalente al de este servicio.

</div></div><p class="callout warning">**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**](https://docs.nuby.ai/listar-facturas-venta.html).</p>

##### **Ejemplos de petición**

```http
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**

```http
GET /service/v2/public/invoices/10231/pdf HTTP/1.1
Host: mi-inmobiliaria.nuby.app
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
```

<p class="callout info">**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.</p>

<div class="page-content" dir="auto" id="bkmrk-"><div dir="auto">  </div></div>#### **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:

```json
{
  "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**

<div class="page-content" dir="auto" id="bkmrk-campo-tipo-descripci"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 8%;"></col><col style="width: 70%;"></col></colgroup><thead><tr><th>Campo</th><th>Tipo</th><th>Descripción</th></tr></thead><tbody><tr><td>`status`</td><td>integer</td><td>Código del estado de la respuesta. `200` cuando la consulta fue exitosa.</td></tr><tr><td>`message`</td><td>string</td><td>Cadena vacía cuando la consulta fue exitosa.</td></tr><tr><td>`body`</td><td>object</td><td>Enlace y vigencia del PDF. En un error, este campo no existe.</td></tr><tr><td>`pagination`</td><td>array</td><td>Siempre una lista vacía `[]`: esta consulta no pagina.</td></tr><tr><td>`filters_applied`</td><td>array</td><td>Siempre una lista vacía `[]`: esta consulta no tiene filtros.</td></tr></tbody></table>

</div></div>##### **Campos del cuerpo (`body`)**

<div class="page-content" dir="auto" id="bkmrk-campo-tipo-descripci-1"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 8%;"></col><col style="width: 70%;"></col></colgroup><thead><tr><th>Campo</th><th>Tipo</th><th>Descripción</th></tr></thead><tbody><tr><td>`factura_id`</td><td>integer</td><td>Identificador de la factura consultada. Es el mismo valor que enviaste en la dirección.</td></tr><tr><td>`pdf_link`</td><td>string</td><td>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**.</td></tr><tr><td>`expira_en`</td><td>string</td><td>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**.</td></tr></tbody></table>

</div></div>##### **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.

<p class="callout warning">**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.</p>

##### **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.

<p class="callout warning">**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.</p>

##### **Cómo se usa el enlace**

<div class="page-content" dir="auto" id="bkmrk-en-el-navegador%3A-abr"><div dir="auto">- **En el navegador:** abre `pdf_link` tal como llega; el visor público valida el hash y muestra el PDF.
- **En un correo o un chat:** envía el enlace completo, sin recortarlo: si se pierde parte del hash, el visor lo rechaza.
- **En tu portal:** genera el enlace en el momento en que el usuario hace clic en "Ver factura", así nunca muestras un enlace vencido.

  </div></div>#### **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:

```json
{
  "statusCode": 401,
  "error": {
    "type": "SERVER_ERROR",
    "description": "JWT Token expired."
  }
}
```

<p class="callout info">**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`.</p>

<div class="page-content" dir="auto" id="bkmrk-situaci%C3%B3n-http-error"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 34%;"></col><col style="width: 14%;"></col><col style="width: 52%;"></col></colgroup><thead><tr><th>Situación</th><th>HTTP</th><th>`error.description` exacta</th></tr></thead><tbody><tr><td>No se envió el encabezado `Authorization`</td><td>`401`</td><td>`JWT Token required.`</td></tr><tr><td>El encabezado no tiene el formato `Bearer {token}`</td><td>`400`</td><td>`JWT Token not send.`</td></tr><tr><td>El token no fue encontrado en el sistema</td><td>`401`</td><td>`JWT Token not found.`</td></tr><tr><td>El token expiró</td><td>`401`</td><td>`JWT Token expired.`</td></tr></tbody></table>

</div></div>##### **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:

```json
{
  "success": false,
  "status": 404,
  "message": "La factura 990 no existe.",
  "error_code": "FACTURA_NO_ENCONTRADA"
}
```

<div class="page-content" dir="auto" id="bkmrk-c%C3%B3digo-http-error_co"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 14%;"></col><col style="width: 64%;"></col></colgroup><thead><tr><th>Código HTTP</th><th>`error_code`</th><th>Cuándo ocurre y cómo resolverlo</th></tr></thead><tbody><tr><td>`400`</td><td>`VALIDATION_ERROR`</td><td>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.</td></tr><tr><td>`401`</td><td>—</td><td>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`).</td></tr><tr><td>`403`</td><td>`CONSUMIDOR_NO_AUTENTICADO`</td><td>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.`</td></tr><tr><td>`403`</td><td>`RECURSO_NO_AUTORIZADO`</td><td>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.</td></tr><tr><td>`404`</td><td>`FACTURA_NO_ENCONTRADA`</td><td>No existe una factura con ese identificador. Mensaje: `La factura {factura_id} no existe.` Verifica el `factura_id` o búscalo en el [listado](https://docs.nuby.ai/listar-facturas-venta.html). La respuesta **no** incluye `warnings`.</td></tr><tr><td>`409`</td><td>`FACTURA_CONTRATO_NO_SOPORTADA`</td><td>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.</td></tr><tr><td>`500`</td><td>`INTERNAL_ERROR`</td><td>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.</td></tr></tbody></table>

</div></div>##### **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:

```json
{
  "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"
    }
  ]
}
```

<div class="page-content" dir="auto" id="bkmrk-campo-de-warnings%5B%5D-"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 22%;"></col><col style="width: 8%;"></col><col style="width: 70%;"></col></colgroup><thead><tr><th>Campo de `warnings[]`</th><th>Tipo</th><th>Significado</th></tr></thead><tbody><tr><td>`code`</td><td>string</td><td>Siempre `FACTURA_CONTRATO_NO_SOPORTADA`. Es el valor que debes evaluar en tu código.</td></tr><tr><td>`factura_id`</td><td>integer</td><td>Identificador de la factura rechazada.</td></tr><tr><td>`scope`</td><td>string</td><td>Siempre `solo_facturas_de_venta`: confirma que el rechazo es por alcance, no por permisos.</td></tr></tbody></table>

</div></div><p class="callout info">**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.</p>

<p class="callout warning">**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.</p>

<div class="page-content" dir="auto" id="bkmrk--1"><div dir="auto">  ---

</div></div>#### **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`).

<div class="page-content" dir="auto" id="bkmrk-curl-token%3D%22tu_token"><div dir="auto"><details><summary>cURL</summary>

```bash
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}"
```

</details><details><summary>PHP</summary>

```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";
}
```

</details><details><summary>Python</summary>

```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}")
```

</details><details><summary>JavaScript (Fetch API)</summary>

```javascript
// 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');
});
```

</details><details><summary>Power Query M (Excel / Power BI)</summary>

```m
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
```

</details></div></div><p class="callout info">**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.</p>

<div class="page-content" dir="auto" id="bkmrk--2"><div dir="auto">  </div></div>#### **6. Buenas prácticas y límites conocidos**

##### **Recomendaciones**

<div class="page-content" dir="auto" id="bkmrk-pide-el-enlace-justo"><div dir="auto">- **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_en` antes 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`.

</div></div>##### **Límites de este servicio**

<div class="page-content" dir="auto" id="bkmrk-solo-devuelve-el-enl"><div dir="auto">- **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_SOPORTADA` y 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: `pagination` y `filters_applied` vienen 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).

</div></div><p class="callout info">**¿Necesitas más datos que el enlace?** El [**detalle de la factura**](https://docs.nuby.ai/ver-detalle-factura.html) 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**](https://docs.nuby.ai/listar-facturas-venta.html).</p>