# Horvell API v1.1.0

Documentación de la API pública de Horvell: expedientes y documentos de un despacho, con claves de despacho y de cliente del despacho.

URL base: `https://api.horvell.com/v1`

## Dos tipos de acceso

Horvell emite dos tipos de clave. Las dos usan la misma API y las mismas llamadas, pero ven y pueden hacer cosas distintas. El prefijo de la clave te dice cuál tienes.

| | Clave de despacho | Clave de cliente |
| --- | --- | --- |
| **Para quién es** | La informática del propio despacho o su proveedor de software. | Un cliente del despacho (una aseguradora, una empresa, un fondo) que quiere ver sus asuntos desde su sistema. |
| **Quién la crea** | Un administrador del despacho, en Configuración › Integraciones. | Un administrador del despacho, en la ficha del cliente › Acceso API. |
| **Prefijo** | `hv_tk_…` | `hv_ck_…` |
| **Qué ve** | Todos los expedientes del despacho, de todos sus clientes. | Solo los expedientes de ese cliente. Nada de otros clientes, ni siquiera si existen. |
| **Puede escribir** | Sí, con el permiso `documentos:write`: subir documentos. | No. Siempre de solo lectura. |
| **Permisos** | Los que elija el administrador: `expedientes:read`, `documentos:read`, `documentos:write`. | Fijos: `expedientes:read` y `documentos:read`. |
| **Límite** | 600 peticiones por minuto. | 120 peticiones por minuto. |
| **Ejemplo de uso** | Volcar cada noche los expedientes y documentos al ERP o al cuadro de mando del despacho. | La aseguradora consulta desde su sistema de siniestros en qué punto está cada reclamación y descarga los escritos. |

> **¿Tienes una clave de cliente?.** Elige **Clave de cliente** arriba y la documentación se adapta: los ejemplos usan tu tipo de clave y las llamadas que no puedes usar aparecen atenuadas.

Con una clave de cliente, lo que no es de tu cliente no existe: pedir un expediente de otro devuelve el mismo `404` que uno inexistente, y los listados nunca lo incluyen.

## Introducción

La API de Horvell da acceso a los **expedientes** y **documentos** de un despacho desde otro sistema: un ERP, un cuadro de mando, el programa de siniestros de una aseguradora o una automatización propia. Con una clave de despacho también se pueden **subir documentos** a un expediente.

- Es una API REST sobre HTTPS. Todo, peticiones y respuestas, va en JSON y en UTF-8.
- Las fechas van en UTC con formato RFC 3339: `2026-10-22T09:30:00.000Z`.
- Cada objeto lleva un `id` con prefijo de su tipo (`exp_…`, `doc_…`) que no cambia nunca. Úsalo para identificarlo en tu sistema.
- Los nombres de campos y valores están en español, como en Horvell.
- Los errores siguen el estándar RFC 9457 y llevan un `code` estable para tu programa.

- Contrato OpenAPI 3.1: https://api.horvell.com/v1/openapi.json (impórtalo en Postman, Insomnia o Bruno).
- Esta documentación en la web: https://horvell.com/docs/api

