# Usuarios externos

Identifica a los usuarios de tu aplicación, vincúlalos a una ficha de cliente del CRM y consulta el listado con /external/v1/users.

Con `/external/v1/users` tu aplicación registra en UTILIA OS a sus
usuarios finales y consulta los que ya tiene vinculados. Es el primer
paso para emitir facturas, guardar tarjetas o abrir tickets en nombre de
esos usuarios.

## Autenticación

Todas las peticiones llevan la clave de API de la aplicación en la
cabecera `X-Api-Key`:

```
X-Api-Key: <api_key_de_la_app>
```

## Plan y permisos

Antes que el permiso de la aplicación se comprueba el plan de tu
organización. Sin el aplicativo **Integraciones** contratado, la
respuesta es `403` con `errorCode: 'MODULE_NOT_ENABLED'`.

| Endpoint | Aplicativo del plan | Permiso de la API (scope) |
| - | - | - |
| `POST /external/v1/users/identify` | Integraciones | `crm:users:write` |
| `GET /external/v1/users` | Integraciones | `crm:users:read` |
| NIF y dirección de facturación guardados, en las dos rutas | Integraciones | además, `crm:users:read:fiscal-data` |

`crm:users:read:fiscal-data` («Ver los datos fiscales de los usuarios») es un permiso de datos personales críticos: para un autónomo, el NIF lo identifica. Solo lo concede un Super Administrador, con un motivo de al menos veinte caracteres. No sustituye a los otros dos: el listado sigue pidiendo `crm:users:read` y el alta, `crm:users:write`.

> **Atención: Desde el 21 de octubre de 2026, el NIF solo con su permiso**
>
> Hasta ese día, el listado y la respuesta de `identify` devolvían el NIF (`taxId`) y la dirección de facturación (`billingAddress`) guardados con el permiso básico. Ahora solo llegan con `crm:users:read:fiscal-data`. Si tu integración los leía de ahí, pide a un Super Administrador que conceda el permiso; si no, guárdalos en tu sistema cuando los mandes. Que no lleguen no significa que no estén guardados.

> **Atención: identify exige crm:users:write desde el 5 de septiembre de 2026**
>
> Dar de alta o actualizar a un usuario final trata datos personales y
> fiscales (correo, NIF y dirección de facturación), así que exige
> `crm:users:write`; la lectura sola (`crm:users:read`) no basta.
>
> Si tu aplicación ya usaba `identify`, concédele `crm:users:write` antes
> de nada: no hay periodo de gracia y la llamada responde
> `403 INSUFFICIENT_SCOPE` desde el primer momento.

Sin el permiso adecuado, la respuesta es `403 INSUFFICIENT_SCOPE` con el permiso que
falta en `requiredScopes`. El catálogo completo está en
[Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes).

## Endpoints

**`POST /api/external/v1/users/identify`**

Da de alta o actualiza un usuario externo por su externalId. Permiso: crm:users:write.

**`GET /api/external/v1/users`**

Lista los usuarios externos vinculados a la aplicación, primero los que han tenido actividad más reciente. Permiso: crm:users:read; el NIF y la dirección de facturación, solo con crm:users:read:fiscal-data.

## Cuerpo de identify

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `externalId` | `string` | Sí | Identificador estable del usuario en tu sistema (máximo 255 caracteres). Junto con la aplicación, es la clave para crear o actualizar. |
| `email` | `string` | No | Correo del usuario. Se valida su formato. |
| `name` | `string` | No | Nombre o razón social (máximo 255 caracteres). |
| `avatarUrl` | `string` | No | Dirección de la imagen del usuario. Se valida que sea una URL. |
| `metadata` | `object` | No | Metadatos libres asociados al usuario. |
| `taxId` | `string` | No | NIF o CIF del usuario final (máximo 50 caracteres). Se copia a su ficha de cliente del CRM y al cliente de la pasarela de pago. |
| `billingAddress` | `object` | No | Dirección de facturación. Si la envías, sustituye a la guardada. Es la que se imprime en el PDF de sus facturas. |
| `clientId` | `string` | No | Ficha de cliente del CRM a la que vincular el usuario, cuando tu aplicación la creó antes con POST /external/v1/clients o con el propio puente. Sin este campo, UTILIA OS crea o reutiliza la ficha la primera vez que factura, guarda una tarjeta o crea una suscripción para el usuario. |

