# Editar Factura en Borrador [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 **modifica una factura de venta que todavía está en borrador** en nuby. Puedes cambiar la cabecera y los ítems; nuby vuelve a calcular los totales, el saldo y la contabilidad del documento. La factura sigue en estado `5` (Borrador) y sigue **sin numerar**: la edición no consume consecutivo.

Solo puedes editar **tus** borradores: los que creó tu consumidor por API. Una factura generada desde un contrato de arrendamiento, una factura ya emitida o un borrador creado por la interfaz de nuby quedan fuera del alcance de este servicio. La emisión es un paso aparte: editar no envía nada a la DIAN.

<p class="callout info">**¿Para qué sirve este servicio?**  
Sirve para **corregir un borrador antes de emitirlo**. La creación y la emisión son actos separados justamente para que exista este paso: entre una y otra puedes ajustar el documento tantas veces como necesites, sin gastar numeración y sin enviar nada a la DIAN.</p>

##### **Casos de uso concretos**

<div class="page-content" dir="auto" id="bkmrk-revisi%C3%B3n-humana-ante"><div dir="auto">- **Revisión humana antes de emitir.** Creas el borrador por API, un contador lo revisa en la interfaz de nuby y tu sistema aplica el ajuste final con este servicio.
- **Corregir precios, cantidades o impuestos.** Si el pedido cambió después de crear el borrador, reenvías los ítems completos y nuby recalcula totales y saldo.
- **Cambiar la fecha o el plazo.** Ajustas `fecha`, `dias_vencimiento` o `fecha_vencimiento` para que coincidan con la operación real.
- **Cambiar el tercero.** Si facturaste a la persona equivocada, envías otro `tercero_id` —o el objeto `tercero`— y nuby vuelve a escribir el documento.
- **Completar datos comerciales.** Agregas `observaciones`, `vendedor_id`, `centro_id` o las `formas_de_pago` que faltaban.
- **Aplicar anticipos.** El campo `anticipos[]` también se acepta en la edición, así que puedes aplicar un anticipo que no estaba disponible al crear el borrador.

</div></div>##### **Resumen del servicio**

<div class="page-content" dir="auto" id="bkmrk-aspecto-valor-m%C3%A9todo"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 30%;"></col><col style="width: 70%;"></col></colgroup><thead><tr><th>Aspecto</th><th>Valor</th></tr></thead><tbody><tr><td>Método y ruta</td><td>`PUT|PATCH /service/v2/public/invoices/{factura_id}`</td></tr><tr><td>Permiso (scope) requerido</td><td>`invoices:update`</td></tr><tr><td>Respuesta exitosa</td><td>`200` con `factura_id`, `estado_id`, `consecutivo`, `valor_total` y `valor_saldo`</td></tr><tr><td>Efecto del documento</td><td>Sigue en estado `5` (Borrador), sin numerar, con totales, saldo y contabilidad recalculados</td></tr><tr><td>Protección contra duplicados</td><td>Idempotencia por `referencia_externa` + consumidor + factura + contenido</td></tr><tr><td>¿Envía a la DIAN?</td><td>**No**. El envío es la operación de emisión</td></tr></tbody></table>

</div></div>##### **Lo que este servicio NO hace**

<div class="page-content" dir="auto" id="bkmrk-no-numera-ni-emite.-"><div dir="auto">- **No numera ni emite.** El consecutivo real y el envío a la DIAN son de la operación de emisión.
- **No acepta cambios parciales.** `PUT` y `PATCH` comparten comportamiento: el payload describe el documento completo y nuby lo vuelve a grabar. Un campo que omitas no se conserva "por defecto": se valida como faltante.
- **No cambia el documento de destino.** El `factura_id` del path manda: un `factura_id` en el cuerpo nunca decide qué factura se edita.
- **No toca facturas fuera de borrador.** Una factura emitida responde `409 FACTURA_NO_EDITABLE`.
- **No toca facturas de contrato.** El alcance se evalúa primero, sin importar quién la creó.
- **No revalida el período contable ni la facturación electrónica.** Ambas cosas se validaron al crear el borrador.
- **No permite editar la factura de otro consumidor.** La pertenencia se evalúa antes del estado.

</div></div>##### **Dónde encaja en el ciclo**

```text
1. Crear borrador         POST /invoices                              (guía anterior)
2. Editar borrador        PUT|PATCH /invoices/{factura_id}            (esta página)
3. Emitir a la DIAN       POST /invoices/{factura_id}/issue
4. Consultar detalle      GET /invoices/{factura_id}
5. Obtener el PDF         GET /invoices/{factura_id}/pdf
```

