LogiSign.

Guía de integración

API para conectar las aplicaciones de tu empresa.

# Integración de aplicaciones

Base URL stage: `https://logisign.ilstg.com`. Producción: `https://logisign.ilprod.com`. OpenAPI: `/openapi.json`. El esquema no contiene credenciales ni documentos.

El backend de cada aplicación usa `Authorization: Bearer <API_KEY>`. Crear una credencial por aplicación desde la consola administrativa. No exponer esta clave en el navegador ni incluirla en parámetros de URL. No hay CORS público: las solicitudes administrativas son entre servidores.

## Crear una solicitud

`POST /api/v1/envelopes`, multipart con `file` (un PDF) o el campo repetido `files` (varios PDF), y `metadata` (JSON). No enviar `file` y `files` juntos. Incluir `Idempotency-Key` de 8 a 100 caracteres, conservada al reintentar la misma operación.

```python
import os
import uuid
import json
import httpx

base = 'https://logisign.ilstg.com'
headers = {'Authorization': 'Bearer ' + os.environ['LOGISIGN_API_KEY']}
# Persistir esta clave junto a la operación de negocio antes de hacer la petición.
operation_key = str(uuid.uuid4())
metadata = {
    'title': 'Acuerdo de confidencialidad',
    'name': 'Nombre del firmante',
    'email': 'firmante@empresa.com',
    'reference': 'CONTRATO-001',
    'expires_in_days': 7,
    'send_email': True,
    'brand_stamp': True,
    'identity_mode': 'none',  # none | photos | liveness (verificación automática)
    'placements': [
        {'page': 1, 'x': 0.10, 'y': 0.75, 'width': 0.35, 'height': 0.12}
    ]
}
with open('contrato.pdf', 'rb') as document:
    response = httpx.post(
        base + '/api/v1/envelopes',
        headers={**headers, 'Idempotency-Key': operation_key},
        files={'file': ('contrato.pdf', document, 'application/pdf')},
        data={'metadata': json.dumps(metadata)}, timeout=90,
    )
response.raise_for_status()
request = response.json()
# Guardar request['id']. Abrir request['signing_url'] en el navegador del firmante.
```

Respuesta `201`: `id`, `status: pending`, `signing_url`, `original_hash`, fechas y datos del firmante. En una repetición idéntica se devuelve la misma solicitud con `replayed: true` y `signing_url: null`. La respuesta de creación solo incluye la URL la primera vez; se puede recuperar después con el endpoint de enlace indicado al final. Con `send_email: true` la invitación queda en la cola transaccional de la creación original. Si cambia el contenido con la misma clave, respuesta `409`.

Entre uno y cinco PDF por solicitud, máximo 5 MiB y 40 páginas en total. PDF estático: no admite contraseñas, formularios interactivos, JavaScript, archivos incrustados ni firmas digitales existentes. La solicitud original se conserva inmutable. El correo de un firmante pendiente puede corregirse con el endpoint documentado al final, dejando historial; cambiar su nombre requiere cancelar y crear otra solicitud.

### Coordenadas

`page` comienza en 1. `x`, `y`, `width`, `height` son fracciones de 0 a 1 del área visible de la página después de aplicar su rotación. El origen está arriba a la izquierda. El backend exige un tamaño mínimo de 100 × 55 puntos PDF y que la caja quede dentro de la página. Se pueden definir hasta 20 posiciones para el mismo firmante, en distintas páginas.

La consola permite seleccionar página, arrastrar la caja y ajustar el tamaño desde cualquiera de sus cuatro esquinas, también con pantalla táctil. `placements` es obligatorio para nuevas solicitudes: al menos una posición por PDF (no por página). La API rechaza con 422 un conjunto que omita posiciones en alguno de sus documentos. Las posiciones solicitadas se conservan como referencia, pero el editor no añade firmas automáticamente: el firmante puede pulsar «Firmar aquí» en las posiciones indicadas o colocar y ajustar sus marcas expresamente en las páginas que elija. Puede arrastrarlas, ajustar su tamaño y añadir iniciales. El expediente conserva las posiciones solicitadas y las finalmente confirmadas, sin reescribir la solicitud original.

## Abrir la pantalla de firma

Redirigir o abrir una ventana nueva con `signing_url`. El token está en el fragmento `#` y no se transmite como URL al servidor ni aparece en los access logs. La página lo toma en memoria y limpia la barra de direcciones. Al recargar, volver a abrir la invitación original.

También se puede incrustar la URL en un iframe si el administrador agrega el origen concreto a `FRAME_ANCESTORS`, por ejemplo `'self' https://portal.ilprod.com`. Configurar solo los orígenes necesarios. Nunca pasar la clave de aplicación al iframe. La finalización se confirma desde el backend de la aplicación consultando el estado; no se confía en eventos del navegador para considerar firmado un documento.

## Consultar el resultado y recuperar el PDF

```python
result = httpx.get(base + '/api/v1/envelopes/' + request['id'], headers=headers).json()
if result['ok'] and result['status'] == 'signed':
    signed = httpx.get(
        base + '/api/v1/envelopes/' + request['id'] + '/files/signed', headers=headers
    )
    signed.raise_for_status()
    # Guardar signed.content y result['signed_hash'] en la aplicación de origen.
```

Consultar cada 5–15 segundos mientras el flujo está abierto. No hay webhooks en esta primera versión. `ok: true` en la consulta significa que el PDF final y las evidencias quedaron guardados; no significa que el correo llegó a la bandeja de entrada.

| Operación | Método y ruta |
|---|---|
| Listar últimas 100 solicitudes | `GET /api/v1/envelopes?offset=0` |
| Consultar estado y envíos | `GET /api/v1/envelopes/{id}` |
| Cancelar pendiente | `POST /api/v1/envelopes/{id}/cancel` |
| Descargar original | `GET /api/v1/envelopes/{id}/files/original` |
| Descargar firmado | `GET /api/v1/envelopes/{id}/files/signed` |
| Descargar evidencia PDF | `GET /api/v1/envelopes/{id}/files/certificate` |
| Descargar manifiesto | `GET /api/v1/envelopes/{id}/files/evidence` |
| Descargar expediente completo | `GET /api/v1/envelopes/{id}/files/archive` |
| Reintentar correos fallidos | `POST /api/v1/envelopes/{id}/mail/retry` |
| Clave pública y huella | `GET /api/v1/public-key` |
| Estado del servicio | `GET /healthz` |