Campos de `billingAddress`:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `street` | `string` | Sí | Calle y número o indicación de la dirección (máximo 255 caracteres). |
| `city` | `string` | Sí | Ciudad o municipio (máximo 100 caracteres). |
| `postalCode` | `string` | Sí | Código postal (máximo 20 caracteres). |
| `country` | `string` | Sí | País en texto libre o código ISO (máximo 100 caracteres). Mejor en castellano (por ejemplo, «España») para que coincida con el PDF de la AEAT. |
| `province` | `string` | No | Provincia o región administrativa (máximo 100 caracteres). |

`taxId` y `billingAddress` también viajan en el bloque `user` de la
creación de una factura, y el alta rellena con los datos del receptor
los huecos que el usuario no tenga guardados. Aun así, mándalos aquí en
cuanto los tengas: una factura emitida a un cliente sin identificador
fiscal obliga a rectificarla después. Lo que declare `identify` manda
siempre sobre lo que se deduzca del receptor, y omitir un campo no borra
el que ya estaba guardado. Si tu aplicación llamaba a `identify` sin
datos fiscales, no tienes que cambiar nada: los dos campos son opcionales.

## Parámetros del listado

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `page` | `number` | No | Página que se pide. Por defecto, 1. |
| `limit` | `number` | No | Usuarios por página, de 1 a 100. Por defecto, 20. |

La respuesta trae `users` y un bloque `pagination` con `page`, `limit`,
`total` y `totalPages`. Cada usuario lleva `taxId` y `billingAddress`
solo si la aplicación tiene `crm:users:read:fiscal-data`; sin él, esos
dos campos no vienen, ni siquiera a `null`, y ni se leen de la base de
datos.

## Qué devuelve identify

`identify` responde siempre con el usuario dado de alta o actualizado,
con `id`, `appId`, `externalId`, `lastSeenAt`, `createdAt` y
`updatedAt`. Lo demás depende de los permisos de la aplicación y de
dónde está su clave, porque cualquiera que tenga una clave publicada en
una página podría mandar el `externalId` de otro usuario y leer su ficha:

**¿Qué datos devuelve identify?**

¿Tiene la aplicación crm:users:read (o crm:users:read:fiscal-data) y una clave que no está publicada en páginas web?

- No: **Solo lo que mandas**. Los campos que enviaste en esta llamada, con el valor que quedó guardado. Lo que no mandaste no viene.
- Sí: ¿Tiene además crm:users:read:fiscal-data?
  - Sí: **La ficha guardada entera**. Correo, nombre, imagen, metadatos, ficha de cliente, NIF y dirección de facturación.
  - No: **La ficha guardada, sin lo fiscal**. Todo menos el NIF y la dirección de facturación, que solo vienen si los mandaste en esta llamada.