<p class="callout info">Si todavía no tienes un borrador, créalo primero con [**Crear Factura en Borrador**](https://docs.nuby.ai/crear-factura-borrador.html). El `factura_id` que devuelve esa operación es el que va en el path de esta.</p>

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

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

Apunta tu sistema a la siguiente dirección. Puedes usar **PUT** o **PATCH**: las dos llegan al mismo servicio y tienen la misma semántica (documento completo).

```http
PUT   https://{{instancia}}/service/v2/public/invoices/{factura_id}
PATCH https://{{instancia}}/service/v2/public/invoices/{factura_id}
```

##### **¿Qué debo colocar en `{{instancia}}`?**

La **dirección web principal** que utilizas a diario para ingresar a tu plataforma, incluyendo la terminación `.nuby.app` o `.arrendasoft.co`. Por ejemplo `inmobiliaria.nuby.app`.

<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/{factura_id}`.</p>

**¿Qué debes colocar en `{factura_id}`?** El identificador interno que devolvió la creación (campo `body.factura_id`). Debe ser un **entero mayor que 0**. Si no lo es, la respuesta es `400` con *Debe indicar un factura\_id entero mayor que 0 para operar sobre la factura.* **No** construyas ese valor a partir del consecutivo: en un borrador el consecutivo es un marcador sin numerar.

##### **Encabezados requeridos**

<div class="page-content" dir="auto" id="bkmrk-encabezado-valor-obl"><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>Valor</th><th>Obligatorio</th></tr></thead><tbody><tr><td>`Content-Type`</td><td>`application/json`</td><td>Sí</td></tr><tr><td>`Authorization`</td><td>`Bearer <token>`, con el token obtenido en el servicio **Login**</td><td>Sí</td></tr></tbody></table>

</div></div>##### **Permiso requerido (scope)**

El cliente OAuth con el que se genera el token debe tener habilitado el permiso `invoices:update`. Ojo: es distinto del permiso de creación (`invoices:create`). Un cliente con permiso para crear no necesariamente puede editar.

<div class="page-content" dir="auto" id="bkmrk-403-consumidor_no_au"><div dir="auto">- `403 CONSUMIDOR_NO_AUTENTICADO` → el token es válido pero no corresponde a un cliente OAuth habilitado. Mensaje: *El cliente autenticado no está autorizado para consumir este endpoint.*
- `403 RECURSO_NO_AUTORIZADO` → al cliente le falta `invoices:update`. Mensaje: *El consumidor no tiene habilitado el recurso solicitado.*

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

##### **Precondiciones del documento**

<div class="page-content" dir="auto" id="bkmrk-precondici%C3%B3n-qu%C3%A9-pas"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 34%;"></col><col style="width: 66%;"></col></colgroup><thead><tr><th>Precondición</th><th>Qué pasa si no se cumple</th></tr></thead><tbody><tr><td>La factura es de **venta** (no nació de un contrato de arrendamiento).</td><td>`409 FACTURA_CONTRATO_NO_SOPORTADA` con `warnings[]`. No se evalúa el estado ni la pertenencia.</td></tr><tr><td>La factura la creó **tu consumidor** por API.</td><td>`403 RECURSO_AJENO`.</td></tr><tr><td>La factura está en **borrador** (`estado_id = 5`).</td><td>`409 FACTURA_NO_EDITABLE` con el estado actual en el mensaje.</td></tr><tr><td>El payload cumple el contrato de entrada (mismos campos obligatorios que la creación).</td><td>`422 VALIDATION_ERROR` con `errors` por campo.</td></tr></tbody></table>

</div></div><p class="callout info">**La edición no revalida la configuración del sistema.** Ni `ENABLE_FACT_ELECT` ni el período contable de la fecha se vuelven a evaluar aquí: se validaron al crear el borrador y la fecha del documento no cambió de período por editarse.</p>

<div class="page-content" dir="auto" id="bkmrk--1"><div dir="auto">  </div></div>#### **2. La Petición (¿Qué debes enviarnos?)**

El cuerpo es un **objeto JSON** con el mismo contrato de entrada de la creación: la referencia de idempotencia de **esta edición** y, al mismo nivel, la cabecera y los ítems del documento.

<div class="page-content" dir="auto" id="bkmrk-elemento-valor-m%C3%A9tod"><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>Elemento</th><th>Valor</th></tr></thead><tbody><tr><td>Método</td><td>`PUT` o `PATCH` (equivalen)</td></tr><tr><td>`Content-Type`</td><td>`application/json`</td></tr><tr><td>`Authorization`</td><td>`Bearer <token>`</td></tr><tr><td>Cuerpo</td><td>Objeto JSON con la cabecera y el arreglo `items`</td></tr></tbody></table>

</div></div>##### **Cabecera de la factura**

<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: 12%;"></col><col style="width: 10%;"></col><col style="width: 48%;"></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>`referencia_externa`</td><td>texto</td><td>Sí</td><td>—</td><td>Identificador único de **esta edición**. Si falta: `422` con *El campo "referencia\_externa" es obligatorio.* No reutilices la referencia de la creación: esa ya quedó consumida por otra operación.</td></tr><tr><td>`tercero_id`</td><td>entero</td><td>Sí, o el objeto `tercero`</td><td>—</td><td>Tercero de la factura. Si envías `tercero_id` y `tercero`, gana `tercero_id`.</td></tr><tr><td>`tercero`</td><td>objeto</td><td>Sí, o `tercero_id`</td><td>—</td><td>Datos del tercero para resolverlo o crearlo en la misma operación. Sin ninguno de los dos: *Debe indicar tercero\_id o el objeto tercero.*</td></tr><tr><td>`fecha`</td><td>texto `AAAA-MM-DD`</td><td>Sí</td><td>—</td><td>Fecha de la factura. **Debes enviarla aunque no la cambies**: el payload es el estado completo del documento. Formato inválido: *La fecha de la factura es obligatoria y debe tener el formato AAAA-MM-DD.*</td></tr><tr><td>`dias_vencimiento`</td><td>entero</td><td>Sí, salvo que uses `fecha_vencimiento` o `pago_total: true`</td><td>—</td><td>Entero **mayor o igual a 1**. Con `pago_total: true` se ignora.</td></tr><tr><td>`fecha_vencimiento`</td><td>texto `AAAA-MM-DD`</td><td>No</td><td>—</td><td>Alternativa a `dias_vencimiento`: nuby deriva los días. Si envías ambas y se contradicen: *La fecha de vencimiento no coincide con los días de vencimiento enviados.*</td></tr><tr><td>`resolucion_id`</td><td>entero</td><td>No</td><td>la resolución que ya tiene la factura</td><td>Si lo omites, nuby **conserva** la resolución del documento. Si lo envías, debe existir, estar activa y ser de venta. Cambiarla reescribe la resolución del documento: hazlo solo si sabes lo que implica.</td></tr><tr><td>`moneda`</td><td>texto</td><td>No</td><td>`COP`</td><td>Moneda del documento.</td></tr><tr><td>`tasa_cambio`</td><td>numérico</td><td>No</td><td>`null`</td><td>Tasa de cambio aplicable. Solo tiene sentido con moneda extranjera.</td></tr><tr><td>`vendedor_id`</td><td>entero</td><td>No</td><td>`null`</td><td>Usuario de nuby que figura como vendedor.</td></tr><tr><td>`centro_id`</td><td>entero</td><td>No</td><td>`null`</td><td>Centro de costo del documento.</td></tr><tr><td>`observaciones`</td><td>texto</td><td>No</td><td>`null`</td><td>Nota libre del documento.</td></tr><tr><td>`pago_total`</td><td>booleano</td><td>No</td><td>`false`</td><td>Si es verdadero, la factura queda de contado y `dias_vencimiento` se ignora.</td></tr><tr><td>`formas_de_pago`</td><td>arreglo de objetos</td><td>Sí, cuando `pago_total` es `true`</td><td>`[]`</td><td>Con `pago_total: true` debes enviar al menos una: *Debe agregar al menos una forma de pago cuando pago\_total es verdadero.*</td></tr><tr><td>`anticipos[]`</td><td>arreglo</td><td>No</td><td>`[]`</td><td>Anticipos a aplicar. Acepta objetos (`[{"documento_id": 5}]`) o una lista plana (`[5]`).</td></tr><tr><td>`items`</td><td>arreglo de objetos</td><td>Sí</td><td>—</td><td>Líneas del documento. Al menos una: *Debe enviar al menos un ítem en items.* nuby **reemplaza** el detalle con lo que envíes.</td></tr></tbody></table>

</div></div>##### **Ítems (`items[]`)**

La edición reutiliza el mismo contrato de ítem de la creación: `producto_id`, `cantidad` y `valor_unitario` son obligatorios y se validan todos antes de tocar el documento; el resto de campos (descuentos, impuestos, retenciones, `descripcion`, `descuentos_id`, `otros_impuestos`, `tercero_aplica_id`, `valor_unitario_ori`) son opcionales. Si un ítem es inválido, la factura **no se modifica**.

<div class="page-content" dir="auto" id="bkmrk-producto_id-%E2%86%92-entero"><div dir="auto">- `producto_id` → entero mayor que 0. *producto\_id debe ser un entero mayor que 0.*
- `cantidad` → entero mayor que 0. *cantidad debe ser un entero mayor que 0.*
- `valor_unitario` → numérico y distinto de cero. *valor\_unitario debe ser numérico y distinto de 0.*

</div></div><p class="callout warning">**El detalle se reemplaza, no se mezcla.** Envía todas las líneas que debe tener el documento. Una línea que no envíes deja de existir en la factura, y los totales se recalculan con las líneas enviadas.</p>

##### **El `factura_id` del path manda**

Si tu cuerpo trae un `factura_id`, nuby lo descarta: la factura que se edita es la del path. Es una protección deliberada contra errores de tu integración, porque un identificador equivocado en el cuerpo cambiaría el documento equivocado.

```json
{
  "referencia_externa": "INV-2026-0001-EDIT-1",
  "tercero_id": 4821,
  "fecha": "2026-10-21",
  "dias_vencimiento": 15,
  "items": [
    { "producto_id": 340, "descripcion": "Administración octubre 2026", "cantidad": 2, "valor_unitario": 1500 }
  ]
}
```

Enviado a `PUT /invoices/10231`, edita la factura `10231` y la respuesta también dice `factura_id: 10231`.

##### **Campos que siguen prohibidos**

La edición valida los mismos guardas que la creación, en este orden:

<div class="page-content" dir="auto" id="bkmrk-grupo-campos-respues"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 31%;"></col><col style="width: 14%;"></col><col style="width: 55%;"></col></colgroup><thead><tr><th>Grupo</th><th>Campos</th><th>Respuesta</th></tr></thead><tbody><tr><td>Datos de contrato o movimiento</td><td>`movimiento_id`, `contrato_id`, `procesado`, `documento_respaldo_id`, `inmobiliaria`</td><td>`422 FACTURA_CONTRATO_NO_PERMITIDA` con `warnings[]`. Enviarlos en `null`, `""` o `0` no es un error.</td></tr><tr><td>Campos protegidos</td><td>`estado_id`, `consecutivo`, `cufe`, `dian_id`, `estado_dian`, `creado_por`, `tipo_factura`</td><td>`422 CAMPO_NO_PERMITIDO`. Se evalúa la **presencia** de la clave: enviarla en `null` también se rechaza.</td></tr></tbody></table>

</div></div>```json
{
  "success": false,
  "status": 422,
  "message": "El payload intenta fijar el campo estado_id, que solo el sistema puede asignar en una factura de venta.",
  "error_code": "CAMPO_NO_PERMITIDO",
  "errors": {
    "estado_id": [
      "El campo estado_id solo lo puede asignar el sistema."
    ]
  }
}
```

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

##### **Cambiar plazo y observaciones (documento completo)**

```http
PUT /service/v2/public/invoices/10231
Authorization: Bearer <token>
Content-Type: application/json
```

```json
{
  "referencia_externa": "INV-2026-0001-EDIT-1",
  "tercero_id": 4821,
  "fecha": "2026-10-21",
  "dias_vencimiento": 15,
  "moneda": "COP",
  "observaciones": "Administración octubre 2026 (ajustada)",
  "pago_total": false,
  "items": [
    {
      "producto_id": 340,
      "descripcion": "Administración octubre 2026",
      "cantidad": 1,
      "valor_unitario": 1200000,
      "por_iva": 19,
      "iva": 228000
    }
  ]
}
```

##### **Reemplazar el detalle con dos líneas (PATCH)**

```json
{
  "referencia_externa": "INV-2026-0001-EDIT-2",
  "tercero_id": 4821,
  "fecha": "2026-10-22",
  "fecha_vencimiento": "2026-11-21",
  "items": [
    { "producto_id": 340, "descripcion": "Administración octubre 2026", "cantidad": 1, "valor_unitario": 1000000 },
    { "producto_id": 512, "descripcion": "Servicio de aseo octubre 2026", "cantidad": 2, "valor_unitario": 150000 }
  ]
}
```

##### **Pasar el documento a contado**

```json
{
  "referencia_externa": "INV-2026-0001-EDIT-3",
  "tercero_id": 4821,
  "fecha": "2026-10-21",
  "pago_total": true,
  "formas_de_pago": [
    { "tipo_pago_id": 1, "valor": 1190000 }
  ],
  "items": [
    { "producto_id": 340, "cantidad": 1, "valor_unitario": 1000000, "por_iva": 19, "iva": 190000 }
  ]
}
```

<div class="page-content" dir="auto" id="bkmrk--2"><div dir="auto">  </div></div>#### **3. La Respuesta (¿Qué te entregaremos?)**

Si la edición es válida, nuby regraba el documento, recalcula totales y saldo, vuelve a escribir la contabilidad y responde `200`:

```json
{
  "success": true,
  "status": 200,
  "message": "La factura de venta se actualizó correctamente.",
  "body": {
    "factura_id": 10231,
    "estado_id": 5,
    "consecutivo": -1,
    "prefijo": "FE",
    "valor_total": 3570,
    "valor_saldo": 3570
  },
  "idempotent_replay": false
}
```

##### **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>`success`</td><td>booleano</td><td>`true` cuando la edición se aplicó.</td></tr><tr><td>`status`</td><td>entero</td><td>Código HTTP replicado en el cuerpo. Aquí siempre `200`.</td></tr><tr><td>`message`</td><td>texto</td><td>*La factura de venta se actualizó correctamente.*</td></tr><tr><td>`body`</td><td>objeto</td><td>Estado del documento después de la edición (tabla siguiente).</td></tr><tr><td>`idempotent_replay`</td><td>booleano</td><td>`false` en una edición nueva; `true` cuando la respuesta se recuperó de una solicitud anterior. En un replay la marca también aparece dentro de `body`.</td></tr></tbody></table>

</div></div>##### **Campos de `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>entero</td><td>Identificador de la factura editada: siempre el del path.</td></tr><tr><td>`estado_id`</td><td>entero</td><td>Estado del documento después de editar. Aquí `5` (Borrador).</td></tr><tr><td>`consecutivo`</td><td>entero</td><td>Marcador de "sin numerar" (cero o negativo). La edición no consume consecutivo real.</td></tr><tr><td>`valor_total`</td><td>numérico</td><td>Total recalculado por nuby con los ítems que acabas de enviar.</td></tr><tr><td>`valor_saldo`</td><td>numérico</td><td>Saldo recalculado. Es el valor que se tendrá en cuenta al emitir.</td></tr></tbody></table>

</div></div><p class="callout warning">**No des por hecho el valor de `consecutivo`.** Compara siempre contra `estado_id` y contra la presencia de `cufe`: mientras `estado_id = 5` el documento no está numerado y su `consecutivo` es un marcador interno que no debes mostrar al cliente final.</p>

##### **Qué se recalcula exactamente**

<div class="page-content" dir="auto" id="bkmrk-detalle%3A-se-reemplaz"><div dir="auto">- **Detalle:** se reemplaza con los `items` enviados.
- **Totales y saldo:** nuby recalcula `valor_total` y `valor_saldo` con los impuestos, descuentos y retenciones del detalle.
- **Contabilidad:** el documento contable se vuelve a escribir con el nuevo detalle.
- **Resolución:** se conserva la que tenía la factura cuando no envías `resolucion_id`.

  </div></div>#### **4. Idempotencia y reintentos**

##### **Reintento de una edición ya completada (replay)**

Si repites la petición con la **misma** `referencia_externa`, la misma factura y el mismo contenido, nuby devuelve la respuesta guardada con `idempotent_replay: true`. No se vuelve a escribir el documento:

```json
{
  "success": true,
  "status": 200,
  "message": "La factura de venta se actualizó correctamente.",
  "body": {
    "factura_id": 10231,
    "estado_id": 5,
    "consecutivo": -1,
    "prefijo": "FE",
    "valor_total": 3570,
    "valor_saldo": 3570
  },
  "idempotent_replay": true
}
```

La clave de idempotencia incluye el `factura_id`: la misma referencia aplicada a **otra** factura responde `409 IDEMPOTENCY_PAYLOAD_MISMATCH`, no un replay. Y la misma referencia con otro contenido también es `409`: *La referencia externa ya fue usada con un payload distinto.*

##### **Reglas de reintento**

<div class="page-content" dir="auto" id="bkmrk-situaci%C3%B3n-qu%C3%A9-hace-n"><div dir="auto"><table style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 25%;"></col><col style="width: 14%;"></col><col style="width: 61%;"></col></colgroup><thead><tr><th>Situación</th><th>Qué hace nuby con la referencia</th><th>Qué debes hacer</th></tr></thead><tbody><tr><td>Rechazo `422` de negocio</td><td>**Libera** la referencia</td><td>Corrige el payload y reintenta con la **misma** referencia.</td></tr><tr><td>`409 FACTURA_NO_EDITABLE`</td><td>No consume la referencia</td><td>No insistas: la factura ya no es editable. Consulta su estado con [**Ver Detalle de una Factura**](https://docs.nuby.ai/ver-detalle-factura.html).</td></tr><tr><td>Fallo `500`</td><td>Deja la solicitud **fallida**</td><td>Reintenta con la misma referencia.</td></tr><tr><td>Éxito `200`</td><td>Deja la solicitud **completada**</td><td>Una edición nueva necesita una referencia nueva.</td></tr></tbody></table>

  </div></div>#### **5. 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 pase expira y el sistema bloquea el acceso. Cuando esto ocurra, tu sistema simplemente debe volver a consumir el servicio de **Login** para pedir un pase nuevo y reintentar con la misma `referencia_externa`.

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

<p class="callout info">**Ese `401` lo emite el middleware de autenticación**, no el servicio de facturas, así que su forma es distinta: no trae `success` ni `error_code`.</p>

##### **Orden de evaluación: es contrato, no detalle interno**

nuby evalúa en este orden exacto. Importa porque determina **qué** error recibes cuando hay varios problemas a la vez:

<div class="page-content" dir="auto" id="bkmrk-%23-qu%C3%A9-se-eval%C3%BAa-rech"><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>\#</th><th>Qué se evalúa</th><th>Rechazo</th></tr></thead><tbody><tr><td>1</td><td>Scope del consumidor</td><td>`403 RECURSO_NO_AUTORIZADO` / `403 CONSUMIDOR_NO_AUTENTICADO`</td></tr><tr><td>2</td><td>El `factura_id` del path es un entero positivo</td><td>`400 VALIDATION_ERROR`</td></tr><tr><td>3</td><td>El cuerpo es un objeto JSON</td><td>`400 VALIDATION_ERROR`</td></tr><tr><td>4</td><td>**Alcance**: ¿es una factura de contrato?</td><td>`409 FACTURA_CONTRATO_NO_SOPORTADA`</td></tr><tr><td>5</td><td>**Pertenencia**: ¿la creó tu consumidor?</td><td>`403 RECURSO_AJENO`</td></tr><tr><td>6</td><td>`referencia_externa` presente</td><td>`422 VALIDATION_ERROR`</td></tr><tr><td>7</td><td>Idempotencia de la solicitud</td><td>`409 IDEMPOTENCY_IN_PROGRESS` / `409 IDEMPOTENCY_PAYLOAD_MISMATCH`</td></tr><tr><td>8</td><td>**Estado**: la factura sigue en borrador</td><td>`409 FACTURA_NO_EDITABLE`</td></tr><tr><td>9</td><td>Validación del payload</td><td>`422` (`VALIDATION_ERROR`, `CAMPO_NO_PERMITIDO`, `FACTURA_CONTRATO_NO_PERMITIDA`, …)</td></tr></tbody></table>

</div></div><p class="callout warning">**El alcance gana sobre la pertenencia y sobre el estado.** Una factura de contrato responde `409 FACTURA_CONTRATO_NO_SOPORTADA` aunque además sea de otro consumidor o esté fuera de borrador: nuby no evalúa nada más. Y el estado gana sobre la validación: una factura ya emitida responde `409` aunque tu payload también tenga errores.</p>

##### **Tabla de códigos HTTP, causas y mensajes**

<div class="page-content" dir="auto" id="bkmrk-http-error_code-caus"><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>HTTP</th><th>`error_code`</th><th>Causa y mensaje exacto</th></tr></thead><tbody><tr><td>`400`</td><td>`VALIDATION_ERROR`</td><td>El `factura_id` del path no es un entero positivo. *Debe indicar un factura\_id entero mayor que 0 para operar sobre la factura.*</td></tr><tr><td>`400`</td><td>`VALIDATION_ERROR`</td><td>El cuerpo no es un objeto JSON. *El cuerpo de la solicitud debe ser un objeto JSON.*</td></tr><tr><td>`400`</td><td>—</td><td>El encabezado `Authorization` no tiene el formato `Bearer {token}`. *JWT Token not send.*</td></tr><tr><td>`401`</td><td>—</td><td>Falta el encabezado `Authorization`: *JWT Token required.*</td></tr><tr><td>`401`</td><td>—</td><td>El token no existe: *JWT Token not found.*</td></tr><tr><td>`401`</td><td>—</td><td>El token expiró: *JWT Token expired.* Vuelve a consumir **Login**.</td></tr><tr><td>`403`</td><td>`CONSUMIDOR_NO_AUTENTICADO`</td><td>El token no corresponde a un cliente OAuth habilitado. *El cliente autenticado no está autorizado para consumir este endpoint.*</td></tr><tr><td>`403`</td><td>`RECURSO_NO_AUTORIZADO`</td><td>Al cliente le falta `invoices:update`. *El consumidor no tiene habilitado el recurso solicitado.*</td></tr><tr><td>`403`</td><td>`RECURSO_AJENO`</td><td>La factura la creó la interfaz de nuby u otro consumidor. *La factura indicada no pertenece al consumidor autenticado.*</td></tr><tr><td>`404`</td><td>`FACTURA_NO_ENCONTRADA`</td><td>nuby tiene registrada la pertenencia de ese identificador pero la factura ya no existe. *La factura {factura\_id} no existe.*</td></tr><tr><td>`409`</td><td>`FACTURA_CONTRATO_NO_SOPORTADA`</td><td>La factura nació de un contrato de arrendamiento. Incluye `warnings[]` con `factura_id`. Mensaje: *La factura {factura\_id} 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.*</td></tr><tr><td>`409`</td><td>`FACTURA_NO_EDITABLE`</td><td>La factura no está en borrador. Mensaje: *La factura {factura\_id} no está en borrador (estado {estado\_id}). Solo se pueden editar facturas en estado borrador.*</td></tr><tr><td>`409`</td><td>`IDEMPOTENCY_IN_PROGRESS`</td><td>Hay una solicitud en curso con esa referencia. *La solicitud con esta referencia externa está en proceso.*</td></tr><tr><td>`409`</td><td>`IDEMPOTENCY_PAYLOAD_MISMATCH`</td><td>La referencia ya se usó con otro contenido o sobre otra factura. *La referencia externa ya fue usada con un payload distinto.*</td></tr><tr><td>`422`</td><td>`VALIDATION_ERROR`</td><td>Falta `referencia_externa` (*El campo "referencia\_externa" es obligatorio.*) o el payload no cumple el formato (*El payload no es válido.*). Trae `errors` por campo.</td></tr><tr><td>`422`</td><td>`FACTURA_CONTRATO_NO_PERMITIDA`</td><td>El payload trae datos de contrato con valor. Incluye `warnings[]`. Mensaje: *El payload intenta crear o modificar una factura con datos de contrato ({campo}). Esta API solo crea facturas de venta; las facturas de contrato se generan desde el flujo de contratos.*</td></tr><tr><td>`422`</td><td>`CAMPO_NO_PERMITIDO`</td><td>El payload intenta fijar un campo del sistema. *El payload intenta fijar el campo {campo}, que solo el sistema puede asignar en una factura de venta.*</td></tr><tr><td>`422`</td><td>`RESOLUCION_CONTRATO_NO_PERMITIDA`</td><td>El `resolucion_id` enviado es de contrato. *La resolución {resolucion\_id} se usa para facturas de contrato. Esta API solo admite resoluciones de facturación de venta.*</td></tr><tr><td>`422`</td><td>`RESOLUCION_NO_DISPONIBLE`</td><td>La resolución enviada no existe o no está activa para facturación. *No hay una resolución de facturación de venta activa disponible para crear la factura.*</td></tr><tr><td>`422`</td><td>`DATOS_FACTURA_INVALIDOS`</td><td>El motor de facturación rechazó el documento. El `message` trae la causa concreta (por ejemplo, datos fiscales incompletos del tercero).</td></tr><tr><td>`500`</td><td>`INTERNAL_ERROR`</td><td>Fallo técnico, con mensaje genérico a propósito: el detalle queda en el log de nuby.</td></tr><tr><td>`500`</td><td>`IDEMPOTENCY_STORAGE_ERROR`</td><td>No se pudo registrar la solicitud para controlar duplicados. *No se pudo registrar la solicitud de idempotencia.*</td></tr></tbody></table>

</div></div>##### **Forma exacta de `warnings[]` al rechazar una factura de contrato**

Es un arreglo con un solo elemento, y su forma es fija: `code`, `factura_id` y `scope`.

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

Y esta es la forma de la advertencia cuando el **payload** trae datos de contrato (sin `factura_id`, porque todavía no hay documento):

```json
{
  "success": false,
  "status": 422,
  "message": "El payload intenta crear o modificar una factura con datos de contrato (movimiento_id). Esta API solo crea facturas de venta; las facturas de contrato se generan desde el flujo de contratos.",
  "error_code": "FACTURA_CONTRATO_NO_PERMITIDA",
  "errors": {
    "movimiento_id": [
      "El payload no puede incluir el campo movimiento_id en una factura de venta."
    ]
  },
  "warnings": [
    { "code": "FACTURA_CONTRATO_NO_PERMITIDA", "scope": "solo_facturas_de_venta" }
  ]
}
```

<p class="callout info">**Un `factura_id` que no existe suele responder `403 RECURSO_AJENO`.** La pertenencia se evalúa antes de la existencia: si nuby no tiene registrado que tu consumidor creó ese identificador, no puede distinguir "no es tuya" de "no existe" y responde `403`. El `404 FACTURA_NO_ENCONTRADA` aparece cuando la pertenencia sí está registrada pero la factura ya no existe.</p>

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

</div></div>#### **6. Ejemplos de integración**

Cada ejemplo envía el documento completo y usa una `referencia_externa` nueva por edición.

<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 -X PUT "https://${INSTANCIA}/service/v2/public/invoices/${FACTURA_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "referencia_externa": "INV-2026-0001-EDIT-1",
    "tercero_id": 4821,
    "fecha": "2026-10-21",
    "dias_vencimiento": 15,
    "moneda": "COP",
    "observaciones": "Administración octubre 2026 (ajustada)",
    "pago_total": false,
    "items": [
      {
        "producto_id": 340,
        "descripcion": "Administración octubre 2026",
        "cantidad": 1,
        "valor_unitario": 1200000,
        "por_iva": 19,
        "iva": 228000
      }
    ]
  }'