Los estados de solicitud son `pending`, `signed`, `cancelled` y `expired`. Los envíos son `pending`, `accepted`, `failed` y `obsolete`. `accepted` indica aceptación de LogiMail, sin confirmar entrega. Los reintentos tienen espera creciente y máximo ocho intentos; puede haber duplicados si el proveedor acepta y luego se pierde la respuesta. Nunca se repite la firma por un reintento de correo.

Errores: `401` credencial o enlace inválido; `403` acceso prohibido o código incorrecto; `404` no encontrado o de otra aplicación; `409` conflicto/estado/integridad; `410` enlace vencido; `413` tamaño; `422` datos inválidos; `429` límite de intentos. Una falla de red exige consultar estado antes de repetir operaciones.


## Flujo guiado de firma (v2)

La pantalla pública separa seis pasos: **Elige → Crea → Ubica → Revisa → Valida → Tu copia**. Ofrece dibujo manuscrito, texto con fuentes Allura/Great Vibes/Caveat autohospedadas. La pantalla no permite cargar una imagen de firma. La firma y las iniciales se renderizan sobre el documento y se pueden arrastrar y redimensionar. Se admiten hasta 100 marcas, incluyendo al menos una firma completa. Las iniciales se pueden colocar en una esquina de todas las páginas.

La API de creación de solicitudes sigue siendo compatible. Los siguientes endpoints pertenecen a la sesión personal del firmante (Bearer del enlace, no clave de la aplicación):

1. `GET /api/v1/signing/pages/{page}`: abrir todas las páginas antes de preparar.
2. `POST /api/v1/signing/prepare`: enviar `original_hash`, `signature` (PNG data URL), `initials` (PNG opcional) y `placements`. Cada posición usa coordenadas normalizadas y `kind: signature|initials`. Tamaño mínimo 18 × 10 puntos. Devuelve `draft_id`, `pdf_hash` y vencimiento de 30 minutos.
3. `GET /api/v1/signing/preview/{draft_id}/pages/{page}`: mostrar la vista previa exacta del PDF preparado. La apariencia del PDF final no incluye la caja de selección, tiradores ni leyendas del editor.
4. `POST /api/v1/signing/otp` con `{ "draft_id": "..." }`: solicitar el código para ese documento preparado.
5. `POST /api/v1/signing/complete`: enviar `draft_id`, `code`, `consent: true`, `consent_version: "1.0"` y `original_hash`; no reenviar `signature`. El PDF final son exactamente los bytes que se prepararon, sin volver a generarlo.

Preparar otra versión invalida el código anterior. El código queda vinculado al `draft_id`; no se puede cambiar una posición y reutilizar la validación. El flujo anterior sin borrador sigue disponible para integraciones existentes que aún no han preparado una vista previa, con sus posiciones originales.

Los nuevos expedientes usan `logisign-evidence-v2`, con las posiciones definitivas, las posiciones iniciales solicitadas y, cuando se usan, `iniciales.png` y su hash. El verificador conserva compatibilidad con v1 y v2. La migración de datos es aditiva y conserva originales y expedientes históricos.

## Revisión continua y avisos al solicitante

Las páginas se muestran una debajo de otra. `GET /api/v1/signing/pages/{page}?blurred=true` entrega una miniatura desenfocada, sin registrar apertura. El botón con el ojo solicita la página legible y registra `page_served`. El servidor exige apertura de todas las páginas antes de preparar o firmar. Esto acredita una solicitud de visualización; no prueba lectura, comprensión ni quién miró la pantalla.

`GET /api/v1/signing/files/original` permite al firmante descargar el original sin modificar durante la vigencia de una solicitud no cancelada. La descarga no sustituye la apertura individual de las páginas. Antes de firmar, la primera petición de descarga añade `original_download_requested` al expediente. No se modifica el manifiesto ya sellado por descargas posteriores.

La creación admite `requester_email` opcional: dirección del solicitante que recibirá los avisos. Las aplicaciones de confianza deben proporcionarla desde su backend. Cuando la solicitud se crea con una sesión de usuario, el servidor toma el correo de esa cuenta y descarta cualquier dirección enviada por el formulario. Para solicitudes anteriores sin este campo no se inventa ni se cambia el destinatario histórico.

Se encolan una sola vez por solicitud los avisos `opened` (primera página legible solicitada), `downloaded_original` (primera copia sin firmar solicitada) y `completed_requester` (firma finalizada). El último incluye el PDF firmado y el certificado del proceso. El firmante sigue recibiendo su propia copia (`completed`). No se usan píxeles de seguimiento de correo. Una petición de descarga no prueba que la persona haya guardado el archivo.

## Usuarios de la plataforma

Cada cuenta pertenece a un espacio de cliente. Su administrador invita solicitantes; cada solicitante ve sus documentos y el administrador puede consultar todos los de ese espacio. Las credenciales de aplicaciones conservan su aislamiento.

El navegador inicia sesión con correo y contraseña (`POST /api/v1/auth/login`) y recibe una cookie HttpOnly, Secure bajo HTTPS, SameSite Strict, con vigencia de 8 horas. Las mutaciones con sesión exigen `X-LogiSign-Request: 1` y origen propio. Las contraseñas se guardan con scrypt y salt aleatorio; sesiones y enlaces se guardan como hashes. No hay contraseñas ni sesiones en localStorage.

`GET /auth/me`, `POST /auth/logout`, `POST /auth/password`, `POST /auth/forgot` y `POST /auth/reset` bajo `/api/v1` gestionan el acceso. El cambio y la recuperación revocan todas las sesiones anteriores. La contraseña temporal no permite usar la aplicación hasta cambiarla. Mínimo 12 caracteres para la contraseña elegida. La recuperación responde igual para correos registrados y desconocidos; los límites protegen el inicio de sesión y los envíos.

El administrador usa `GET /users`, `POST /users` (nombre y correo), `POST /users/{id}/invite` para reenviar invitación/recuperación y `DELETE /users/{id}` para desactivar un solicitante. Invitaciones de 24 horas y recuperación de 30 minutos, enlaces de un solo uso; pedir otro invalida el anterior. El envío usa la misma conexión LogiMail y una cola cifrada persistente, con reintentos.

