# Bandeja de entrada

> La bandeja de entrada (inbox) es el flujo para incorporar documentos externos (PDFs de facturas de proveedores, tickets escaneados o ficheros Facturae, además de nóminas) y convertirlos…


La bandeja de entrada (`inbox`) es el flujo para **incorporar documentos
externos** (PDFs de facturas de proveedores, tickets escaneados o
ficheros Facturae, además de nóminas) y convertirlos automáticamente en
gastos o tickets, o en nóminas, con datos pre-rellenados.

El flujo conceptual tiene cuatro pasos:

1. **Subir** el archivo binario con `POST /uploads` y obtener un `upl_<uuid>`.
2. **Crear** el item de bandeja con `POST /inbox` referenciando el upload.
   El sistema escanea el documento en background.
3. **Esperar** (o suscribirse al webhook `inbox.scanned`) hasta que
   `status` pase a `scanned`. La extracción aparece en `scan.extraction`.
4. **Proponer** un documento real con `POST /inbox/{id}/proposeBill`
   o `proposeTicket`. Devuelve un prototipo listo para enviar a
   `POST /bills` con un campo adicional `fromInbox: { taskId }` que
   vincula los dos documentos.

Para una nómina, usa `POST /inbox/{id}/proposePayroll` y envía después
el prototipo a `POST /payrolls` con el mismo campo `fromInbox`.

Los `propose*` **no crean nada en BD**: son funciones puras que mapean
los datos extraídos al shape esperado por la API de creación. Permite
al cliente (o a un agente) revisar y ajustar la propuesta antes de
materializarla.

> En los ejemplos de esta página, los UUIDs (`tas_…`, `upl_…`,
> `con_…`) son ilustrativos. Cada empresa tiene los suyos; sustitúyelos
> por los identificadores reales que devuelve la API.

## El prefijo es `tas_`, no `inb_`

Los items de la bandeja **se identifican con prefijo `tas_<uuid>`**
porque internamente son "tareas" (`task`) pendientes de procesar. Las
tareas existen también para otros usos internos; la bandeja es solo
una vista de las que provienen de subidas, emails o sincronizaciones
externas. Para la API pública, todos los `inbox.id` son `tas_*`.

## Estados del item

`status` describe el escaneo automático:

- **`pending`** — el item se acaba de crear, el escaneo no ha terminado.
  `scan` es `null`.
- **`scanned`** — el escaneo terminó con éxito. `scan` y
  `scan.extraction` están poblados.
- **`scan_failed`** — el escaneo terminó sin datos extraíbles (PDF
  ilegible, imagen sin texto, etc.). `scan.completedAt` y `scan.mimeType`
  están rellenos pero `scan.extraction` puede ser nulo o muy parcial.

## Cómo llegan documentos a la bandeja

Un item puede llegar a la bandeja por cuatro vías. Todas generan items
con el mismo schema y se procesan con la misma lógica de escaneo y
propuesta; el campo `source` dice por cuál entró cada uno.

### API (`source: upload`)