```

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

```php
<?php

$instancia = 'mi-inmobiliaria.nuby.app'; // Reemplaza con tu dirección web completa
$token = 'TU_TOKEN_AQUI';                // Token obtenido del servicio Login
$facturaId = 10231;                      // factura_id devuelto al crear el borrador

$url = "https://{$instancia}/service/v2/public/invoices/{$facturaId}";

$payload = [
    'referencia_externa' => 'INV-2026-0001-EDIT-1',
    'tercero_id' => 4821,
    'fecha' => '2026-10-21',
    'dias_vencimiento' => 15,
    'moneda' => 'COP',
    'observaciones' => 'Administración octubre 2026 (ajustada)',
    'pago_total' => false,
    'items' => [
        [
            'producto_id' => 340,
            'descripcion' => 'Administración octubre 2026',
            'cantidad' => 1,
            'valor_unitario' => 1200000,
            'por_iva' => 19,
            'iva' => 228000,
        ],
    ],
];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    "Authorization: Bearer {$token}",
]);

$respuesta = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if (curl_errno($ch)) {
    echo 'Error de conexión: ' . curl_error($ch); // reintenta con la MISMA referencia
} else {
    $info = json_decode($respuesta, true);

    if ($httpCode === 200) {
        echo "Borrador actualizado: factura_id={$info['body']['factura_id']}"
            . " total={$info['body']['valor_total']}"
            . " saldo={$info['body']['valor_saldo']}\n";

        if (!empty($info['idempotent_replay'])) {
            echo "Nota: fue un reintento idempotente, no se regrabó el documento.\n";
        }
    } else {
        echo "Rechazo HTTP {$httpCode}: {$info['message']}\n";
        echo "error_code: " . ($info['error_code'] ?? 'N/A') . "\n";

        if (!empty($info['warnings'])) {
            print_r($info['warnings']);
        }
    }
}