Todos los correos usan una plantilla HTML con la identidad visual de LogiSign y alternativa de texto para clientes que la necesiten. Las invitaciones administrativas, incluidas las del primer administrador, usan un enlace para establecer la contraseña. No se generan contraseñas para enviarlas por correo; cualquier mensaje antiguo de tipo `temporary` aún en cola se descarta. Al emitir un enlace a una cuenta pendiente de activar, se inutiliza su contraseña temporal anterior y se revocan sus sesiones. Las cuentas ya activadas conservan su contraseña hasta completar el restablecimiento.

## Estampa y cámara opcionales (v3)

`brand_stamp` es falso por defecto en API; la consola lo propone activado. Incluye el emblema en la firma completa; las iniciales mantienen su aspecto simple. Se conserva el logo exacto de la solicitud, con hash sellado. La firma PNG enviada a `prepare` debe ser el trazo original, sin estampa: el servidor compone la estampa una vez.

`identity_mode`: `none` por defecto, `photos` para tres capturas guiadas, `liveness` para añadir el clasificador interno de presentaciones con fotos o pantallas. Crear `liveness` sin servicio configurado devuelve 422. Estos campos son inmutables. Ver [CAMARA.md](CAMARA.md) para alcance, privacidad y configuración.

Con cámara, después de preparar el PDF y antes del OTP, el firmante autoriza el tratamiento y utiliza:

1. `POST /api/v1/signing/photos/start` con `draft_id`, `consent: true`, `consent_version: "photos-1.0"`. Responde `session_id`, `nonce`, `pose`, `index`, `expires`.
2. `POST /api/v1/signing/photos/capture` con `session_id`, `nonce`, `pose` y `image` como JPEG data URL. La captura debe seguir la pose solicitada. Responde el siguiente desafío o `complete: true`. Los desafíos son de un solo uso; no se permite reenviar exactamente la misma imagen. El backend normaliza a máximo 640×640 y no acepta cargas fuera de los límites.
3. Completar las tres capturas antes de solicitar OTP. Si cambia el borrador, repetir las capturas; si se reinician, pedir un código nuevo. La caída o el rechazo del clasificador no permite continuar sin la comprobación.

La pantalla agrega el paso **Fotos** después del checklist de **Revisa**. Para iframe con cámara, además de `FRAME_ANCESTORS`, configurar `allow="camera"` y delegar el permiso desde el sitio que incrusta; abrir una pestaña propia es la opción más compatible.

Con un PDF y estampa o fotos se genera `logisign-evidence-v3`; con varios PDF se genera v4, incluyendo también estas evidencias opcionales. El ZIP incorpora `marca-logisign.png` cuando corresponde y `registro-fotografico.json`, `fotos/frente.jpg`, `fotos/izquierda.jpg`, `fotos/derecha.jpg` cuando hay cámara. El registro incluye consentimiento, desafíos, hashes, tiempos recibidos y resultados/versiones del modelo si aplica. El manifiesto y el registro llevan sello Ed25519. El verificador admite v1, v2, v3 y v4. Las fotografías dentro del ZIP son datos personales legibles: restringir su acceso. Las copias por correo no contienen fotografías.

## Varios PDF en una invitación (v4)

Una solicitud admite hasta cinco PDF, conservando cada original byte por byte. El orden del multipart define el orden de revisión. Nombres, orden, hashes y contenido se vinculan a la clave de idempotencia; cambiarlos exige otra operación. Ejemplo de carga:

```python
from contextlib import ExitStack

# Ejemplo: contrato.pdf tiene dos páginas; anexo.pdf comienza en la página 3.
metadata['placements'] = [
    {'page': 1, 'x': 0.1, 'y': 0.7, 'width': 0.4, 'height': 0.14},
    {'page': 3, 'x': 0.1, 'y': 0.7, 'width': 0.4, 'height': 0.14},
]

with ExitStack() as stack:
    parts = [
        ('files', (name, stack.enter_context(open(name, 'rb')), 'application/pdf'))
        for name in ['contrato.pdf', 'anexo.pdf']
    ]
    response = httpx.post(
        base + '/api/v1/envelopes',
        headers={**headers, 'Idempotency-Key': operation_key},
        files=parts, data={'metadata': json.dumps(metadata)}, timeout=90,
    )
response.raise_for_status()
```

`GET /envelopes/{id}` y `GET /signing` bajo `/api/v1` incluyen `documents`: lista ordenada con `ordinal`, `title`, `original_hash`, `start_page` y `page_count`. La consulta administrativa incluye además `signed_hash` por documento al finalizar. En un PDF legado la lista está vacía y siguen aplicando las rutas habituales.

Las posiciones usan el número global de página de la vista continua: si el primer PDF tiene dos páginas, la primera del segundo es `page: 3`. El editor muestra el nombre del archivo y la numeración local. Cada página debe abrirse y cada PDF debe tener al menos una firma completa; las iniciales solas no bastan.

Al preparar, se generan y sellan tanto la vista conjunta como cada PDF firmado individual. El código autoriza ese conjunto exacto. `complete` requiere `draft_id` y la versión de consentimiento devuelta por `GET /signing`: `1.1-set` para varios documentos, `1.0` para uno. La confirmación es atómica: todos quedan firmados o ninguno. No se admite el flujo antiguo sin borrador para conjuntos. Las fotos opcionales se vinculan al mismo conjunto preparado.

| Copia | Ruta bajo `/api/v1` |
|---|---|
| Original individual | `GET /envelopes/{id}/documents/{ordinal}/original` |
| Firmado individual | `GET /envelopes/{id}/documents/{ordinal}/signed` |
| ZIP de originales | `GET /envelopes/{id}/files/originals` |
| ZIP de firmados | `GET /envelopes/{id}/files/signed_documents` |
| Copia individual para el firmante | `GET /signing/documents/{ordinal}/{original|signed}` |
| ZIP para el firmante | `GET /signing/files/{originals|signed_documents}` |

Las rutas individuales respetan el propietario de la solicitud o el token personal vigente. Para conjuntos, las rutas históricas `files/original` y `files/signed` devuelven la vista conjunta, no un original individual. La consola descarga ZIP y la pantalla final permite también descargar cada firmado. Reabrir el enlace tras firmar muestra las copias mientras siga vigente.