Sube el binario con [`POST /uploads`](https://www.facturadirecta.com/desarrolladores/api/referencia/uploads/) y crea el item con
`POST /inbox` referenciando el upload. Es la única vía que esta API permite
generar directamente, y la que debe usar una integración o un agente que
tenga el fichero en la mano. El item se crea con `status: pending` y el
escaneo se encola al momento.

### Buzón por correo (`source: inbox`)

Cada empresa puede activar una **dirección de correo exclusiva** (del tipo
`nombre-a1b2c3d4e5@inbox.facturadirecta.com`). Cualquier correo que llegue
a ella crea un item por cada adjunto (facturas de compra, tickets o nóminas
en PDF, imagen o Facturae) y además guarda el cuerpo del mensaje como un
PDF adicional, marcado con `contentSubtype: inboxEmailBody`. Es la vía
pensada para que el usuario reenvíe lo que recibe de sus proveedores.

Lo que una integración o un agente debe saber:

- **Dónde está la dirección.** En
  [`GET /settings/company`](https://www.facturadirecta.com/desarrolladores/api/referencia/settings/#ficha-de-la-empresa),
  `content.inbox.email.address`. Solo se devuelve si el buzón está activo
  (`active: true`); el buzón se activa desde Ajustes › Integraciones, no por
  API, y requiere el módulo `inbox` (`content.modules.inbox`).
- **No se verifica el remitente.** Trata la dirección como confidencial:
  dásela al usuario de la empresa, no la publiques en sitios de terceros.
- **Escaneo automático o manual.** `content.inbox.email.autoScan` dice si
  lo recibido se escanea al llegar (planes con escaneos ilimitados). Si es
  `false`, el item queda en la bandeja con `status: pending` hasta que un
  usuario lo escanee desde la interfaz; la API no puede forzar ese escaneo.
- Los items llegan con `source: inbox` y se pueden filtrar así en
  `GET /inbox`.

### Google Drive (`source: googleDrive`)

El usuario puede enlazar desde Ajustes › Integraciones una **carpeta de su
Google Drive** (con permiso de solo lectura). FacturaDirecta la revisa
periódicamente y crea un item por cada fichero nuevo que aparece en ella,
con el mismo tratamiento que un adjunto de correo. Requiere el módulo
`googleDrive` (`content.modules.googleDrive`).

Lo que una integración o un agente debe saber:

- **Qué carpeta es.** En `GET /settings/company`,
  `content.inbox.googleDrive`: `linked` dice si hay carpeta enlazada,
  `folderId` es su identificador en Drive y `folderUrl` la URL para
  abrirla o indicársela al usuario. El nombre de la carpeta no se guarda.
- **Si está parada.** `error` distinto de `null` es el mensaje que ve el
  usuario cuando la sincronización se ha detenido por un error permanente
  (credenciales caducadas, carpeta borrada). La única solución es que el
  usuario vuelva a enlazar la carpeta desde la interfaz.
- Los items llegan con `source: googleDrive`.

### Pantalla de Tareas (`source: scanner`)

Subida manual desde la pantalla de Tareas de la interfaz. No interviene la
API.

## Adjuntos del item

`content.attachments` es la lista de ficheros del item. Por lo general
es un único adjunto (el subido inicialmente), pero algunos orígenes
adjuntan varios (por ejemplo un email con factura PDF + cuerpo HTML).

Cada adjunto tiene `url` S3 directa (permanente mientras el item no se
borre físicamente), `name`, `size`, `contentType` (MIME) y opcionalmente
`contentSubtype`:

- **`facturae`** — el adjunto es un fichero Facturae XML. El sistema lo
  parsea con un extractor nativo (no LLM), por lo que la extracción
  es exacta y rápida.
- **`inboxEmailBody`** — cuerpo del email recibido (cuando `source` es
  `inbox`).

## Extracción y `scan`

Cuando `status` es `scanned`, `content.scan` contiene metadatos del
escaneo y los datos estructurados:

- `scan.completedAt` — cuándo terminó.
- `scan.docType` — tipo de documento detectado (p. ej. `IMAGE`, `PDF`).
- `scan.mimeType` — MIME del archivo.
- `scan.text` — texto plano extraído. **Solo aparece en
  `GET /inbox/{id}`**, no en el listado (optimización de tamaño).
- `scan.extraction` — datos estructurados. El shape depende del tipo
  de documento detectado: `InboxExtractionInvoice` para facturas y
  tickets, `InboxExtractionPayroll` para nóminas.

La extracción contiene campos como `date`, `name` (emisor), `contact`
(NIF/CIF), `number`, `total`, `currency`, `direction` (`RECEIVE` o
`EMIT`)... La API expone estos campos como **strings sin tipar** para no
acoplar la API a la versión actual del extractor; nuevos campos pueden
aparecer sin cambio de contrato.

## Webhook `inbox.scanned`

Cuando un escaneo termina (cualquier `status` final, incluido
`scan_failed`), se dispara el evento de webhook `inbox.scanned`.
Suscribirse permite reaccionar en cuanto los datos están disponibles
en vez de hacer polling. Ver [Webhooks](https://www.facturadirecta.com/desarrolladores/webhooks/) para los
detalles del flujo.

El evento es el mismo tanto si los datos se han obtenido con el escáner como
si el adjunto era un Facturae y se ha leído de forma nativa: para el consumidor
el item queda procesado igual y solo cambia cómo se produjo la extracción.

## Operaciones

- [Lista de items](#lista-de-items)
- [Crear item (subir documento)](#crear-item-subir-documento)
- [Obtener un item](#obtener-un-item)
- [Borrar un item](#borrar-un-item)
- [Proponer factura de compra](#proponer-factura-de-compra)
- [Proponer ticket](#proponer-ticket)
- [Proponer nómina](#proponer-nómina)

## Lista de items

`GET /{companyId}/inbox` devuelve los items de la bandeja.

**Parámetros de consulta específicos:**

- **`status`** — filtra por estado del escaneo: `pending`, `scanned`,
  `scan_failed`.
- **`source`** — filtra por origen: `upload`, `inbox`, `googleDrive`,
  `scanner`.
- **`minDate`**, **`maxDate`** — rango por fecha de creación del item
  (`createdAt`), con los dos extremos incluidos. Cada uno admite:
  - Una fecha y hora ISO 8601 (`YYYY-MM-DDTHH:mm:ss.sssZ`). Se compara el
    instante exacto, con su hora y su desfase horario si lo lleva
    (`+02:00`). Los `:` pueden ir sin codificar; el `+` se codifica como
    `%2B`, o llega como un espacio.
  - Solo la fecha (`YYYY-MM-DD`). Cuenta el día entero en la zona horaria
    de la empresa (por defecto, `Europe/Madrid`):
    `minDate=2026-10-07&maxDate=2026-10-07` devuelve los items creados
    ese día.
- **`archived`** — controla qué items aparecen en el resultado:
  - `false` (por defecto): solo activos.
  - `true`: solo borrados.
  - `all`: ambos.

**Parámetros globales aceptados:**

Acepta además los parámetros estándar `offset`, `limit` y el header
`accept-version`. Ver [Paginación](https://www.facturadirecta.com/desarrolladores/api/guias/pagination/) y
[Autenticación](https://www.facturadirecta.com/desarrolladores/api/guias/authentication/).

**Notas:**

- `scan.text` **no se incluye** en los items del listado para evitar
  cargas grandes. Para verlo, usa `GET /inbox/{id}`.
- Por defecto, los items borrados se omiten. Esto es coherente con
  el comportamiento del `DELETE /inbox/{id}` (borrado recuperable, no
  físico).

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox?status=scanned&source=upload&limit=50"
```

## Crear item (subir documento)

`POST /{companyId}/inbox` crea un nuevo item de bandeja a partir de un
upload previo.

**Flujo de dos pasos:**

1. Subir el binario con `POST /uploads` (multipart) → recibes
   `upl_<uuid>`.
2. Crear el inbox item con este endpoint, pasando el `upl_<uuid>` en
   el campo `upload`.

**Parámetros del body:**

- **`upload`** (obligatorio) — `upl_<uuid>` devuelto por
  `POST /uploads`.
- **`title`** (opcional) — título descriptivo. Si se omite, se usa el
  nombre del archivo del upload.
- **`idempotencyKey`** (opcional) — clave para que el mismo documento no
  entre dos veces en la bandeja. Si ya existe un item activo (sin archivar)
  con esa clave en la empresa, se devuelve ese item con su estado actual y no
  se crea otro. No caduca mientras el item siga activo y no compara el
  fichero: con la misma clave devuelve el item existente aunque envíes otro
  documento. Lo habitual es usar una clave natural del documento, por
  ejemplo un hash del fichero.

**Notas:**

- El item se crea con `status: pending` y se encola el escaneo en
  background.
- La respuesta es el item recién creado. Suscríbete al webhook
  `inbox.scanned` o haz polling sobre `status` para detectar cuándo
  termina el escaneo.

**Parámetros globales aceptados:** `accept-version`.

###### Ejemplo de request JSON

Heredado del ejemplo `default` del openapi:

```json
{
  "upload": "upl_6f8e9b42-1d3c-4a7f-b2e4-9c1d5e7f3a6b",
  "title": "factura.pdf"
}
```

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"upload":"upl_6f8e9b42-1d3c-4a7f-b2e4-9c1d5e7f3a6b","title":"factura.pdf"}' \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox"
```

## Obtener un item

`GET /{companyId}/inbox/{id}` devuelve el detalle de un item por su
ID.

A diferencia del listado, **`scan.text` sí se incluye** (texto plano
extraído del documento).

**Parámetros globales aceptados:** `accept-version`.

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox/tas_3a7f9c12-2d4e-4b8a-9c1f-5d6e8f0a3b2c"
```

## Borrar un item

`DELETE /{companyId}/inbox/{id}` **borra** el item de forma recuperable
(no lo elimina físicamente: queda con `archived: true`). Los items borrados:

- No aparecen en el listado por defecto.
- Conservan sus adjuntos (las URLs S3 siguen siendo accesibles).
- Pueden seguir consultándose por ID si conoces el `tas_*`.
- Aparecen en listados con `archived=true` o `archived=all`.

Si vinculas un inbox item a un documento real vía `fromInbox`, el
sistema borra el item automáticamente (por defecto). Ese es el
camino habitual; este endpoint cubre los casos en que quieres
borrarlo manualmente sin crear un documento (p. ej. spam).

**Respuesta:** `{ result: true }`.

**Parámetros globales aceptados:** `accept-version`.

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -X DELETE \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox/tas_3a7f9c12-2d4e-4b8a-9c1f-5d6e8f0a3b2c"
```

## Proponer factura de compra

`POST /{companyId}/inbox/{id}/proposeBill` toma un item escaneado y
construye un **prototipo de factura de compra** mapeando los datos
extraídos al shape de `BillWrite`. **El endpoint no crea nada en BD**:
devuelve un objeto para que el cliente lo revise, lo ajuste y lo envíe
a `POST /bills`.

**Idempotente y retry-safe.** Llamarlo varias veces sobre el mismo
item devuelve el mismo prototipo (suponiendo que la extracción no
haya cambiado).

**Parámetros del body** (todos opcionales):

- **`contactId`** — si se indica, salta el matching automático y usa
  este contacto en el prototipo.

**Respuesta:**

```json
{
  "content": { "...": "BillWrite — prototipo listo para POST /bills" },
  "matchedContact": null,
  "candidateContacts": [],
  "newContactProposal": { "...": "Contact — proto-contacto con datos extraídos" }
}
```

- **`content`** — el prototipo de bill. Su `uuid` se omite
  intencionadamente; lo asigna `POST /bills` o lo eliges tú al crear.
  Si la factura está en una moneda distinta de la de la empresa,
  `content.main.exchangeRate` trae el tipo de cambio de la fecha de la
  factura (`transactionDate`), el mismo que tomaría la web.
- **`matchedContact`** — contacto único encontrado por la API a partir
  del NIF/CIF/nombre extraídos. `null` si no se encontró ninguno o si
  hay varios candidatos.
- **`candidateContacts`** — lista de candidatos cuando el matching
  encontró varios. Vacío si hay 0 o 1 match.
- **`newContactProposal`** — proto-contacto con los datos extraídos.
  Útil cuando el matching no encontró candidatos: puedes crearlo con
  `POST /contacts` y enlazarlo al bill.

**Flujo recomendado tras la propuesta:**

1. Revisa el `content` y ajústalo si quieres (cambiar líneas, impuestos,
   añadir notas). Si cambias la moneda o la fecha de la factura, quita
   `exchangeRate` para que `POST /bills` tome el de la nueva fecha.
2. Si `matchedContact` es null:
   - O bien usa `candidateContacts` para elegir uno.
   - O bien crea uno desde `newContactProposal` con `POST /contacts`.
   - Pon el ID resultante en `content.main.contact`.
3. Llama a `POST /bills` con `{ content, fromInbox: { taskId, archive } }`:
   - `taskId` es el `tas_*` del item de la bandeja.
   - `archive: true` (por defecto) borra el item de la bandeja tras crear el bill.
   - Esto vincula el bill al inbox; se puede ver desde la interfaz.

**Parámetros globales aceptados:** `accept-version`.

###### Ejemplo de request JSON

Sin body (la API usa la extracción tal cual):

```json
{}
```

Con contacto fijado:

```json
{
  "contactId": "con_2c4f8e21-7b3a-4d9c-9e1f-a8b7c6d5e4f3"
}
```

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{}' \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox/tas_3a7f9c12-2d4e-4b8a-9c1f-5d6e8f0a3b2c/proposeBill"
```

## Proponer ticket

`POST /{companyId}/inbox/{id}/proposeTicket` es equivalente a
[Proponer factura de compra](#proponer-factura-de-compra) pero
construye un prototipo con `subtype: "ticket"` (ver
[Factura de compra vs ticket](https://www.facturadirecta.com/desarrolladores/api/referencia/bills/#factura-de-compra-vs-ticket)
en la página de bills).

Útil cuando el documento escaneado es un ticket de caja (sin proveedor
identificado) en vez de una factura formal.

Misma forma de respuesta y mismo flujo de uso que `proposeBill`. El
prototipo enviado a `POST /bills` ya viene con `subtype: "ticket"`.

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{}' \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox/tas_3a7f9c12-2d4e-4b8a-9c1f-5d6e8f0a3b2c/proposeTicket"
```

## Proponer nómina

`POST /{companyId}/inbox/{id}/proposePayroll` es equivalente a las
anteriores pero construye un prototipo de **nómina** (`PayrollWrite`).
El item debe haber sido escaneado y reconocido como nómina por el
extractor (`scan.extraction` debe ser `InboxExtractionPayroll`).

Misma forma de respuesta: `{ content, matchedContact, candidateContacts,
newContactProposal }`. El contacto en este caso es el empleado.

Para crear la nómina real tras revisar el prototipo, llama a
`POST /payrolls` con `{ content, fromInbox: { taskId, archive } }`.

###### Copy as cURL

```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{}' \
  "https://app.facturadirecta.com/api/$COMPANY_ID/inbox/tas_3a7f9c12-2d4e-4b8a-9c1f-5d6e8f0a3b2c/proposePayroll"
```

## Vinculación con `fromInbox`

Al crear un bill o un payroll desde un inbox item, se incluye en el
body del create el campo opcional:

```json
{
  "content": { "...": "..." },
  "fromInbox": {
    "taskId": "tas_3a7f9c12-2d4e-4b8a-9c1f-5d6e8f0a3b2c",
    "archive": true
  }
}
```

- **`taskId`** — el `tas_*` del inbox item.
- **`archive`** (opcional, default `true`) — si `true`, el item de la bandeja se
  borra automáticamente al crear el documento.

`fromInbox` está disponible en `POST /bills` y `POST /payrolls`. No
está disponible en `POST /invoices` (no hay flujo "subir factura de
venta a inbox" a día de hoy).

El vínculo persiste: la factura o nómina creada queda enlazada al inbox
item en sus `attachments`, y la interfaz puede mostrar el original.

## Recomendaciones

- **Suscríbete al webhook `inbox.scanned`** en vez de hacer polling
  sobre el listado. El polling funciona pero gasta cuota
  innecesariamente.
- **Usa `idempotencyKey` al crear** items si tu proceso puede enviar dos
  veces el mismo documento (jobs reencolados, reenvíos periódicos...). Te
  ahorra duplicados sin límite de tiempo, mientras el item no se archive.
  Es un mecanismo propio de la bandeja: `POST /inbox` no admite la cabecera
  `Idempotency-Key` del resto de operaciones. Ver
  [Idempotencia](https://www.facturadirecta.com/desarrolladores/api/guias/idempotency/#post-inbox-es-distinto).
- **Llama a `propose*` solo cuando `status: scanned`**. Si lo llamas
  antes, la propuesta saldrá vacía o sin datos extraídos.
- **Para Facturae, la extracción es nativa y exacta**: usa
  `contentSubtype: "facturae"` para identificarlos en el listado y
  saber que la propuesta será fiable sin ajuste manual.
- **`fromInbox.archive: false`** solo si necesitas mantener el inbox
  activo (por ejemplo, para vincularlo a varios bills). El uso normal
  es borrarlo.
- **Para integraciones agentic**, los `propose*` son ideales: solo
  requieren `inbox:read`, no `bills:write` ni `payrolls:write`. Un
  agente puede proponer y dejar al humano (o a otro agente con scopes
  superiores) el acto de crear.

## Errores comunes

- `400` — `minDate` o `maxDate` es una fecha y hora que no existe, como
  `2026-02-30T10:00:00Z`. Ver [Fechas que no existen](https://www.facturadirecta.com/desarrolladores/api/guias/errors/#fechas-que-no-existen-400).
- `400 ValidationError` — `upload` con formato inválido o que no
  existe.
- `404 Not Found` — `tas_*` no existe en la empresa.
- `409 Conflict` — `propose*` sobre un item con `status: pending` (el
  escaneo aún no ha terminado).
- `409 Conflict` — `proposePayroll` sobre un item cuya extracción no
  es de nómina.

Ver [Errores y validaciones](https://www.facturadirecta.com/desarrolladores/api/guias/errors/) para el formato
general.

## Referencia exhaustiva

Esta página cubre los matices funcionales y los casos típicos. Para la
referencia exhaustiva de todos los campos del body y la respuesta,
consulta el [Swagger UI](https://www.facturadirecta.com/desarrolladores/api/explorador/) o el
[openapi crudo](https://app.facturadirecta.com/openapi.json).

## Endpoints

| Método | Path | operationId | Scopes | Descripción |
|---|---|---|---|---|
| GET | `/{companyId}/inbox` | `getInboxItems` | `inbox:read` | Lista de items en la bandeja de entrada |
| GET | `/{companyId}/inbox/{id}` | `getInboxItem` | `inbox:read` | Detalle de un item de la bandeja |
| POST | `/{companyId}/inbox` | `createInboxItem` | `inbox:write` | Subir un documento a la bandeja de entrada |
| POST | `/{companyId}/inbox/{id}/proposeBill` | `proposeBill` | `inbox:read` | Propuesta de factura de compra desde un item de la bandeja |
| POST | `/{companyId}/inbox/{id}/proposePayroll` | `proposePayroll` | `inbox:read` | Propuesta de nómina desde un item de la bandeja |
| POST | `/{companyId}/inbox/{id}/proposeTicket` | `proposeTicket` | `inbox:read` | Propuesta de ticket de compra desde un item de la bandeja |
| DELETE | `/{companyId}/inbox/{id}` | `deleteInboxItem` | `inbox:write` | Archivar un item de la bandeja |

## Scopes

- **`inbox:read`** — Lectura de la bandeja de entrada de documentos pendientes de procesar.
- **`inbox:write`** — Subida y gestión de documentos en la bandeja de entrada.


Fuente: https://github.com/facturadirecta/api-docs/blob/a9c89bac0578614ce4cdbb89e099e60b7e71f80e/sections/inbox.md