curl_close($ch);

?>
```

</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                      # factura_id devuelto al crear el borrador

# 2. Prepara la dirección y el cuerpo
url = f"https://{instancia}/service/v2/public/invoices/{factura_id}"

headers = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {token}"
}

payload = {
    "referencia_externa": "INV-2026-0001-EDIT-1",
    "tercero_id": 4821,
    "fecha": "2026-10-21",
    "dias_vencimiento": 15,
    "moneda": "COP",
    "observaciones": "Administración octubre 2026 (ajustada)",
    "pago_total": False,
    "items": [
        {
            "producto_id": 340,
            "descripcion": "Administración octubre 2026",
            "cantidad": 1,
            "valor_unitario": 1200000,
            "por_iva": 19,
            "iva": 228000
        }
    ]
}

# 3. Envía la petición y procesa la respuesta
try:
    response = requests.put(url, json=payload, headers=headers, timeout=30)

    print(f"Código de estado HTTP: {response.status_code}")

    if response.status_code == 200:
        info = response.json()
        body = info.get("body", {})

        print(f"Borrador actualizado: factura_id={body.get('factura_id')}")
        print(f"Total: {body.get('valor_total')} | Saldo: {body.get('valor_saldo')}")

        if info.get("idempotent_replay"):
            print("Nota: reintento idempotente, no se regrabó el documento.")
    else:
        info = response.json()
        print(f"Mensaje: {info.get('message')}")
        print(f"error_code: {info.get('error_code')}")

        for aviso in info.get("warnings") or []:
            print(f"  advertencia: {aviso}")

except Exception as e:
    # Ante un timeout, reintenta con la MISMA referencia_externa.
    print(f"Ocurrió un error de conexión: {e}")
```