Firmante y solicitante reciben cada PDF firmado como adjunto separado, más un certificado del conjunto. El ZIP de evidencias v4 contiene `documentos/01/original.pdf`, `documentos/01/firmado.pdf`, etc., además de la vista conjunta y el resto del expediente. Cada archivo está incluido en los hashes del manifiesto y en la comprobación de custodia. Los snapshots incluyen todos los archivos.

La revisión previa a verificar muestra un checklist de cada documento preparado, con sus firmas e iniciales colocadas y páginas abiertas. El checklist aparece después de que el servidor acepta la preparación de todos los documentos. No indica una firma finalizada: aún faltan las verificaciones y el consentimiento. Tanto el checklist como las pantallas de cámara y código ocultan el visor PDF y concentran la interfaz en la acción pendiente. Volver a cambiar posiciones requiere preparar de nuevo la solicitud.


## QR y comparación de una copia

Las solicitudes nuevas incorporan `verification_stamp: v1` al evento inicial sellado. Al preparar el PDF se añade debajo del área visible de cada página una franja de 88 puntos (164 en páginas estrechas), sin cubrir el contenido; aumenta la altura del PDF final. Cada PDF del conjunto tiene su QR propio y conserva su número de páginas. La franja forma parte del borrador aprobado y de sus hashes antes de pedir el OTP. El flujo legado añade la franja al generar su firmado. No se reescriben archivos ni borradores históricos.

El texto impreso identifica el **SHA-256 del original**, abreviado con `...`. El hash de los bytes finales no se imprime dentro de ese mismo archivo, ya que eso cambiaría el hash. El certificado, el manifiesto y el validador contienen el SHA-256 final completo.

El QR abre `/verify#<codigo>`. Es una capacidad de consulta autenticada con HMAC y un propósito distinto del token de firma; no sirve para firmar ni descargar archivos. `GET /api/v1/verification/{codigo}` responde `status`, `document`, `original_hash`, `signed_hash`, `signed_at` e `integrity_checked`. Comprueba la cadena de custodia y el manifiesto antes de responder, no publica nombres, correos ni fotografías. Devuelve 404 para códigos inválidos y 409 si la custodia no concuerda. Las consultas no agregan eventos al expediente finalizado. La validación de documentos firmados continúa disponible después del vencimiento de la invitación; depende de conservar el dominio, la base y la clave de custodia.

**Consultar un QR no valida por sí solo la copia que lo contiene.** La pantalla distingue «firma registrada» de «copia verificada». El usuario selecciona su PDF y el navegador calcula SHA-256 con Web Crypto, comparándolo con el hash final. No sube el PDF. Un archivo modificado o perteneciente a otro documento no coincide, aunque muestre un QR válido. No sustituye la verificación offline con una clave pública obtenida independientemente.

## Carga Word (.docx y .doc)

La consola y los endpoints de carga admiten PDF, Word `.docx` y Word `.doc`, incluso mezclados en una misma invitación. Se mantienen los límites de cinco archivos, 5 MiB de fuentes y 40 páginas; la vista PDF conjunta también debe caber en 5 MiB. No se admite `.docm` ni documentos cifrados. Se firma la representación PDF, no un Word editable. Revisa las fuentes, tablas y saltos de página después de convertir.

`POST /api/v1/pdf/inspect` y `/api/v1/pdf/preview/{page}` conservan sus nombres por compatibilidad y aceptan Word. `inspect` devuelve `converted`, `source_hash` (SHA-256 del Word, o null para PDF), `original_hash` (SHA-256 del PDF de trabajo), `pages` y `preview`. La conversión es privada, por propietario; la misma copia PDF se reutiliza durante la revisión y creación. Las conversiones sin solicitud caducan a las 24 horas y el proceso de limpieza las elimina. Las conversiones vinculadas a una solicitud se conservan con ella.

Al crear, enviar opcionalmente `expected_hashes` como campo multipart JSON: una lista de `original_hash` devueltos por `inspect`, en el mismo orden que los archivos. La consola siempre lo envía. Una discrepancia devuelve 422 y exige cargar y revisar de nuevo; evita enviar una conversión diferente de la inspeccionada. El campo no altera el contenido de `metadata`. La clave de idempotencia incluye la huella y nombre del Word mediante el descriptor sellado del documento.

Los documentos convertidos incluyen `source_hash`, `source_name` y `source_type` en su descriptor. `original_hash` sigue identificando el PDF de trabajo. `GET /api/v1/envelopes/{id}/documents/{ordinal}/source` y `GET /api/v1/signing/documents/{ordinal}/source` descargan el Word original con los mismos controles de acceso que las copias PDF. El ZIP de originales incluye tanto el Word como el PDF sin firmar.

Un expediente con Word utiliza `logisign-evidence-v5`, incluso si contiene un solo documento. Añade `documentos/NN/fuente.docx` o `fuente.doc` al ZIP; los bytes y hashes del Word, del PDF convertido y del PDF firmado se vinculan al manifiesto y sus eventos sellados. `scripts/verify.py` verifica v1–v5 y rechaza la alteración del Word de origen. La copia por correo sigue siendo PDF. El QR muestra el hash del PDF de trabajo; el certificado y expediente incluyen además el hash del Word.

### Conversor autohospedado

Añadir `compose.word.yaml` a `COMPOSE_FILE` y arrancar el servicio `word` antes de actualizar `app`. Usa Gotenberg 8.37.0 fijado por digest con LibreOffice, en una red Docker interna, sin puertos publicados ni acceso a datos o secretos de LogiSign. Las descargas remotas y rutas Chromium están deshabilitadas; los recursos externos de LibreOffice están bloqueados. Solo `app` comparte esa red. `WORD_SUBNET` permite elegir una subred libre; en producción se usa `10.245.43.0/24`. La configuración establece `WORD_CONVERTER_URL=http://word:3000`, límites de recursos, tiempo de conversión y tamaño. Una caída del servicio conserva el formulario y muestra un error; no crea una solicitud incompleta.

## Varios firmantes, interesados en copia y orden (v6)