¿Buscas otra forma de conectar Horvell (Outlook, Holded, remesas SEPA, Kmaleon, Excel…)? Están todas en [integraciones](https://horvell.com/integraciones).

**URL base**

```text
https://api.horvell.com/v1
```

**Formato de un error**

```json
{
  "type": "https://api.horvell.com/v1/docs#errores-no_encontrado",
  "title": "No encontrado",
  "status": 404,
  "code": "no_encontrado",
  "request_id": "req_q3Zl8vY1cN0kT5bW"
}
```

## Inicio rápido

1. **Consigue una clave.** La crea un administrador del despacho. Para el propio despacho: Configuración › Integraciones › Claves de la API del despacho. Para un cliente: ficha del cliente › Acceso API. Se muestra **una sola vez**: guárdala al momento en un lugar seguro.
2. **Guárdala como variable de entorno.** Nunca la escribas en el código ni la subas a un repositorio. En los ejemplos de esta página se lee de `HORVELL_API_KEY`.
3. **Haz tu primera llamada.** Pide los dos primeros expedientes. Si recibes un objeto `list`, ya estás dentro. Si recibes `401`, revisa la clave.

**Tu primera llamada**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/expedientes?limit=2" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta**

```json
{
  "object": "list",
  "data": [
    { "object": "expediente", "id": "exp_6537a1b2c3d4e5f607182930", "numero": "EXP-2026-0142", … },
    { "object": "expediente", "id": "exp_6537a1b2c3d4e5f607182935", "numero": "EXP-2026-0143", … }
  ],
  "has_more": true,
  "next_cursor": "eyJmIjoiMjAyNi0xMC0yMlQwOTozMDowMC4wMDBaIiwiaSI6IjY1MzdhMWIyIn0.k4vQ2s"
}
```

## Autenticación

Cada petición lleva la clave en la cabecera `Authorization`, con el esquema `Bearer`:

- Sin clave, o con una clave revocada, caducada o mal copiada, la respuesta es `401 no_autenticado`.
- Una clave sin el permiso que pide la llamada recibe `403 permiso_insuficiente`.
- La clave solo sirve en la API. No da acceso a la aplicación de Horvell, y una sesión de la aplicación no sirve en la API.
- Si una clave se expone, pide al despacho que la **revoque** o la **rote** desde Horvell: deja de funcionar al instante.
- La clave nunca va en la URL. Solo en la cabecera.

**Cabecera**

```http
Authorization: Bearer hv_tk_TU_CLAVE
```

## Sincronizar con tu sistema

La forma recomendada de tener una copia al día es una sincronización periódica (cada 15 minutos, cada hora, cada noche) en dos fases: primero lo que se ha **borrado** y después lo que ha **cambiado**.

1. **Primera carga.** Recorre [`GET /v1/expedientes`](https://horvell.com/docs/api#listar-expedientes) y [`GET /v1/documentos`](https://horvell.com/docs/api#listar-todos-documentos) siguiendo `next_cursor` hasta que `has_more` sea `false`. Antes de empezar, apunta la hora.
2. **Eliminados.** En cada sincronización siguiente, pide [`GET /v1/eliminados`](https://horvell.com/docs/api#listar-eliminados) con `updated_since` = la hora que apuntaste, y borra de tu sistema lo que te devuelva. Borrar un expediente implica sus documentos.
3. **Cambios.** Después pide `GET /v1/expedientes?updated_since=…` y `GET /v1/documentos?updated_since=…`, y guarda lo que te devuelvan, identificando cada objeto por su `id`. Apunta la nueva hora para la próxima vez.

> **Resta un margen a la hora.** `updated_since` es inclusivo. Usa la hora de **inicio** de la sincronización anterior menos un minuto: es mejor recibir algo dos veces (lo sobrescribes por su `id`) que perderlo.

Un cambio en los **señalamientos** de un expediente también lo hace reaparecer en `updated_since`, aunque su `actualizado_en` no cambie. Los documentos tienen su propio listado y no hace falta pedirlos expediente por expediente.

Los eliminados se guardan **180 días**. Si tu sistema pasa más tiempo sin sincronizar, recibirás `410 sincronizacion_caducada`: vuelve a la primera carga y borra lo que ya no aparezca.

**Sincronización completa**

```javascript
const API = "https://api.horvell.com/v1";
const cabeceras = { Authorization: `Bearer ${process.env.HORVELL_API_KEY}` };
const esperar = (s) => new Promise((ok) => setTimeout(ok, s * 1000));

// Recorre todas las páginas de un listado.
async function* recorrer(ruta, filtros = {}) {
  let cursor = null;
  do {
    const url = new URL(API + ruta);
    for (const [k, v] of Object.entries({ ...filtros, limit: 100 })) url.searchParams.set(k, v);
    if (cursor) url.searchParams.set("cursor", cursor);
    const r = await fetch(url, { headers: cabeceras });
    if (r.status === 429) {
      await esperar(Number(r.headers.get("Retry-After") ?? 1));
      continue; // repite la misma página
    }
    if (!r.ok) throw new Error((await r.json()).code);
    const pagina = await r.json();
    yield* pagina.data;
    cursor = pagina.next_cursor;
  } while (cursor);
}

// desde: inicio de la sincronización anterior (ISO) o null la primera vez.
export async function sincronizar(desde) {
  const inicio = new Date(Date.now() - 60_000).toISOString(); // margen de 1 min
  const filtros = desde ? { updated_since: desde } : {};

  if (desde) {
    for await (const e of recorrer("/eliminados", filtros)) {
      if (e.tipo === "expediente") await miSistema.borrarExpediente(e.recurso);
      else await miSistema.borrarDocumento(e.recurso);
    }
  }

  for await (const expediente of recorrer("/expedientes", filtros)) {
    await miSistema.guardarExpediente(expediente);
  }

  for await (const documento of recorrer("/documentos", filtros)) {
    await miSistema.guardarDocumento(documento);
  }

  return inicio; // guárdalo para la próxima vez
}
```

## Subir un documento (solo claves de despacho)

El archivo no pasa por la API: va directo al almacenamiento con un enlace firmado. Así se suben archivos grandes sin cortes. Solo claves de despacho con el permiso `documentos:write`.

1. **Iniciar.** [`POST /v1/expedientes/{id}/subidas`](https://horvell.com/docs/api#iniciar-subida) con el nombre, tipo, tamaño y categoría del archivo, y una `Idempotency-Key` única. Te devolvemos `url_subida` y las `cabeceras` que debe llevar el envío.
2. **Enviar el archivo.** `PUT` del archivo a `url_subida`, con esas cabeceras y **sin** `Authorization`. El enlace dura 15 minutos.
3. **Completar.** [`POST /v1/subidas/{id}/completar`](https://horvell.com/docs/api#completar-subida). Comprobamos que el archivo ha llegado y creamos el documento, que aparece al momento en Horvell.

> **Reintentos seguros.** Si cualquier paso falla por red, repítelo. El paso 1 con la misma `Idempotency-Key` y el mismo cuerpo devuelve la misma subida con un enlace nuevo; el paso 3 devuelve siempre el mismo documento. Nunca se crea un duplicado.

Hasta **50 MB** por archivo. Tienes 30 minutos desde el paso 1 para completar la subida. Formatos admitidos: `application/pdf`, `application/msword`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/vnd.ms-excel`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `application/vnd.oasis.opendocument.text`, `application/vnd.oasis.opendocument.spreadsheet`, `application/rtf`, `application/vnd.ms-outlook`, `image/png`, `image/jpeg`, `image/heic`, `image/webp`, `message/rfc822`, `text/plain`, `text/csv`, `text/rtf`, `application/zip`, `image/tiff`, `application/vnd.ms-powerpoint`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `application/vnd.oasis.opendocument.presentation`, `text/xml`, `application/pkcs7-signature`, `application/pkcs7-mime`, `application/xades+xml`, `audio/mpeg`, `audio/mp4`, `audio/wav`, `audio/ogg`, `audio/aac`, `video/mp4`, `video/quicktime`, `video/webm`, `video/x-msvideo`.

**Subida completa**

```javascript
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const API = "https://api.horvell.com/v1";
const cabeceras = { Authorization: `Bearer ${process.env.HORVELL_API_KEY}` };

export async function subirDocumento(idExpediente, ruta, { categoria, nombre }) {
  const archivo = await readFile(ruta);
  // Guárdala junto al documento: si algo falla, reintenta con la misma.
  const idempotencia = randomUUID();

  // 1. Iniciar
  const r1 = await fetch(`${API}/expedientes/${idExpediente}/subidas`, {
    method: "POST",
    headers: { ...cabeceras, "Content-Type": "application/json", "Idempotency-Key": idempotencia },
    body: JSON.stringify({
      nombre_archivo: ruta.split("/").pop(),
      tipo_contenido: "application/pdf",
      tamano: archivo.length,
      categoria,
      nombre,
    }),
  });
  if (!r1.ok) throw new Error((await r1.json()).code);
  const subida = await r1.json();

  // 2. Enviar el archivo directo al almacenamiento (sin Authorization)
  const r2 = await fetch(subida.url_subida, {
    method: subida.metodo,
    headers: subida.cabeceras,
    body: archivo,
  });
  if (!r2.ok) throw new Error(`PUT ${r2.status}`);

  // 3. Completar: crea el documento
  const r3 = await fetch(`${API}/subidas/${subida.id}/completar`, {
    method: "POST",
    headers: cabeceras,
  });
  if (!r3.ok) throw new Error((await r3.json()).code);
  return r3.json(); // el Documento
}
```

## Paginación

Los listados devuelven un objeto `list` con una página de resultados. Para la siguiente, repite la misma petición añadiendo `cursor` con el `next_cursor` que recibiste. Cuando `has_more` es `false`, has terminado.

- `limit` va de 1 a 100 (25 por defecto). Para sincronizar, usa 100.
- Pide todas las páginas con **los mismos filtros**. Si cambias un filtro a mitad, el cursor deja de valer (`400 cursor_invalido`).
- El cursor es opaco: no lo interpretes ni lo construyas, solo devuélvelo.
- El orden es estable (`actualizado_en` y después `id`), así que no te saltarás elementos aunque cambien datos mientras recorres.

**Página siguiente**

```bash
curl "https://api.horvell.com/v1/expedientes?limit=100&cursor=eyJmIjoiMjAyNi0xMC0yMlQwOTozMDowMC4wMDBaIiwiaSI6IjY1MzdhMWIyIn0.k4vQ2s" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

## Caché con ETag

El detalle de un expediente y el de un documento llevan una cabecera `ETag`. Guárdala con el objeto y envíala en `If-None-Match` la próxima vez que lo pidas: si no ha cambiado, la respuesta es `304 Not Modified`, sin cuerpo.

Es más rápido, no gasta datos y sigue contando para el límite de peticiones.

**Petición condicional**

```bash
curl -i "https://api.horvell.com/v1/expedientes/exp_6537a1b2c3d4e5f607182930" \
  -H "Authorization: Bearer $HORVELL_API_KEY" \
  -H 'If-None-Match: W/"1792661400000-3-1792574000000-1"'

# HTTP/2 304
```

## Límites de uso

| Límite | Valor |
| --- | --- |
| Clave de despacho | 600 peticiones por minuto |
| Clave de cliente | 120 peticiones por minuto |
| Por dirección IP | 1.200 peticiones por minuto |
| Subidas por despacho | 30 por minuto |
| Tamaño de archivo | 50 MB |
| Elementos por página | 100 |

Cada respuesta lleva `RateLimit-Limit`, `RateLimit-Remaining` y `RateLimit-Reset` (segundos hasta que se recupera el cupo). Si lo superas, recibirás `429 demasiadas_peticiones` con `Retry-After`: espera esos segundos y repite.

> **Nota.** Una sincronización bien hecha (`updated_since`, `limit=100`, `ETag`) rara vez se acerca al límite. Si lo alcanzas a menudo, reparte las peticiones en el tiempo en lugar de lanzarlas en paralelo.

**Cabeceras de límite**

```http
HTTP/2 200
RateLimit-Limit: 600
RateLimit-Remaining: 557
RateLimit-Reset: 41
```

## Versiones y cambios

Esta es la **v1** y va en la URL (`/v1/…`). Dentro de ella solo **añadimos**: campos nuevos, llamadas nuevas y valores nuevos en las enumeraciones. Nunca quitamos ni renombramos un campo, ni cambiamos su tipo, ni hacemos obligatorio algo que no lo era. Un cambio así sería una v2, y la v1 seguiría funcionando.

> **Ignora lo que no conozcas.** Programa tu integración para que tolere campos y valores nuevos: ignora los campos que no esperas y trata un valor desconocido de `estado`, `tipo` o `categoria` como «otro». Así no se romperá cuando ampliemos la API.

## Referencia

## Expedientes

Los asuntos del despacho, con sus procedimientos, señalamientos e intervinientes.

### Listar expedientes

`GET /v1/expedientes`

Devuelve los expedientes a los que tiene acceso tu clave, de 25 en 25 por defecto. Es la llamada con la que empieza cualquier sincronización.

**Claves:** Despacho, Cliente · **Permiso:** `expedientes:read`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query | entero | no | Elementos por página, de 1 a 100. |
| `cursor` | query | texto | no | `next_cursor` de la página anterior. |
| `updated_since` | query | fecha | no | RFC 3339. Inclusivo: devuelve lo modificado en o después de esta fecha. |
| `numero` | query | texto | no | Número exacto del expediente. |
| `referencia_externa` | query | texto | no | Código exacto del expediente en el sistema del que se importó. |
| `referencia` | query | texto | no | Valor exacto de cualquiera de las tres referencias del cliente (`referencias`). |
| `numero_procedimiento` | query | texto | no | Número de autos exacto de alguno de sus procedimientos, p. ej. `1234/2026`. |
| `nig` | query | texto | no | NIG exacto de alguno de sus procedimientos. |
| `estado` | query | lista de enum | no | Uno o varios estados separados por coma, p. ej. `en_curso,pendiente_cliente`. Valores: `nuevo`, `en_estudio`, `en_curso`, `pendiente_cliente`, `pendiente_tercero`, `en_pausa`, `cerrado`, `archivado`. |
| `cliente` | query | texto | no | Solo claves de despacho: expedientes de ese cliente. Con una clave de cliente devuelve `400 parametro_no_permitido`. |

- Orden: `actualizado_en` ascendente, del más antiguo al más reciente. Así, si una sincronización se corta, puedes continuar desde el último que guardaste.
- Una clave de cliente solo ve los expedientes de su cliente. No hace falta (ni se permite) filtrar por `cliente`.
- Los filtros de texto (`numero`, `referencia`, `referencia_externa`, `numero_procedimiento`, `nig`) buscan el valor exacto, no por aproximación. `referencia` busca en las tres referencias del cliente a la vez.
- Un cambio en los señalamientos de un expediente también lo hace salir con `updated_since`, aunque su `actualizado_en` no cambie.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/expedientes?updated_since=2026-10-22T00:00:00Z&estado=en_curso,pendiente_cliente&limit=100" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "list",
  "data": [
    {
      "object": "expediente",
      "id": "exp_6537a1b2c3d4e5f607182930",
      "numero": "EXP-2026-0142",
      "titulo": "Reclamación de cantidad · Transportes del Turia",
      "referencia_externa": "457906",
      "referencias": {
        "referencia_1": "PRE-2019-004512",
        "referencia_2": "CART-2026-118",
        "referencia_3": null
      },
      "tipo": "procesal_litigacion",
      "estado": "en_curso",
      "situacion_procesal": "Contestación a la demanda presentada",
      "cuantia": {
        "tipo": "determinada",
        "importe": 18450.75
      },
      "etiquetas": [
        "recobro",
        "prioritario"
      ],
      "cliente": {
        "id": "cli_6537a1b2c3d4e5f607182933",
        "nombre": "Aseguradora Ejemplo, S.A."
      },
      "responsable": {
        "nombre": "Laura Martín"
      },
      "procedimientos": [
        {
          "organismo": "Juzgado de Primera Instancia",
          "num_organismo": "4",
          "poblacion": "Valencia",
          "tipo_procedimiento": "Juicio verbal",
          "numero_procedimiento": "1234/2026",
          "nig": "4625042120260012345",
          "fecha_alta": "2026-03-02T00:00:00.000Z"
        }
      ],
      "senalamientos": [
        {
          "tipo": "vista",
          "estado": "senalado",
          "titulo": "Vista del juicio verbal",
          "fecha_inicio": "2026-11-18T09:30:00.000Z",
          "fecha_fin": "2026-11-18T10:30:00.000Z",
          "zona_horaria": "Europe/Madrid",
          "ubicacion": "Ciudad de la Justicia, sala 12"
        }
      ],
      "intervinientes": [
        {
          "nombre": "Transportes Ficticios del Turia, S.L.",
          "rol": "Demandado"
        },
        {
          "nombre": "Ana Pérez Gil",
          "rol": "Procuradora"
        }
      ],
      "creado_en": "2026-02-27T08:15:00.000Z",
      "actualizado_en": "2026-10-22T09:30:00.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJmIjoiMjAyNi0xMC0yMlQwOTozMDowMC4wMDBaIiwiaSI6IjY1MzdhMWIyIn0.k4vQ2s"
}
```

**Errores:** [`parametro_invalido`](https://horvell.com/docs/api#errores-parametro_invalido), [`parametro_no_permitido`](https://horvell.com/docs/api#errores-parametro_no_permitido), [`cursor_invalido`](https://horvell.com/docs/api#errores-cursor_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

### Detalle de un expediente

`GET /v1/expedientes/{id}`

Devuelve un expediente con sus procedimientos, señalamientos e intervinientes.

**Claves:** Despacho, Cliente · **Permiso:** `expedientes:read`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | texto | sí | Id del expediente (`exp_…`). |

- La respuesta lleva una cabecera `ETag`. Si la envías en `If-None-Match` en la siguiente consulta y el expediente no ha cambiado, recibirás un `304` sin cuerpo: más rápido y no gasta datos.
- Un expediente que no existe y uno que tu clave no puede ver devuelven el mismo `404`.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/expedientes/exp_6537a1b2c3d4e5f607182930" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "expediente",
  "id": "exp_6537a1b2c3d4e5f607182930",
  "numero": "EXP-2026-0142",
  "titulo": "Reclamación de cantidad · Transportes del Turia",
  "referencia_externa": "457906",
  "referencias": {
    "referencia_1": "PRE-2019-004512",
    "referencia_2": "CART-2026-118",
    "referencia_3": null
  },
  "tipo": "procesal_litigacion",
  "estado": "en_curso",
  "situacion_procesal": "Contestación a la demanda presentada",
  "cuantia": {
    "tipo": "determinada",
    "importe": 18450.75
  },
  "etiquetas": [
    "recobro",
    "prioritario"
  ],
  "cliente": {
    "id": "cli_6537a1b2c3d4e5f607182933",
    "nombre": "Aseguradora Ejemplo, S.A."
  },
  "responsable": {
    "nombre": "Laura Martín"
  },
  "procedimientos": [
    {
      "organismo": "Juzgado de Primera Instancia",
      "num_organismo": "4",
      "poblacion": "Valencia",
      "tipo_procedimiento": "Juicio verbal",
      "numero_procedimiento": "1234/2026",
      "nig": "4625042120260012345",
      "fecha_alta": "2026-03-02T00:00:00.000Z"
    }
  ],
  "senalamientos": [
    {
      "tipo": "vista",
      "estado": "senalado",
      "titulo": "Vista del juicio verbal",
      "fecha_inicio": "2026-11-18T09:30:00.000Z",
      "fecha_fin": "2026-11-18T10:30:00.000Z",
      "zona_horaria": "Europe/Madrid",
      "ubicacion": "Ciudad de la Justicia, sala 12"
    }
  ],
  "intervinientes": [
    {
      "nombre": "Transportes Ficticios del Turia, S.L.",
      "rol": "Demandado"
    },
    {
      "nombre": "Ana Pérez Gil",
      "rol": "Procuradora"
    }
  ],
  "creado_en": "2026-02-27T08:15:00.000Z",
  "actualizado_en": "2026-10-22T09:30:00.000Z"
}
```

**Errores:** [`id_invalido`](https://horvell.com/docs/api#errores-id_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_encontrado`](https://horvell.com/docs/api#errores-no_encontrado), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

## Documentos

Los archivos de cada expediente: sus datos y su descarga.

### Listar los documentos de un expediente

`GET /v1/expedientes/{id}/documentos`

Devuelve los documentos de un expediente: nombre, tipo, tamaño, categoría y fechas. El archivo se descarga aparte.

**Claves:** Despacho, Cliente · **Permiso:** `documentos:read`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | texto | sí | Id del expediente (`exp_…`). |
| `limit` | query | entero | no | Elementos por página, de 1 a 100. |
| `cursor` | query | texto | no | `next_cursor` de la página anterior. |
| `updated_since` | query | fecha | no | RFC 3339. Inclusivo: devuelve lo modificado en o después de esta fecha. |
| `categoria` | query | enum | no | Solo documentos de esta categoría. Valores: `identificacion`, `documentacion_cliente`, `contratos_escrituras`, `escritos_comunicaciones`, `procesal_administrativo`, `prueba`, `facturacion_economico`, `otro`. |

- Orden: `actualizado_en` ascendente.
- Los PDF de las facturas del despacho no se incluyen.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/expedientes/exp_6537a1b2c3d4e5f607182930/documentos?categoria=escritos_comunicaciones" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "list",
  "data": [
    {
      "object": "documento",
      "id": "doc_6537a1b2c3d4e5f607182931",
      "expediente": "exp_6537a1b2c3d4e5f607182930",
      "nombre": "Contestación a la demanda",
      "nombre_archivo": "contestacion-demanda.pdf",
      "tipo_contenido": "application/pdf",
      "tamano": 254331,
      "categoria": "escritos_comunicaciones",
      "etiquetas": [
        "presentado"
      ],
      "origen": null,
      "creado_en": "2026-10-20T16:42:00.000Z",
      "actualizado_en": "2026-10-20T16:42:00.000Z"
    },
    {
      "object": "documento",
      "id": "doc_6537a1b2c3d4e5f607182936",
      "expediente": "exp_6537a1b2c3d4e5f607182930",
      "nombre": "Informe pericial",
      "nombre_archivo": "informe-pericial.pdf",
      "tipo_contenido": "application/pdf",
      "tamano": 1048576,
      "categoria": "prueba",
      "etiquetas": [
        "perito"
      ],
      "origen": "api",
      "creado_en": "2026-10-22T10:05:12.000Z",
      "actualizado_en": "2026-10-22T10:05:12.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

**Errores:** [`parametro_invalido`](https://horvell.com/docs/api#errores-parametro_invalido), [`cursor_invalido`](https://horvell.com/docs/api#errores-cursor_invalido), [`id_invalido`](https://horvell.com/docs/api#errores-id_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_encontrado`](https://horvell.com/docs/api#errores-no_encontrado), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

### Listar todos los documentos

`GET /v1/documentos`

Devuelve los documentos de todos los expedientes a los que tiene acceso tu clave. Con `updated_since` es la forma de sincronizar documentos de una vez, sin recorrer expediente por expediente.

**Claves:** Despacho, Cliente · **Permiso:** `documentos:read`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query | entero | no | Elementos por página, de 1 a 100. |
| `cursor` | query | texto | no | `next_cursor` de la página anterior. |
| `updated_since` | query | fecha | no | RFC 3339. Inclusivo: devuelve lo modificado en o después de esta fecha. |
| `categoria` | query | enum | no | Solo documentos de esta categoría. Valores: `identificacion`, `documentacion_cliente`, `contratos_escrituras`, `escritos_comunicaciones`, `procesal_administrativo`, `prueba`, `facturacion_economico`, `otro`. |
| `expediente` | query | texto | no | Solo los documentos de este expediente (`exp_…`). Si no está al alcance de la clave, la lista sale vacía. |

- Orden: `actualizado_en` ascendente, del más antiguo al más reciente, como el resto de listados.
- Una clave de cliente solo recibe los documentos de los expedientes de su cliente.
- Con `expediente` equivale al listado de un expediente. Si ese expediente no está al alcance de tu clave, la lista sale vacía.
- Los PDF de las facturas del despacho no se incluyen.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/documentos?updated_since=2026-10-22T00:00:00Z&limit=100" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "list",
  "data": [
    {
      "object": "documento",
      "id": "doc_6537a1b2c3d4e5f607182931",
      "expediente": "exp_6537a1b2c3d4e5f607182930",
      "nombre": "Contestación a la demanda",
      "nombre_archivo": "contestacion-demanda.pdf",
      "tipo_contenido": "application/pdf",
      "tamano": 254331,
      "categoria": "escritos_comunicaciones",
      "etiquetas": [
        "presentado"
      ],
      "origen": null,
      "creado_en": "2026-10-20T16:42:00.000Z",
      "actualizado_en": "2026-10-20T16:42:00.000Z"
    },
    {
      "object": "documento",
      "id": "doc_6537a1b2c3d4e5f607182936",
      "expediente": "exp_6537a1b2c3d4e5f607182930",
      "nombre": "Informe pericial",
      "nombre_archivo": "informe-pericial.pdf",
      "tipo_contenido": "application/pdf",
      "tamano": 1048576,
      "categoria": "prueba",
      "etiquetas": [
        "perito"
      ],
      "origen": "api",
      "creado_en": "2026-10-22T10:05:12.000Z",
      "actualizado_en": "2026-10-22T10:05:12.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

**Errores:** [`parametro_invalido`](https://horvell.com/docs/api#errores-parametro_invalido), [`cursor_invalido`](https://horvell.com/docs/api#errores-cursor_invalido), [`id_invalido`](https://horvell.com/docs/api#errores-id_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

### Metadatos de un documento

`GET /v1/documentos/{id}`

Devuelve los datos de un documento, sin el archivo.

**Claves:** Despacho, Cliente · **Permiso:** `documentos:read`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | texto | sí | Id del documento (`doc_…`). |

- Admite `ETag` e `If-None-Match` igual que el detalle de un expediente.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/documentos/doc_6537a1b2c3d4e5f607182931" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "documento",
  "id": "doc_6537a1b2c3d4e5f607182931",
  "expediente": "exp_6537a1b2c3d4e5f607182930",
  "nombre": "Contestación a la demanda",
  "nombre_archivo": "contestacion-demanda.pdf",
  "tipo_contenido": "application/pdf",
  "tamano": 254331,
  "categoria": "escritos_comunicaciones",
  "etiquetas": [
    "presentado"
  ],
  "origen": null,
  "creado_en": "2026-10-20T16:42:00.000Z",
  "actualizado_en": "2026-10-20T16:42:00.000Z"
}
```

**Errores:** [`id_invalido`](https://horvell.com/docs/api#errores-id_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_encontrado`](https://horvell.com/docs/api#errores-no_encontrado), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

### Descargar un documento

`GET /v1/documentos/{id}/contenido`

Responde con una redirección `302` a un enlace temporal y firmado del archivo. Tu cliente HTTP solo tiene que seguirla.

**Claves:** Despacho, Cliente · **Permiso:** `documentos:read`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | texto | sí | Id del documento (`doc_…`). |

- El enlace caduca a los 5 minutos. No lo guardes: pide uno nuevo cada vez que necesites el archivo.
- Al seguir la redirección no reenvíes la cabecera `Authorization`: el enlace ya va firmado. La mayoría de clientes HTTP lo hacen solos al cambiar de dominio.
- El archivo llega con su `Content-Type` original y exactamente `tamano` bytes.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl -L -o documento.pdf "https://api.horvell.com/v1/documentos/doc_6537a1b2c3d4e5f607182931/contenido" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Errores:** [`id_invalido`](https://horvell.com/docs/api#errores-id_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_encontrado`](https://horvell.com/docs/api#errores-no_encontrado), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

## Subidas

Añadir documentos a un expediente en tres pasos. Solo claves de despacho.

### Iniciar la subida de un documento (paso 1 de 3)

`POST /v1/expedientes/{id}/subidas`

Paso 1 de 3 para subir un documento a un expediente. Registra la subida y te devuelve un enlace firmado al que enviar el archivo.

**Claves:** Despacho · **Permiso:** `documentos:write`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | texto | sí | Id del expediente (`exp_…`). |
| `idempotency-key` | header | texto | sí | Valor único por documento (p. ej. un UUID). Repetir con el mismo cuerpo devuelve la misma subida, nunca una nueva. |

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `nombre_archivo` | texto | sí | Nombre del archivo con su extensión. (mín. 1 caracteres, máx. 255 caracteres) |
| `tipo_contenido` | texto | sí | Tipo MIME del archivo. Debe ser uno de los admitidos. (mín. 1 caracteres, máx. 150 caracteres) |
| `tamano` | entero | sí | Tamaño exacto del archivo en bytes, hasta 50000000. (mín. 1, máx. 50.000.000) |
| `categoria` | enum | sí | Categoría con la que se guardará el documento. Valores: `identificacion`, `documentacion_cliente`, `contratos_escrituras`, `escritos_comunicaciones`, `procesal_administrativo`, `prueba`, `facturacion_economico`, `otro`. |
| `nombre` | texto | no | Nombre visible en Horvell. Si no lo envías, se usa `nombre_archivo`. (máx. 255 caracteres) |
| `etiquetas` | lista de texto | no | Hasta 20 etiquetas de 60 caracteres como máximo. (máx. 20 elementos, cada uno mín. 1 caracteres, cada uno máx. 60 caracteres) |

- Paso 2: haz un `PUT` del archivo a `url_subida` con las `cabeceras` que te devolvemos. El archivo va directo al almacenamiento, hasta 50 MB.
- Paso 3: [completa la subida](https://horvell.com/docs/api#completar-subida) para crear el documento.
- `Idempotency-Key` es obligatoria. Si algo falla, repite con la misma clave y el mismo cuerpo: recibirás la misma subida (`200`) con un enlace nuevo, nunca una segunda.
- Tienes 30 minutos desde este paso para completar la subida. El enlace de `url_subida` dura 15.
- Solo claves de despacho con el permiso `documentos:write`. Con el periodo de prueba del despacho caducado, devuelve `402`.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl -X POST "https://api.horvell.com/v1/expedientes/exp_6537a1b2c3d4e5f607182930/subidas" \
  -H "Authorization: Bearer $HORVELL_API_KEY" \
  -H "Idempotency-Key: 9b2f1c4e-3d5a-4e8b-a1f0-6c7d8e9f0a1b" \
  -H "Content-Type: application/json" \
  -d '{
  "nombre_archivo": "informe-pericial.pdf",
  "tipo_contenido": "application/pdf",
  "tamano": 1048576,
  "categoria": "prueba",
  "nombre": "Informe pericial",
  "etiquetas": [
    "perito"
  ]
}'
```

**Respuesta 200**

```json
{
  "object": "subida",
  "id": "sub_6537a1b2c3d4e5f607182932",
  "estado": "pendiente",
  "expediente": "exp_6537a1b2c3d4e5f607182930",
  "url_subida": "https://almacenamiento.horvell.com/subidas/sub_6537a1b2c3d4e5f607182932?firma=…",
  "metodo": "PUT",
  "cabeceras": {
    "content-type": "application/pdf"
  },
  "url_caduca_en": "2026-10-22T10:20:00.000Z",
  "caduca_en": "2026-10-22T10:35:00.000Z",
  "documento": null
}
```

**Errores:** [`parametro_invalido`](https://horvell.com/docs/api#errores-parametro_invalido), [`tipo_no_permitido`](https://horvell.com/docs/api#errores-tipo_no_permitido), [`idempotency_key_requerida`](https://horvell.com/docs/api#errores-idempotency_key_requerida), [`suscripcion_requerida`](https://horvell.com/docs/api#errores-suscripcion_requerida), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_encontrado`](https://horvell.com/docs/api#errores-no_encontrado), [`subida_caducada`](https://horvell.com/docs/api#errores-subida_caducada), [`tipo_contenido_no_soportado`](https://horvell.com/docs/api#errores-tipo_contenido_no_soportado), [`idempotencia_conflicto`](https://horvell.com/docs/api#errores-idempotencia_conflicto), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

### Completar la subida y crear el documento (paso 3 de 3)

`POST /v1/subidas/{id}/completar`

Paso 3 de 3. Comprueba que el archivo ha llegado y crea el documento en el expediente.

**Claves:** Despacho · **Permiso:** `documentos:write`

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | texto | sí | Id de la subida (`sub_…`) que devolvió el paso 1. |

- Es idempotente: repetirlo devuelve el mismo documento (`200`) y nunca crea otro.
- Si respondemos `409 archivo_no_subido`, el `PUT` del paso 2 no llegó a completarse. Repítelo y vuelve aquí.
- El documento aparece al momento en Horvell, con origen `api`.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl -X POST "https://api.horvell.com/v1/subidas/sub_6537a1b2c3d4e5f607182932/completar" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "documento",
  "id": "doc_6537a1b2c3d4e5f607182931",
  "expediente": "exp_6537a1b2c3d4e5f607182930",
  "nombre": "Contestación a la demanda",
  "nombre_archivo": "contestacion-demanda.pdf",
  "tipo_contenido": "application/pdf",
  "tamano": 254331,
  "categoria": "escritos_comunicaciones",
  "etiquetas": [
    "presentado"
  ],
  "origen": null,
  "creado_en": "2026-10-20T16:42:00.000Z",
  "actualizado_en": "2026-10-20T16:42:00.000Z"
}
```

**Errores:** [`id_invalido`](https://horvell.com/docs/api#errores-id_invalido), [`suscripcion_requerida`](https://horvell.com/docs/api#errores-suscripcion_requerida), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`no_encontrado`](https://horvell.com/docs/api#errores-no_encontrado), [`archivo_no_subido`](https://horvell.com/docs/api#errores-archivo_no_subido), [`subida_en_curso`](https://horvell.com/docs/api#errores-subida_en_curso), [`subida_caducada`](https://horvell.com/docs/api#errores-subida_caducada), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

## Eliminados

Lo que ha dejado de existir, para que tu sistema lo borre también.

### Listar lo que ha dejado de existir

`GET /v1/eliminados`

Devuelve lo que ha dejado de existir para tu clave: expedientes y documentos borrados, o que pasaron a otro cliente. Es la otra mitad de cualquier sincronización.

**Claves:** Despacho, Cliente

| Parámetro | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query | entero | no | Elementos por página, de 1 a 100. |
| `cursor` | query | texto | no | `next_cursor` de la página anterior. |
| `updated_since` | query | fecha | no | RFC 3339. Inclusivo: devuelve lo modificado en o después de esta fecha. |
| `tipo` | query | enum | no | Solo expedientes o solo documentos. Valores: `expediente`, `documento`. |

- Aplica primero los eliminados y después los cambios de los listados.
- Borrar un expediente implica borrar todos sus documentos: no recibirás un eliminado por cada uno.
- Se guardan 180 días. Si pides un `updated_since` más antiguo, recibirás `410 sincronizacion_caducada` y tendrás que reconciliar con un listado completo.
- No pide un permiso propio: cualquier clave válida puede consultarlo.

**Ejemplo**

```bash
export HORVELL_API_KEY="hv_tk_TU_CLAVE"

curl "https://api.horvell.com/v1/eliminados?updated_since=2026-10-22T00:00:00Z" \
  -H "Authorization: Bearer $HORVELL_API_KEY"
```

**Respuesta 200**

```json
{
  "object": "list",
  "data": [
    {
      "object": "eliminado",
      "id": "eli_6537a1b2c3d4e5f607182934",
      "tipo": "documento",
      "recurso": "doc_6537a1b2c3d4e5f607182936",
      "expediente": "exp_6537a1b2c3d4e5f607182930",
      "motivo": "eliminado",
      "eliminado_en": "2026-10-22T11:20:00.000Z"
    },
    {
      "object": "eliminado",
      "id": "eli_6537a1b2c3d4e5f607182937",
      "tipo": "expediente",
      "recurso": "exp_6537a1b2c3d4e5f607182935",
      "expediente": null,
      "motivo": "cambio_cliente",
      "eliminado_en": "2026-10-22T12:02:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

**Errores:** [`parametro_invalido`](https://horvell.com/docs/api#errores-parametro_invalido), [`cursor_invalido`](https://horvell.com/docs/api#errores-cursor_invalido), [`permiso_insuficiente`](https://horvell.com/docs/api#errores-permiso_insuficiente), [`sincronizacion_caducada`](https://horvell.com/docs/api#errores-sincronizacion_caducada), [`no_autenticado`](https://horvell.com/docs/api#errores-no_autenticado), [`tenant_suspendido`](https://horvell.com/docs/api#errores-tenant_suspendido), [`tenant_eliminado`](https://horvell.com/docs/api#errores-tenant_eliminado), [`demasiadas_peticiones`](https://horvell.com/docs/api#errores-demasiadas_peticiones), [`error_interno`](https://horvell.com/docs/api#errores-error_interno), [`mantenimiento`](https://horvell.com/docs/api#errores-mantenimiento)

## Objetos

### Expediente

Un asunto del despacho, con sus procedimientos, señalamientos e intervinientes.

| Campo | Tipo | Siempre presente | Descripción |
| --- | --- | --- | --- |
| `object` | texto | sí | Siempre `expediente`. |
| `id` | texto | sí | Identificador del expediente en la API. Estable: no cambia nunca. (formato exp_…) |
| `numero` | texto | sí | Número del expediente en el despacho, el que ve el equipo en Horvell. |
| `titulo` | texto | sí | Título del expediente. |
| `referencia_externa` | texto \| null | sí | Código del expediente en el sistema del que se importó (p. ej. Kmaleon). `null` si se creó en Horvell. |
| `referencias` | objeto | sí | Referencias del cliente para este asunto (su número de préstamo, de siniestro, de cartera…), tal como las lleva el despacho. Puedes buscar por cualquiera de ellas con el filtro `referencia`. |
| `referencias.referencia_1` | texto \| null | sí | Referencia 1 del cliente. `null` si no tiene. |
| `referencias.referencia_2` | texto \| null | sí | Referencia 2 del cliente. `null` si no tiene. |
| `referencias.referencia_3` | texto \| null | sí | Referencia 3 del cliente. `null` si no tiene. |
| `tipo` | enum | sí | Área del derecho del expediente. Valores: `mercantil_societario`, `civil_contractual`, `procesal_litigacion`, `penal`, `laboral_seguridad_social`, `administrativo_contencioso`, `fiscal_tributario`, `concursal_reestructuracion`, `familia`, `sucesiones`, `inmobiliario_urbanismo`, `bancario_financiero`, `propiedad_intelectual_industrial`, `proteccion_datos_privacidad`, `compliance`, `extranjeria_nacionalidad`, `notarial_registral`, `otro`. |
| `estado` | enum | sí | Estado de trabajo del expediente en el despacho. Valores: `nuevo`, `en_estudio`, `en_curso`, `pendiente_cliente`, `pendiente_tercero`, `en_pausa`, `cerrado`, `archivado`. |
| `situacion_procesal` | texto \| null | sí | Situación procesal en texto libre, tal como la anota el despacho. `null` si no hay. |
| `cuantia` | objeto \| null | sí | Cuantía del asunto. `null` si no se ha indicado. |
| `cuantia.tipo` | enum | sí | Si la cuantía está fijada o no. Valores: `determinada`, `indeterminada`. |
| `cuantia.importe` | número \| null | sí | Importe en euros. `null` si la cuantía es indeterminada. |
| `etiquetas` | lista de texto | sí | Etiquetas que el despacho ha puesto al expediente. |
| `cliente` | objeto | sí | Cliente del despacho al que pertenece el expediente. |
| `cliente.id` | texto | sí | Identificador del cliente (`cli_…`). (formato cli_…) |
| `cliente.nombre` | texto | sí | Nombre o razón social del cliente. |
| `responsable` | objeto \| null | sí | Persona del despacho que lleva el expediente. `null` si no hay. |
| `responsable.nombre` | texto | sí | Nombre del abogado o abogada. |
| `procedimientos` | lista de objetos | sí | Procedimientos judiciales o administrativos del expediente. Puede estar vacío. |
| `procedimientos[].organismo` | texto \| null | sí | Juzgado u organismo. |
| `procedimientos[].num_organismo` | texto \| null | sí | Número del juzgado u organismo. |
| `procedimientos[].poblacion` | texto \| null | sí | Población del juzgado u organismo. |
| `procedimientos[].tipo_procedimiento` | texto \| null | sí | Tipo de procedimiento (juicio verbal, ordinario…). |
| `procedimientos[].numero_procedimiento` | texto \| null | sí | Número de autos, p. ej. `1234/2026`. |
| `procedimientos[].nig` | texto \| null | sí | Número de Identificación General del procedimiento. |
| `procedimientos[].fecha_alta` | fecha \| null | sí | Fecha de alta del procedimiento. |
| `senalamientos` | lista de objetos | sí | Juicios, vistas, plazos y demás señalamientos del expediente. |
| `senalamientos[].tipo` | enum | sí | Tipo de señalamiento. Valores: `juicio`, `vista`, `comparecencia`, `audiencia_previa`, `declaracion`, `plazo`, `otro`. |
| `senalamientos[].estado` | enum | sí | Estado del señalamiento. Valores: `senalado`, `suspendido`, `celebrado`, `cancelado`. |
| `senalamientos[].titulo` | texto | sí | Título del señalamiento. |
| `senalamientos[].fecha_inicio` | fecha | sí | Inicio, en UTC. |
| `senalamientos[].fecha_fin` | fecha \| null | sí | Fin, en UTC. `null` si no tiene. |
| `senalamientos[].zona_horaria` | texto | sí | Zona horaria IANA en la que se fijó. |
| `senalamientos[].ubicacion` | texto \| null | sí | Sala, sede o dirección. `null` si no consta. |
| `intervinientes` | lista de objetos | sí | Partes y profesionales que intervienen. |
| `intervinientes[].nombre` | texto | sí | Nombre de la persona o entidad. |
| `intervinientes[].rol` | texto \| null | sí | Su papel en el asunto (demandado, procurador…). |
| `creado_en` | fecha | sí | Fecha de alta del expediente, en UTC. |
| `actualizado_en` | fecha | sí | Última modificación de los datos propios del expediente, en UTC. Un cambio en sus señalamientos no la mueve, pero sí lo hace volver a salir con `updated_since`. |

```json
{
  "object": "expediente",
  "id": "exp_6537a1b2c3d4e5f607182930",
  "numero": "EXP-2026-0142",
  "titulo": "Reclamación de cantidad · Transportes del Turia",
  "referencia_externa": "457906",
  "referencias": {
    "referencia_1": "PRE-2019-004512",
    "referencia_2": "CART-2026-118",
    "referencia_3": null
  },
  "tipo": "procesal_litigacion",
  "estado": "en_curso",
  "situacion_procesal": "Contestación a la demanda presentada",
  "cuantia": {
    "tipo": "determinada",
    "importe": 18450.75
  },
  "etiquetas": [
    "recobro",
    "prioritario"
  ],
  "cliente": {
    "id": "cli_6537a1b2c3d4e5f607182933",
    "nombre": "Aseguradora Ejemplo, S.A."
  },
  "responsable": {
    "nombre": "Laura Martín"
  },
  "procedimientos": [
    {
      "organismo": "Juzgado de Primera Instancia",
      "num_organismo": "4",
      "poblacion": "Valencia",
      "tipo_procedimiento": "Juicio verbal",
      "numero_procedimiento": "1234/2026",
      "nig": "4625042120260012345",
      "fecha_alta": "2026-03-02T00:00:00.000Z"
    }
  ],
  "senalamientos": [
    {
      "tipo": "vista",
      "estado": "senalado",
      "titulo": "Vista del juicio verbal",
      "fecha_inicio": "2026-11-18T09:30:00.000Z",
      "fecha_fin": "2026-11-18T10:30:00.000Z",
      "zona_horaria": "Europe/Madrid",
      "ubicacion": "Ciudad de la Justicia, sala 12"
    }
  ],
  "intervinientes": [
    {
      "nombre": "Transportes Ficticios del Turia, S.L.",
      "rol": "Demandado"
    },
    {
      "nombre": "Ana Pérez Gil",
      "rol": "Procuradora"
    }
  ],
  "creado_en": "2026-02-27T08:15:00.000Z",
  "actualizado_en": "2026-10-22T09:30:00.000Z"
}
```

**Valores de `tipo`**

- `mercantil_societario` — Mercantil / Societario
- `civil_contractual` — Civil / Contractual
- `procesal_litigacion` — Procesal / Litigación
- `penal` — Penal
- `laboral_seguridad_social` — Laboral / Seguridad Social
- `administrativo_contencioso` — Administrativo / Contencioso
- `fiscal_tributario` — Fiscal / Tributario
- `concursal_reestructuracion` — Concursal / Reestructuración
- `familia` — Familia
- `sucesiones` — Sucesiones
- `inmobiliario_urbanismo` — Inmobiliario / Urbanismo
- `bancario_financiero` — Bancario / Financiero
- `propiedad_intelectual_industrial` — Propiedad Intelectual / Industrial
- `proteccion_datos_privacidad` — Protección de Datos / Privacidad
- `compliance` — Compliance
- `extranjeria_nacionalidad` — Extranjería / Nacionalidad
- `notarial_registral` — Notarial / Registral
- `otro` — Otro

**Valores de `estado`**

- `nuevo` — Nuevo. Recién abierto; todavía no se ha empezado a trabajar.
- `en_estudio` — En estudio. El despacho está analizando el asunto antes de actuar.
- `en_curso` — En curso. En tramitación activa.
- `pendiente_cliente` — Pendiente cliente. Esperando algo del cliente: documentación, una decisión, una firma…
- `pendiente_tercero` — Pendiente tercero. Esperando a un tercero: juzgado, notaría, registro, contraparte…
- `en_pausa` — En pausa. Parado de forma temporal por decisión del despacho o del cliente.
- `cerrado` — Cerrado. Terminado. Se puede reabrir.
- `archivado` — Archivado. Cerrado y archivado de forma definitiva.

**Valores de `cuantia.tipo`**

- `determinada` — Determinada. Hay un importe concreto en `importe`.
- `indeterminada` — Indeterminada. No se puede fijar; `importe` es `null`.

**Valores de `senalamientos[].tipo`**

- `juicio` — Juicio
- `vista` — Vista
- `comparecencia` — Comparecencia
- `audiencia_previa` — Audiencia previa
- `declaracion` — Declaración
- `plazo` — Plazo. Vencimiento de un plazo procesal o administrativo.
- `otro` — Otro

**Valores de `senalamientos[].estado`**

- `senalado` — Señalado. Fijado y pendiente de celebrarse.
- `suspendido` — Suspendido. Aplazado; puede volver a señalarse con otra fecha.
- `celebrado` — Celebrado. Ya tuvo lugar.
- `cancelado` — Cancelado. No se celebrará.

### Documento

Un archivo de un expediente. El contenido se descarga aparte.

| Campo | Tipo | Siempre presente | Descripción |
| --- | --- | --- | --- |
| `object` | texto | sí | Siempre `documento`. |
| `id` | texto | sí | Identificador del documento en la API. Estable. (formato doc_…) |
| `expediente` | texto | sí | Expediente al que pertenece. (formato exp_…) |
| `nombre` | texto | sí | Nombre visible del documento en Horvell. |
| `nombre_archivo` | texto | sí | Nombre del archivo, con su extensión. |
| `tipo_contenido` | texto | sí | Tipo MIME del archivo. |
| `tamano` | entero | sí | Tamaño del archivo en bytes. |
| `categoria` | enum | sí | Categoría del documento en el expediente. Valores: `identificacion`, `documentacion_cliente`, `contratos_escrituras`, `escritos_comunicaciones`, `procesal_administrativo`, `prueba`, `facturacion_economico`, `otro`. |
| `etiquetas` | lista de texto | sí | Etiquetas del documento. |
| `origen` | enum \| null | sí | Vía automática por la que entró; `null` = subido a mano. Valores: `resend`, `outlook-addin`, `ingesta`, `api`. |
| `creado_en` | fecha | sí | Fecha de alta del documento, en UTC. |
| `actualizado_en` | fecha | sí | Última modificación del documento, en UTC. |

```json
{
  "object": "documento",
  "id": "doc_6537a1b2c3d4e5f607182931",
  "expediente": "exp_6537a1b2c3d4e5f607182930",
  "nombre": "Contestación a la demanda",
  "nombre_archivo": "contestacion-demanda.pdf",
  "tipo_contenido": "application/pdf",
  "tamano": 254331,
  "categoria": "escritos_comunicaciones",
  "etiquetas": [
    "presentado"
  ],
  "origen": null,
  "creado_en": "2026-10-20T16:42:00.000Z",
  "actualizado_en": "2026-10-20T16:42:00.000Z"
}
```

**Valores de `categoria`**

- `identificacion` — Identificación. DNI, NIE, pasaporte, escrituras de poder…
- `documentacion_cliente` — Documentación cliente. Lo que aporta el cliente para el asunto.
- `contratos_escrituras` — Contratos / Escrituras. Contratos, escrituras y documentos notariales.
- `escritos_comunicaciones` — Escritos / Comunicaciones. Escritos presentados, correos y comunicaciones.
- `procesal_administrativo` — Procesal / Administrativo. Resoluciones, notificaciones y actos del juzgado o la Administración.
- `prueba` — Prueba. Pruebas e informes periciales.
- `facturacion_economico` — Facturación / Económico. Justificantes de pago, provisiones y documentos económicos.
- `otro` — Otro. Cualquier otro documento.

**Valores de `origen`**

- `resend` — Correo entrante. Llegó por correo al buzón del despacho en Horvell.
- `outlook-addin` — Outlook. Se guardó desde el complemento de Outlook.
- `ingesta` — Importación. Entró al importar una carpeta de expedientes.
- `api` — API. Lo subió un integrador con una clave de despacho.

### Subida

Una subida de documento en curso (solo claves de despacho).

| Campo | Tipo | Siempre presente | Descripción |
| --- | --- | --- | --- |
| `object` | texto | sí | Siempre `subida`. |
| `id` | texto | sí | Identificador de la subida. Lo necesitas para completarla. (formato sub_…) |
| `estado` | enum | sí | En qué punto está la subida. Valores: `pendiente`, `completando`, `completada`, `caducada`. |
| `expediente` | texto | sí | Expediente en el que se creará el documento. (formato exp_…) |
| `url_subida` | texto \| null | sí | URL firmada para hacer `PUT` del archivo. `null` si ya se completó. |
| `metodo` | texto | sí | Método HTTP para enviar el archivo: siempre `PUT`. |
| `cabeceras` | objeto | sí | Cabeceras obligatorias en el `PUT` (el `content-type` debe coincidir). |
| `url_caduca_en` | fecha \| null | sí | Hasta cuándo vale `url_subida` (15 minutos). Si caduca, repite el paso 1 con la misma `Idempotency-Key` y recibirás una nueva. |
| `caduca_en` | fecha | sí | Después de esta fecha (30 minutos desde el paso 1) la subida ya no se puede completar. |
| `documento` | texto \| null | sí | Documento creado al completar. `null` hasta entonces. (formato doc_…) |

```json
{
  "object": "subida",
  "id": "sub_6537a1b2c3d4e5f607182932",
  "estado": "pendiente",
  "expediente": "exp_6537a1b2c3d4e5f607182930",
  "url_subida": "https://almacenamiento.horvell.com/subidas/sub_6537a1b2c3d4e5f607182932?firma=…",
  "metodo": "PUT",
  "cabeceras": {
    "content-type": "application/pdf"
  },
  "url_caduca_en": "2026-10-22T10:20:00.000Z",
  "caduca_en": "2026-10-22T10:35:00.000Z",
  "documento": null
}
```

**Valores de `estado`**

- `pendiente` — Pendiente. Esperando el archivo (paso 2) y la confirmación (paso 3).
- `completando` — Completando. Se está creando el documento. Repite el paso 3 en unos segundos.
- `completada` — Completada. El documento ya existe; está en `documento`.
- `caducada` — Caducada. Pasaron los 30 minutos sin completarla. Empieza otra con una `Idempotency-Key` nueva.

### Eliminado

Algo que ha dejado de existir para tu clave.

| Campo | Tipo | Siempre presente | Descripción |
| --- | --- | --- | --- |
| `object` | texto | sí | Siempre `eliminado`. |
| `id` | texto | sí | Identificador del registro de eliminación (`eli_…`). (formato eli_…) |
| `tipo` | enum | sí | Qué dejó de existir. Valores: `expediente`, `documento`. |
| `recurso` | texto | sí | Id del expediente (`exp_…`) o documento (`doc_…`) que ya no existe para esta clave. |
| `expediente` | texto \| null | sí | Solo documentos: expediente al que pertenecía. (formato exp_…) |
| `motivo` | enum | sí | `eliminado`: se borró. `cambio_cliente`: pasó a otro cliente (solo lo ven las claves del cliente anterior). Valores: `eliminado`, `cambio_cliente`. |
| `eliminado_en` | fecha | sí | Cuándo dejó de existir para esta clave, en UTC. |

```json
{
  "object": "eliminado",
  "id": "eli_6537a1b2c3d4e5f607182934",
  "tipo": "documento",
  "recurso": "doc_6537a1b2c3d4e5f607182936",
  "expediente": "exp_6537a1b2c3d4e5f607182930",
  "motivo": "eliminado",
  "eliminado_en": "2026-10-22T11:20:00.000Z"
}
```

**Valores de `tipo`**

- `expediente` — Expediente. Bórralo junto con todos sus documentos.
- `documento` — Documento. Borra solo ese documento.

**Valores de `motivo`**

- `eliminado` — Eliminado. Se borró en Horvell.
- `cambio_cliente` — Cambio de cliente. Pasó a otro cliente. Solo lo reciben las claves del cliente anterior, que ya no deben verlo.

### Error

El cuerpo de cualquier respuesta de error (RFC 9457, `application/problem+json`).

| Campo | Tipo | Siempre presente | Descripción |
| --- | --- | --- | --- |
| `type` | texto | sí | Enlace a la explicación de este error en la documentación. |
| `title` | texto | sí | Título corto del error. |
| `status` | entero | sí | El mismo código HTTP de la respuesta. |
| `detail` | texto | no | Explicación concreta de este caso, cuando la hay. |
| `code` | texto | sí | Código estable del error. Usa este campo, no `title`, para decidir qué hacer. |
| `request_id` | texto | sí | Identificador de la petición. Inclúyelo si nos escribes. |
| `errors` | lista de objetos | no | Detalle por campo en errores de validación. |
| `errors[].campo` | texto | sí | Campo con el problema, p. ej. `limit`. |
| `errors[].mensaje` | texto | sí | Qué le pasa. |

```json
{
  "type": "https://api.horvell.com/v1/docs#errores-no_encontrado",
  "title": "No encontrado",
  "status": 404,
  "code": "no_encontrado",
  "request_id": "req_q3Zl8vY1cN0kT5bW"
}
```

## Errores

Todas las respuestas de error usan `application/problem+json` (RFC 9457). Decide qué hacer según `code`, nunca según el texto.

| code | HTTP | Cuándo pasa | Qué hacer |
| --- | --- | --- | --- |
| `parametro_invalido` | 400 | Un parámetro, una cabecera o el cuerpo no cumple el formato. `errors` lista cada campo con su motivo. | Corrige los campos que indica `errors` y repite. |
| `parametro_no_permitido` | 400 | Has enviado un parámetro que tu tipo de clave no admite. P. ej. `cliente` con una clave de cliente. | Quita el parámetro: una clave de cliente ya está limitada a su cliente. |
| `cursor_invalido` | 400 | El `cursor` está manipulado, caducado o lo pides con filtros distintos a los de la página anterior. | Vuelve a empezar el listado sin `cursor`, con los mismos filtros en todas las páginas. |
| `id_invalido` | 400 | El identificador no tiene el formato de su tipo (`exp_…`, `doc_…`, `sub_…`). | Usa el `id` tal como lo devuelve la API, sin modificarlo. |
| `tipo_no_permitido` | 400 | El tipo de archivo de la subida no está entre los admitidos. | Convierte el archivo a un formato admitido (PDF, Word, Excel, imagen, correo…) o revisa `tipo_contenido`. |
| `idempotency_key_requerida` | 400 | Falta la cabecera `Idempotency-Key` al iniciar una subida. Hoy la API lo devuelve como `parametro_invalido` con el campo `idempotency-key`; trata los dos igual. | Envía un valor único por documento, p. ej. un UUID. |
| `no_autenticado` | 401 | Falta la cabecera `Authorization`, o la clave no existe, está revocada o ha caducado. | Comprueba la clave. Si la revocaron o caducó, pide una nueva al despacho. |
| `suscripcion_requerida` | 402 | El periodo de prueba del despacho ha terminado. Las lecturas siguen funcionando; las escrituras no. | Avisa al despacho. Las subidas volverán a funcionar cuando active su suscripción. |
| `permiso_insuficiente` | 403 | La clave no tiene el permiso que pide la llamada, o es una clave de cliente intentando escribir. | Pide al despacho una clave con ese permiso. Las claves de cliente son siempre de solo lectura. |
| `tenant_suspendido` | 403 | El despacho tiene la cuenta suspendida o pendiente de activar. | Contacta con el despacho. La API volverá a responder cuando reactive su cuenta. |
| `no_encontrado` | 404 | El recurso no existe o tu clave no tiene acceso a él. La API no distingue los dos casos a propósito. | Revisa el `id`. Si sincronizas, puede haberse eliminado: consulta `/v1/eliminados`. |
| `archivo_no_subido` | 409 | Intentas completar una subida, pero el archivo no ha llegado al almacenamiento. | Haz el `PUT` del paso 2 (comprueba que respondió 200) y repite el paso 3. |
| `subida_en_curso` | 409 | Otra petición está completando esta misma subida ahora mismo. | Espera unos segundos y repite el paso 3: recibirás el mismo documento. |
| `subida_caducada` | 410 | Han pasado más de 30 minutos desde el paso 1 sin completar la subida. | Empieza otra subida con una `Idempotency-Key` nueva. |
| `sincronizacion_caducada` | 410 | Has pedido eliminados con un `updated_since` de hace más de 180 días, que ya no se conservan. | Haz una reconciliación completa: recorre los listados sin `updated_since` y borra lo que ya no aparezca. |
| `tenant_eliminado` | 410 | El despacho ha dado de baja su cuenta. | La clave no volverá a funcionar. Deja de sincronizar. |
| `tipo_contenido_no_soportado` | 415 | El cuerpo de la petición no es `application/json`. | Envía el cuerpo como JSON con la cabecera `Content-Type: application/json`. |
| `idempotencia_conflicto` | 422 | Has reutilizado una `Idempotency-Key` con un cuerpo distinto. | Usa una clave nueva para cada documento distinto. Repite con la misma clave solo si el cuerpo es idéntico. |
| `demasiadas_peticiones` | 429 | Has superado el límite por minuto de tu clave, o el de tu IP. | Espera los segundos que indica `Retry-After` y repite. Reparte las peticiones en el tiempo. |
| `error_interno` | 500 | Algo ha fallado de nuestro lado. | Repite pasados unos segundos. Si persiste, escríbenos con el `request_id`. |
| `error_almacenamiento` | 503 | El almacenamiento de archivos no responde en este momento. | Repite pasados unos segundos. |
| `mantenimiento` | 503 | La API está en mantenimiento programado. | Espera los segundos que indica `Retry-After` y repite. |

## Registro de cambios

### 1.1.0 · 23 de octubre de 2026

- Nuevo [`GET /v1/documentos`](#listar-todos-documentos): los documentos de todos los expedientes de la clave, con `updated_since`. Sincronizar documentos ya no exige una llamada por expediente.

### 1.0.0 · 23 de octubre de 2026

- Primera versión pública: expedientes, documentos, descarga, subida en tres pasos y eliminados.
- Claves de despacho con permisos por recurso y claves de cliente de solo lectura.
- Las referencias del cliente (`referencias`) en el expediente y el filtro `referencia` para buscar por ellas.