</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
const facturaId = 10231;                      // factura_id devuelto al crear el borrador

const url = `https://${instancia}/service/v2/public/invoices/${facturaId}`;

const payload = {
    referencia_externa: 'INV-2026-0001-EDIT-1',
    tercero_id: 4821,
    fecha: '2026-10-21',
    dias_vencimiento: 15,
    moneda: 'COP',
    observaciones: 'Administración octubre 2026 (ajustada)',
    pago_total: false,
    items: [
        {
            producto_id: 340,
            descripcion: 'Administración octubre 2026',
            cantidad: 1,
            valor_unitario: 1200000,
            por_iva: 19,
            iva: 228000
        }
    ]
};

// 2. Función para editar el borrador
async function editarBorrador() {
    try {
        const response = await fetch(url, {
            method: 'PUT', // PATCH tiene la misma semántica
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${token}`
            },
            body: JSON.stringify(payload)
        });

        console.log(`Código de estado HTTP: ${response.status}`);

        const info = await response.json();

        if (response.status === 200) {
            const body = info.body || {};

            console.log(`Borrador actualizado: factura_id=${body.factura_id}`);
            console.log(`Total: ${body.valor_total} | Saldo: ${body.valor_saldo}`);

            if (info.idempotent_replay) {
                console.log('Nota: reintento idempotente, no se regrabó el documento.');
            }
        } else {
            console.log(`Mensaje: ${info.message}`);
            console.log(`error_code: ${info.error_code}`);

            (info.warnings || []).forEach((aviso) => console.log('  advertencia:', aviso));
        }
    } catch (error) {
        // Ante un timeout, reintenta con la MISMA referencia_externa.
        console.error(`Ocurrió un error de conexión: ${error}`);
    }
}

// 3. Ejecutamos la función
editarBorrador();
```

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

<p class="callout warning">**Power Query no puede elegir el verbo de la petición.** `Web.Contents` con `Content` envía siempre `POST`, y este servicio solo acepta `PUT` y `PATCH`. Por eso la edición desde Excel o Power BI se hace a través de una función de tu backend (o de una función de Azure) que reciba el JSON y lo reenvíe a nuby con `PUT`. El ejemplo muestra el cuerpo exacto que debes enviar a esa función y cómo leer el resultado.</p>

```text
let
    // 1. Configura la dirección de TU función, que reenvía con PUT a nuby
    urlFuncion = "https://tu-servicio.example.com/facturas/10231/editar",
    token = "TU_TOKEN_AQUI",  // Token obtenido del servicio Login

    // 2. Construye el cuerpo JSON del contrato de edición
    cuerpo = Json.FromValue([
        referencia_externa = "INV-2026-0001-EDIT-1",
        tercero_id = 4821,
        fecha = "2026-10-21",
        dias_vencimiento = 15,
        moneda = "COP",
        observaciones = "Administración octubre 2026 (ajustada)",
        pago_total = false,
        items = {
            [
                producto_id = 340,
                descripcion = "Administración octubre 2026",
                cantidad = 1,
                valor_unitario = 1200000,
                por_iva = 19,
                iva = 228000
            ]
        }
    ]),

    // 3. Envía el cuerpo a tu función (ella añade el verbo PUT y el token)
    response = Web.Contents(urlFuncion, [
        Headers = [
            #"Content-Type" = "application/json",
            #"Authorization" = "Bearer " & token
        ],
        Content = cuerpo
    ]),

    // 4. Decodifica la respuesta JSON que devuelve nuby
    jsonResponse = Json.Document(response),
    body = jsonResponse[body],

    // 5. Convierte el resultado en tabla
    tabla = Table.FromRecords({body}),
    expandido = Table.ExpandRecordColumn(tabla, "Column1",
        {"factura_id", "estado_id", "consecutivo", "prefijo", "valor_total", "valor_saldo"},
        {"FacturaID", "EstadoID", "ConsecutivoSinNumerar", "ValorTotal", "ValorSaldo"})
in
    expandido
```

<p class="callout info">**Siguiente paso:** cuando el borrador esté correcto, emítelo con [**Emitir Factura a la DIAN**](https://docs.nuby.ai/emitir-factura-dian.html). Recuerda que una vez emitida la factura ya no se puede editar.</p>

</details></div></div>