La consola usa **Documentos → Personas → Firmas → Opciones → Revisar**. El solicitante puede añadir hasta diez firmantes y diez personas en copia, ordenar los firmantes con las flechas y definir posiciones por persona y documento. Todos los firmantes firman todos los documentos. Los interesados no firman: reciben un aviso inicial y, al terminar, los PDF finales y el certificado.

La API admite `signers` como lista **ordenada**. Sus elementos contienen `name`, `email` y `placements` (1–20 posiciones por persona, al menos una por documento). `cc` contiene `name` y `email`. Los correos deben ser únicos entre ambas listas. Las posiciones de distintas personas no pueden superponerse. Ejemplo para un documento:

```json
{
  "title": "Acuerdo con dos firmantes",
  "send_email": true,
  "expires_in_days": 7,
  "identity_mode": "none",
  "brand_stamp": true,
  "signers": [
    {"name": "Primera persona", "email": "primera@example.com", "placements": [{"page": 1, "x": 0.08, "y": 0.70, "width": 0.35, "height": 0.14}]},
    {"name": "Segunda persona", "email": "segunda@example.com", "placements": [{"page": 1, "x": 0.52, "y": 0.70, "width": 0.35, "height": 0.14}]}
  ],
  "cc": [{"name": "Interesado", "email": "interesado@example.com"}]
}
```

`signers` sustituye a los campos superiores `name`, `email` y `placements`; la variante anterior de un firmante sigue siendo compatible. Con varias personas `send_email` debe ser `true`. La respuesta inicial contiene únicamente el enlace del primer firmante. Los enlaces de los siguientes se generan y encolan al confirmar el turno anterior. No se entregan enlaces ni códigos a interesados en copia. El orden, destinatarios y posiciones solicitadas quedan fijados al crear el sobre; para cambiarlos hay que cancelarlo y crear otro. La vigencia es común a todo el sobre y comienza al crearlo.

`GET /api/v1/envelopes/{id}` conserva el identificador inicial y añade `workflow`: estado global, `completed`, `total`, `position`, lista `signers` con orden/estado/fecha y `cc`. La lista administrativa muestra una sola solicitud por sobre. `status: signed` y `ok: true` se devuelven únicamente cuando todos terminaron. La cancelación detiene el turno pendiente y conserva las etapas ya firmadas.

Cada firmante abre sus propias páginas, prepara su versión y valida su propio OTP y fotografías cuando se pidieron. El original de un turno posterior es exactamente el PDF confirmado en el turno anterior, incluidas sus firmas visibles. El servidor impide cubrir marcas anteriores o la franja QR. El cliente debe enviar en `complete` la `consent_version` obtenida de `GET /signing`: para este flujo es `2.0-ordered`. Autoriza las firmas posteriores y la entrega final a los participantes. El `ok` de `POST /signing/complete` confirma **esa etapa**; consultar `workflow.status` o la solicitud raíz para confirmar todo el sobre.

Los avisos al solicitante de apertura/descarga se generan por firmante. `step_completed` confirma a cada firmante intermedio su etapa. `workflow_completed` envía los PDF finales individuales y el certificado del sobre a firmantes, solicitante e interesados, deduplicando destinatarios. No adjunta fotografías, códigos ni enlaces personales.

Las rutas de descarga final sobre el ID raíz devuelven los documentos de la última etapa; antes de terminar responden 409. Cada firmante puede descargar los PDF finales con su enlace vigente cuando todos hayan terminado. Su `/signing/files/archive` contiene únicamente la evidencia de su etapa, con sus propias fotos si correspondían. El archivo administrativo `/envelopes/{id}/files/archive` contiene el expediente completo v6: originales y finales individuales, Word de origen, certificado global y los expedientes de todas las etapas bajo `pasos/NN/`. Estos últimos usan `logisign-evidence-step-v1`. El verificador offline admite ambos formatos y comprueba el orden y la continuidad de los hashes, además de los formatos históricos v1–v5.

La franja QR se añade una sola vez. Su consulta permanece pendiente hasta completar todas las firmas y después compara contra el hash del PDF final. Los originales, versiones intermedias, evidencias de cada firmante y definición sellada del orden se conservan en SQLite y en los snapshots existentes.

## Clientes y usuarios (multitenant)

Cada cliente es un espacio independiente de usuarios, sobres y credenciales de aplicación. Las cuentas existentes, documentos históricos e integraciones se conservan en el espacio `ilogic`. La cuenta administradora existente pasa a administrar la plataforma. Un usuario pertenece a un cliente; el correo de acceso es único en la plataforma. Los destinatarios de firmas no necesitan cuenta administrativa.

Roles:

- **Solicitante** (`member`): crea y consulta sus propias solicitudes dentro de su cliente.
- **Administrador del cliente** (`admin`): gestiona usuarios, aplicaciones y solicitudes de su cliente.
- **Administrador de plataforma** (`platform_admin`): crea clientes manualmente y cambia explícitamente de espacio para administrarlos. Este privilegio no se concede mediante invitaciones ni edición de usuarios.

Desde **Usuarios**, un administrador invita por nombre, correo y rol, consulta el estado del envío, reenvía activación/recuperación, cambia nombre o rol y desactiva/reactiva accesos. Las contraseñas siempre se establecen por enlace de un solo uso. Desactivar revoca sesiones y enlaces de recuperación; cambiar rol revoca sesiones. No se puede eliminar el último administrador activo ni transferir una cuenta a otro cliente desde estos endpoints.

| Operación | Ruta bajo `/api/v1` |
|---|---|
| Usuarios del espacio actual | `GET /users` |
| Invitar al espacio actual | `POST /users` con `name`, `email`, `role: member|admin` |
| Editar nombre, rol o estado | `PATCH /users/{id}` con `name`, `role`, `active` |
| Reenviar enlace | `POST /users/{id}/invite` |
| Desactivar usuario | `DELETE /users/{id}` |
| Listar clientes (plataforma) | `GET /tenants` |
| Crear cliente sin enviar correo (plataforma) | `POST /tenants` con `name`, `slug` |
| Renombrar o activar/desactivar cliente (plataforma) | `PATCH /tenants/{id}` con `name`, `active` |
| Cambiar el espacio de la sesión (plataforma) | `POST /auth/tenant` con `tenant_id` |