Una clave cuenta como publicada si la aplicación tiene dominios
autorizados o si la clave se ha usado desde un navegador desde su última
renovación ([Claves dentro de una página web](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#claves-dentro-de-una-página-web)).
Con la del widget, por tanto, `identify` solo devuelve el eco de lo que
mandas, y `clientId` solo si lo mandaste tú.

## El usuario externo se convierte en cliente del CRM

Dar de alta a un usuario no crea todavía nada en el CRM. La ficha de
cliente nace cuando hace falta, la primera vez que hay dinero de por
medio:

**De usuario de tu aplicación a cliente del CRM**

1. **Dar de alta al usuario** (Usuarios externos): identify lo crea o lo actualiza por su externalId.
2. **Primera factura, tarjeta o suscripción** (Finanzas): Es lo que pone en marcha el puente con el CRM.
3. **Buscar una ficha de la aplicación** (CRM): Por NIF exacto y después por correo, solo entre las fichas que creó tu aplicación. Si no: crea una ficha nueva.
4. [**Ficha de cliente lista**](https://os.utilia.ai/docs/guides/crm/clients-and-contacts) (CRM): Con su nombre, NIF, dirección y un contacto de facturación con su correo.

Desde ese momento el usuario existe para Finanzas y para el CRM como
cualquier otro cliente, y la ficha muestra de qué
aplicación viene. La respuesta de `identify` devuelve el `clientId`
cuando ese vínculo ya existe y la aplicación puede leer lo guardado (ver
[Qué devuelve identify](#qué-devuelve-identify)).

### Tu aplicación solo alcanza las fichas que ella creó

El puente de identidad trabaja únicamente sobre fichas de cliente que
haya creado la propia aplicación, con `POST /external/v1/clients` o con
el propio puente. Una ficha nativa del CRM, dada de alta por tu equipo
desde el panel, nunca se toca desde la API externa.

- **`clientId` en `identify`** solo acepta fichas propias. Con cualquier
  otra, la respuesta es `404 CLIENT_NOT_ACCESSIBLE`, nunca un 403: decir
  que existe permitiría descubrir fichas ajenas probando identificadores.
- **La deduplicación automática** busca por NIF exacto y después por
  correo exacto, pero solo entre las fichas propias. Si la coincidencia
  cae en una ficha nativa, no se reutiliza ni se modifica: se crea una
  ficha nueva y la coincidencia queda en el registro de auditoría para
  que tu equipo la revise.

Enlazar un usuario de la aplicación con una ficha nativa sigue siendo
posible, pero lo decide tu equipo desde el panel, con sesión iniciada y
permiso sobre esa ficha. Lo explica
[Panel de aplicaciones externas](https://os.utilia.ai/docs/developers/api/apps-panel).

## Ejemplos

### curl

```bash
# Identificar (alta o actualización)
curl -X POST 'https://os.utilia.ai/api/external/v1/users/identify' \
  -H 'X-Api-Key: utl_xxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "externalId": "user_01HXYZ",
    "email": "cliente@empresa.com",
    "name": "Empresa de Canarias SL",
    "taxId": "B76543210",
    "billingAddress": {
      "street": "Avenida de Canarias 12, planta 3",
      "city": "Las Palmas de Gran Canaria",
      "postalCode": "35001",
      "country": "España",
      "province": "Las Palmas"
    }
  }'

# Listar usuarios vinculados
curl -X GET 'https://os.utilia.ai/api/external/v1/users?page=1&limit=20' \
  -H 'X-Api-Key: utl_xxx'
```

### JavaScript

```typescript
import { UtiliaSDK } from '@utilia-os/sdk-js';

const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_API_KEY!,
});

const user = await sdk.users.identify({
  externalId: 'user_01HXYZ',
  email: 'cliente@empresa.com',
  name: 'Empresa de Canarias SL',
  taxId: 'B76543210',
  billingAddress: {
    street: 'Avenida de Canarias 12, planta 3',
    city: 'Las Palmas de Gran Canaria',
    postalCode: '35001',
    country: 'España',
    province: 'Las Palmas',
  },
});

console.log(user.clientId);
```

### Python

```python
import os

from utilia_sdk import CreateInvoiceBillingAddress, IdentifyUserInput, UtiliaSDK

async with UtiliaSDK(
    base_url="https://os.utilia.ai/api",
    api_key=os.environ["UTILIA_API_KEY"],
) as sdk:
    user = await sdk.users.identify(
        IdentifyUserInput(
            external_id="user_01HXYZ",
            email="cliente@empresa.com",
            name="Empresa de Canarias SL",
            tax_id="B76543210",
            billing_address=CreateInvoiceBillingAddress(
                street="Avenida de Canarias 12, planta 3",
                city="Las Palmas de Gran Canaria",
                postal_code="35001",
                country="España",
                province="Las Palmas",
            ),
        )
    )

    print(user.client_id)
```

## Errores propios de estos endpoints

- `403 MODULE_NOT_ENABLED`: el plan de tu organización no incluye el aplicativo Integraciones.
- `403 INSUFFICIENT_SCOPE`: falta `crm:users:write` al identificar o `crm:users:read` al listar. Que falte `crm:users:read:fiscal-data` no da error: los datos fiscales guardados, simplemente, no vienen.
- `404 CLIENT_NOT_ACCESSIBLE`: el `clientId` no es de tu organización, ya está vinculado a un usuario de otra aplicación o es una ficha nativa del CRM que esta aplicación no creó.
- `400 VALIDATION_ERROR`: el cuerpo no cumple las restricciones (correo mal formado, `externalId` vacío) o `limit` pasa de 100.

## Siguiente paso

Con los usuarios identificados, tu aplicación puede emitir facturas en
su nombre con los [endpoints de facturas](https://os.utilia.ai/docs/developers/api/invoices)
o abrir tickets de soporte con los
[endpoints de tickets](https://os.utilia.ai/docs/developers/api/tickets).