Los endpoints de usuarios y clientes usan sesión y protección CSRF; no admiten claves de una aplicación. `GET /auth/me` y el login incluyen `tenant: {id,name,slug}` y `platform_admin`. La consola envía `X-LogiSign-Tenant` con el espacio mostrado; si otra pestaña cambió el espacio de esa sesión, el servidor devuelve 409 y exige recargar. El identificador enviado por el navegador no concede acceso por sí mismo.

Cada credencial de **Aplicaciones** queda asociada al cliente donde fue creada. El backend deriva su espacio de la credencial e ignora cualquier intento de seleccionar otro cliente con una cabecera. Incluso dentro del mismo cliente cada aplicación consulta solo sus sobres; el administrador del cliente puede consultar todos los de ese espacio. La clave maestra operativa utiliza `ilogic` por defecto y permite seleccionar otro cliente con `X-LogiSign-Tenant`; no debe distribuirse a clientes ni aplicaciones.

La desactivación de un cliente conserva datos, revoca sesiones y recuperación, bloquea sus claves de aplicación y enlaces personales de firma/descarga, y descarta los correos aún pendientes al procesarlos. La consulta pública del QR conserva la verificación de integridad de los documentos ya firmados. Reactivar no reabre sesiones ni renueva plazos: reenviar activaciones cuando corresponda.

No hay autorregistro, facturación ni dominios por cliente. Todos ingresan en el mismo dominio con su correo; el espacio se determina por la cuenta. La administración manual se hace en **Clientes → Crear cliente → Administrar espacio → Usuarios → Invitar**, escogiendo un administrador para el nuevo cliente.


## Aceptación separada del mecanismo de firma

`GET /api/v1/signing` devuelve `mechanism` con `version`, `text` y `sha256`. Antes de confirmar, el cliente debe mostrar el texto y recoger una aceptación expresa separada de la aprobación del documento y del permiso fotográfico.

`POST /api/v1/signing/complete` requiere también `mechanism_accepted: true` y `mechanism_version` con el valor recibido (actualmente `2026-09-22`). Omitirlos, negar la aceptación o enviar una versión distinta devuelve 422; actualizar integraciones directas. No enviar `true` automáticamente ni preseleccionar la casilla. El flujo web ya lo solicita.

El texto, versión, hash y aceptación quedan en `manifest.consent.mechanism`, en el evento `signed` y en el certificado de cada etapa. Los expedientes antiguos conservan su consentimiento original: no se les atribuye retrospectivamente esta aceptación. Esta evidencia no sustituye la verificación de identidad ni la adopción jurídica del mecanismo por la empresa.


## Estado de custodia de la plataforma

`GET /api/v1/custody/status` requiere sesión de administrador de plataforma; no admite la clave maestra ni claves de aplicaciones como sustituto de esa sesión. Devuelve `state` (`missing`, `current`, `stale`, `invalid`, `unavailable`), `checked_at`, `last_block_at`, `pending_signed_records`, `pending_retention_records` y `independent_deposit_verified: false`. Es una comprobación operativa global; no acredita custodia independiente ni legalidad de firmas. Un fallo de comprobación nunca devuelve `current`.

## Doble factor para cuentas de acceso

Cada usuario puede activar TOTP desde **Seguridad**, confirmando contraseña y código del autenticador. Los administradores sin enrolamiento ven un aviso. No se activa ni obliga automáticamente antes de configurar el dispositivo. Al habilitarlo se cierran otras sesiones y se entregan diez códigos de recuperación de un uso; deben guardarse fuera de LogiSign. No vuelven a mostrarse.

- `POST /api/v1/auth/mfa/setup`: sesión y `{current_password}`; devuelve QR generado localmente y clave de configuración de diez minutos.
- `POST /api/v1/auth/mfa/enable`: sesión y `{current_password,code}`; activa y devuelve `recovery_codes` una sola vez.
- `POST /api/v1/auth/login`: `{email,password,code?}`. Con MFA y sin código devuelve `{mfa_required:true}`, **sin sesión**. Reenviar con código TOTP o de recuperación. Un código TOTP aceptado no vuelve a usarse.
- `POST /api/v1/auth/mfa/disable`: sesión, contraseña actual y un código nuevo; desactiva y cierra todas las sesiones.
- `GET /api/v1/auth/me` incluye `mfa_enabled`.

Se mantienen CSRF, límites de intentos y contexto del espacio. Cambiar/restablecer contraseña por correo no elimina MFA. El administrador no dispone de un botón para desactivar el de otros usuarios. Si se pierden dispositivo y todos los códigos, detener intentos y aplicar un procedimiento excepcional documentado con comprobación de identidad y aprobación de un segundo responsable antes de intervención técnica. Nunca quitar MFA únicamente por recibir un correo. Esta protección corresponde a cuentas de administración/operación: el código de firma de destinatarios y las claves API de integraciones son mecanismos distintos. Custodiar las claves API solo en servidores.

`GET /api/v1/custody/status`, exclusivo de sesión de administrador de plataforma, incluye `external_copy: {state,received_at}`. Estados: `missing`, `current`, `pending` (bloque nuevo por copiar), `stale` (más de 30 horas), `invalid`. Un recibo válido acredita la recepción técnica firmada por dev-il; `independent_deposit_verified` permanece `false` mientras no se resuelva la custodia independiente.


## Conservación por expediente

Disponible en **Conservación**, con sesión de administrador del espacio, CSRF y aislamiento por cliente. Las claves API no autorizan estas operaciones. Cada documento se clasifica por separado; las preservaciones cubren el sobre completo y todas sus etapas de firma, incluidas las futuras. Los expedientes existentes aparecen por clasificar.

| Operación | Ruta bajo `/api/v1/retention` |
|---|---|
| Consultar expedientes | `GET ?q=texto&offset=0` (50 por página, `has_more`) |
| Estado e historial | `GET /{id}` |
| Clasificar todos los documentos | `PUT /{id}/classification` |
| Abrir preservación | `POST /{id}/holds` |
| Levantar una preservación | `POST /{id}/holds/{hold_id}/release` |
| Registrar revisión de necesidad de fotografías | `POST /{id}/photo-review` |
| Descargar historial sellado | `GET /{id}/export` |

Toda escritura incluye `revision` recibida en la consulta, `reason` (10–1000 caracteres) y `reference` (3–180). Un cambio concurrente devuelve 409: recargar y revisar antes de repetir. La clasificación añade `documents`, con cada `ordinal` exactamente una vez y `category`: `unclassified`, `commercial`, `labor_essential`, `sst_20` o `unsigned_request`. Comercial recibe `closed_on` y `last_document_on` juntas o ambas pendientes; SST recibe `terminated_on`; las otras categorías no llevan fechas. Las fechas son ISO `YYYY-MM-DD`, reales y no futuras. Una firma parcial también impide clasificar como solicitud sin firmas.

Abrir preservación añade `kind`: `claim`, `litigation`, `investigation`, `incident` u `other`. Pueden coexistir varias; levantar una no levanta las demás. El motivo y la referencia identifican el soporte que debe custodiar la empresa; el sistema no comprueba su autorización jurídica. Preservar no cancela ni detiene la firma.

Los plazos siguen la matriz propuesta y la fecha de Colombia. Comercial calcula diez años desde la fecha más reciente entre cierre, último documento declarado y creación/firma registrada; laboral esencial queda indefinido; SST calcula veinte años desde retiro; solicitudes canceladas o vencidas sin firmas, noventa días desde cierre. Sin fechas suficientes se marca pendiente. `retain_through` es inclusiva; después se requiere revisión, **nunca se autoriza borrar** (`deletion_allowed: false`). La revisión fotográfica anual registra necesidad y soporte, sin retirar fotografías de evidencias selladas.

El historial administrativo se encadena y sella aparte: no modifica PDF, certificados ni ZIP anteriores. Los nuevos checkpoints incluyen sus cabeceras en `manifest.retention`; `pending_retention_records` cuenta expedientes con cambios aún no incluidos en el último bloque. Para verificar una exportación:

```bash
python -m scripts.verify_retention conservacion.json --public-key clave-publica-confiable.pem
# Opcional: comprobar cobertura por un bloque cuyo hash se obtuvo por un canal independiente.
python -m scripts.verify_retention conservacion.json --public-key clave-publica-confiable.pem --checkpoint bloque.json --checkpoint-sha256 HASH_CONFIABLE
```

La clave y el hash de confianza deben provenir de una fuente previamente validada. La exportación contiene datos de responsables y direcciones IP: acceso restringido. La verificación acredita consistencia criptográfica y, si se aporta bloque, cobertura hasta la revisión indicada; no acredita por sí sola independencia, tiempo certificado o autorización para disposición.


## Corregir el correo de un firmante pendiente

En **Documentos → detalle de la solicitud → Corregir correo**, el solicitante o administrador puede corregir el correo del firmante invitado o de quien espera su turno. Se conserva nombre, orden, documentos y posiciones. No permite editar firmantes que ya firmaron ni sobres cancelados o vencidos.

`PATCH /api/v1/envelopes/{id}/signers/{order}/email` usa el mismo acceso por propietario/cliente que la solicitud (sesión con CSRF o credencial de la aplicación propietaria). `order` empieza en 1. Cuerpo:

```json
{
  "email": "correo-correcto@example.com",
  "expected_email": "correo-anterior@example.com",
  "revision": 0,
  "reason": "Corrección del correo ingresado por error."
}
```

Obtener `revision` de `recipient_revision` en `GET /envelopes/{id}` y `expected_email` del firmante mostrado. Una edición concurrente o un correo anterior distinto devuelve 409. No se permiten correos duplicados entre firmantes y personas en copia. Devuelve `ok`, `email`, `recipient_revision` e `invitation_queued`.

Si el firmante ya estaba invitado, invalida su enlace, códigos y preparación pendiente (incluidas fotos), reinicia la revisión de páginas y encola una invitación nueva. Debe revisar y validar nuevamente. Si espera su turno, solo corrige el destinatario de la futura invitación, respetando el orden. No puede retirar mensajes ya entregados ni copias que alguien haya descargado antes de la corrección. El estado de envío sigue visible en el detalle.

Los campos originales y la definición inicial del sobre permanecen inmutables. Cada corrección se añade como evento sellado `signer_email_corrected` a la etapa actualmente pendiente, con correo anterior/nuevo, posición, motivo, responsable, fecha y contexto técnico. El detalle incluye `recipient_changes`. La firma, certificado y copias finales usan el correo corregido; los ZIP conservan tanto la solicitud original como las correcciones. Los bloques de custodia incorporan estos eventos con el mecanismo existente, sin modificar firmas anteriores.

Para verificar estos expedientes, distribuir juntos `scripts/verify.py` y `scripts/recipient_changes.py` (o el repositorio completo). El verificador admite los expedientes históricos sin correcciones. No volver a una versión anterior a esta función después de registrar correcciones: podría dirigir comunicaciones al correo original o rechazar evidencias nuevas. La reversión requiere revisión de compatibilidad y preservación del historial.

### Corregir el correo de una persona en copia

En el mismo detalle, **En copia → Corregir correo** permite corregir cada contacto mientras el sobre esté pendiente. `PATCH /api/v1/envelopes/{id}/cc/{order}/email` recibe el mismo cuerpo; `order` es la posición en la lista de copia, empezando en 1, y `revision` se obtiene de `cc_revision`. Devuelve `ok`, `email`, `cc_revision` e `invitation_queued: false`.

Se aplican los mismos permisos, validación de correo, rechazo de duplicados y control de ediciones concurrentes. El cambio conserva las invitaciones y revisiones de los firmantes. Se registra un evento sellado `cc_email_corrected` en la etapa pendiente, visible en `cc_changes`, y la copia final se dirige al correo corregido. Los avisos pendientes o fallidos para el correo anterior se marcan como obsoletos y se encola un aviso al nuevo correo. Los mensajes ya entregados permanecen en el historial. No se admiten cambios en sobres finalizados, cancelados o vencidos.

### Agregar y quitar firmantes de un sobre enviado

El detalle de un sobre con orden de firma pendiente permite **Agregar firmante** al final (hasta 10) y **Quitar firmante** si aún no firmó. Debe quedar al menos una persona pendiente; para sustituir al único invitado, agregar primero al reemplazo y quitar después al anterior. Los sobres firmados, cancelados o vencidos no se modifican. Las solicitudes antiguas sin orden de firma no ofrecen estos controles.

- `POST /api/v1/envelopes/{id}/signers`: cuerpo con `name`, `email`, `revision` y `reason`.
- `DELETE /api/v1/envelopes/{id}/signers/{order}`: cuerpo con `expected_email`, `revision` y `reason`.

`revision` usa el `recipient_revision` del detalle y `reason` requiere de 10 a 1000 caracteres. Los mismos permisos y controles de duplicados aplican. El alta devuelve 201, la retirada 200; ambos devuelven `ok` y `recipient_revision`. Un cambio concurrente devuelve 409.

Los eventos sellados `signer_added` y `signer_removed` conservan la persona, posición, motivo y responsable en `recipient_changes`. La definición original y las etapas firmadas permanecen inmutables. El total y la lista efectiva se reconstruyen a partir de los eventos; los certificados, notificaciones y copias finales usan los participantes vigentes.

Cada cambio renueva la invitación de la etapa pendiente y elimina su preparación y códigos anteriores para revisar el orden actualizado. Si se retira al invitado, el siguiente ocupa su turno y recibe un enlace nuevo; el enlace retirado deja de funcionar. Las nuevas personas reciben invitación al llegar su turno y eligen las posiciones de firma al revisar el documento. Actualizar también el verificador de expedientes antes de utilizar esta función; las versiones anteriores no interpretan las altas y retiradas.


## Volver a copiar la invitación

Desde **Documentos → detalle → Copiar enlace de invitación** se recupera el enlace del firmante cuyo turno está activo. No envía otro correo, no extiende el vencimiento ni reinicia las páginas revisadas, marcas, fotos o códigos. En un sobre con orden de firma no se emiten enlaces para personas que aún esperan turno. Los sobres firmados, cancelados o vencidos no permiten recuperar una invitación desde este endpoint.

`POST /api/v1/envelopes/{id}/signers/{order}/invitation-link`, con la sesión o credencial propietaria del sobre y protección CSRF cuando se usa sesión:

```json
{"expected_email":"firmante@example.com","revision":0}
```

Tomar `revision` de `recipient_revision` en `GET /envelopes/{id}` y el correo del firmante seleccionado. Si cambió el destinatario o su turno, devuelve 409. Responde `signing_url`, `name`, `email` y `expires`. La URL es una credencial personal: compartir únicamente con el firmante, no incluirla en logs, listados públicos o analítica. El navegador intenta copiarla al portapapeles; si no tiene permiso, muestra un campo para seleccionarla y copiarla manualmente.

Las invitaciones nuevas conservan el token cifrado y recuperan el mismo enlace, incluso después de que se borre el mensaje de la cola de correo. Para sobres históricos se recupera de la cola cifrada si sigue disponible; de lo contrario se crea un enlace adicional estable vinculado a la misma invitación, manteniendo válido el anterior. Cambiar el correo del invitado o agregar o quitar firmantes revoca ambos mediante el cambio de credencial del firmante. Las correcciones de personas en copia conservan los enlaces vigentes. La consulta deja un evento sellado `invitation_link_accessed` con responsable y contexto, sin incluir el token ni la URL.

El campo `signing_url` sigue ausente de las consultas generales y permanece `null` en el replay de creación. Para reconciliar una respuesta inicial perdida, usar el ID devuelto por el replay y este endpoint. El esquema adicional `invitation_links` forma parte de los snapshots existentes; guarda exclusivamente credenciales cifradas y hashes vinculados a la credencial actual. No retroceder a una versión que ignore los enlaces adicionales después de haberlos entregado, pues dejarían de autenticar.

## Identificación declarada obligatoria (30 de septiembre de 2026)

Toda firma pendiente exige ahora tipo, país emisor y número de documento declarados por el firmante. `GET /signing` devuelve `identity_declaration` con `required`, `version`, `text` y `method: self_declared`. No significa verificación de identidad civil. Los expedientes ya firmados no se modifican ni reciben una identidad retroactiva.

Las integraciones directas deben incluir el mismo objeto `identity` tanto en `POST /signing/otp` como en `POST /signing/complete`, además de los campos anteriores:

```json
{
  "document_type": "CC",
  "issuing_country": "CO",
  "document_number": "1234567890",
  "accepted": true,
  "version": "identity-declaration-1.0"
}
```

Ejemplo ficticio. Tipos: `CC`, `CE`, `PPT`, `PA` (pasaporte), `OTHER`. País: dos letras mayúsculas; CC, CE y PPT requieren CO. Número: cadena de 3–24 caracteres ASCII alfanuméricos, sin separadores, conservando ceros iniciales; CC admite 3–10 dígitos. Se normalizan letras a mayúsculas. Estas reglas verifican formato, nunca existencia, titularidad o autenticidad. Mostrar el texto y pedir confirmación expresa, sin preseleccionarla.

Omitir la declaración al pedir OTP devuelve 422. Cambiarla o intentar completar con un código emitido antes de esta actualización devuelve 409: recargar el flujo, declarar la identificación y solicitar otro código. La creación de sobres no cambia. El flujo web recoge la declaración dentro de Valida antes del correo y permite corregirla; la firma preparada permanece intacta.

El OTP queda ligado a la declaración cifrada, el destinatario, el original y el borrador. Cada reenvío crea una declaración con un nonce aleatorio; su hash público no permite enumerar números de documento. Cambiar destinatario, fotos o borrador invalida la confirmación previa. El consentimiento de firma incluye la referencia sellada de la declaración. El certificado muestra tipo, país y dos últimos caracteres; el número completo se incluye únicamente en `identidad-declarada.json` dentro del expediente autorizado, o en `pasos/NN/identidad-declarada.json` para varios firmantes. No aparece en los correos, QR ni manifestaciones públicas generadas por este cambio. El ZIP descargado es legible: su receptor debe custodiarlo con acceso restringido.

El descriptor `identity_declaration` en evento y manifiesto conserva `method: self_declared` y `verified: false`. `scripts/verify.py` comprueba archivo, hash y asociación al firmante, documento, OTP y evento final. Un resultado correcto acredita consistencia del expediente, no identidad oficial. No se envían identificaciones ni fotos a terceros ni se implementa cotejo automático con esta actualización. Ver [evaluación inmobiliaria](IDENTIDAD-INMOBILIARIAS.md).