# API de leads

Crea leads en el CRM desde tu servidor con POST /api/v1/leads y una clave de API: campos y máximos, límites, errores y qué pasa con el lead creado.

`POST /api/v1/leads` crea un lead en el CRM desde cualquier sistema tuyo: tu web, tu tienda en línea, un formulario de WordPress o un proceso automático. Es la vía más flexible, porque decides tú qué campos envías, y la adecuada cuando la petición sale de un servidor. Si quieres captar desde el navegador de tus visitantes sin programar, usa un [formulario insertado](https://os.utilia.ai/docs/developers/lead-forms/embed).

**Crear un lead desde tu servidor**

Participantes: Tu servidor, UTILIA OS y Tu equipo.

1. Tu servidor → UTILIA OS: `POST /api/v1/leads`. Con la cabecera X-API-Key y los datos del lead.
2. UTILIA OS (por su cuenta): Comprueba la clave (401) y su límite (429), el plan (403), los datos (400) y que el correo no esté ya en un lead (409).
3. UTILIA OS → Tu servidor (respuesta): 201: success y lead. Con las cabeceras X-RateLimit del contador de la clave.
4. UTILIA OS → Tu equipo: Lead nuevo en el CRM. Asignado a quien creó la clave, con aviso a las personas y departamentos elegidos en ella.

## Endpoint

**`POST /api/v1/leads`**

Crea un lead en el CRM

## Autenticación

Envía la clave en la cabecera `X-API-Key`:

```bash
X-API-Key: utl_tu_clave_de_api
```

Las claves se crean en [Ajustes › Claves de API](https://os.utilia.ai/dashboard/settings/api-keys), con **Nueva clave** y el tipo **Enviar leads desde tu web**, que solo crea leads. Para crearlas necesitas el permiso `API_CREDENTIALS_MANAGE`, y que el plan incluya el CRM con sus leads. Con `API_CREDENTIALS_VIEW` («Ver las claves de API») solo puedes consultarlas y ver su uso.

Una [clave de API de la organización](https://os.utilia.ai/docs/developers/organization-api-keys/overview) (`utk_…`) no sirve en esta ruta, que responde `401` con ella. Con una de esas claves, si incluye «CRM: escribir», los leads se crean con `POST /api/crm/leads`, la ruta que usa la aplicación.

Cada clave se abre pulsando su fila, en el grupo **Enviar leads**: en su ficha se activa o se desactiva, se eligen sus avisos de leads y se ve su uso. Al renovarla con **Renovar la clave**, al pie de la ficha, la actual deja de valer en el acto y la nueva solo se muestra una vez. Si tienes la verificación en dos pasos, crear o renovar una clave te pide el código.

En la misma pantalla, en el grupo **Aplicaciones externas**, aparecen también las claves de las aplicaciones externas de tu organización, con el nombre de su aplicación. Su ficha dice «Es la clave de la aplicación _nombre de la aplicación_. Se renueva y se retira desde su ficha.» (o, si tu plan no incluye Integraciones, «Es la clave de una aplicación externa.»). Esas no se renuevan, desactivan ni borran desde aquí: se gestionan desde la ficha de la aplicación, que se abre con **Abrir la aplicación** si puedes verla. Si se intenta por la API, la respuesta es `409` con `API_KEY_MANAGED_BY_EXTERNAL_APP`.

> **Atención: La clave no va en el navegador**
>
> Usa la clave solo en tu servidor. Si la pones en el JavaScript de una página, cualquiera puede leerla y crear leads en tu nombre. Si sospechas que se ha filtrado, desactívala o renuévala desde la misma pantalla.

## Campos del cuerpo

Cada campo de texto tiene un máximo de caracteres; si lo superas, la respuesta es 400. Es la causa más habitual de un 400 cuando los datos parecen correctos.

| Nombre | Tipo | Obligatorio | Por defecto | Descripción |
| - | - | - | - | - |
| `email` | `string` | Sí | | Correo principal. Formato válido. Hasta 255 caracteres. |
| `source` | `string` | Sí | | Origen del lead, texto libre ('landing-web', 'formulario-contacto'...). De él se deduce la categoría de origen que verás en el CRM. Hasta 100 caracteres. |
| `fullName` | `string` | No | | Nombre completo. Hasta 200 caracteres. Envía al menos uno de fullName, firstName o lastName. |
| `firstName` | `string` | No | | Nombre. Hasta 100 caracteres. |
| `lastName` | `string` | No | | Apellidos. Hasta 100 caracteres. |
| `secondaryEmail` | `string` | No | | Correo secundario, distinto del principal. Sirve para asociar al lead los correos que lleguen desde otra dirección. Hasta 255 caracteres. |
| `leadType` | `string` | No | `OTHER` | LEAD_MAGNET, DEMO_REQUEST, CONTACT_FORM, REFERRAL, EVENT, COLD_OUTREACH, INBOUND_CALL u OTHER. |
| `phone` | `string` | No | | Teléfono fijo. Hasta 50 caracteres. |
| `mobile` | `string` | No | | Teléfono móvil. Hasta 50 caracteres. |
| `companyName` | `string` | No | | Empresa. Hasta 200 caracteres. |
| `position` | `string` | No | | Cargo. Hasta 150 caracteres. |
| `website` | `string` | No | | Sitio web. Hasta 255 caracteres. |
| `notes` | `string` | No | | Notas. Sin máximo declarado. |
| `tags` | `string[]` | No | | Etiquetas para clasificar el lead. |
| `metadata` | `object` | No | | Datos propios en JSON. Anídalos bajo una clave tuya (ver más abajo). |
| `pageUrl` | `string` | No | | Página donde se captó el lead. Hasta 500 caracteres. |
| `clientTimestamp` | `string` | No | | Momento de la captación en tu sistema, en ISO 8601. |
| `utm` | `object` | No | | Etiquetas de la campaña: source, medium y term (hasta 100 caracteres), campaign (hasta 200) y content (hasta 100). |

**Nombre del lead.** Si no envías `fullName`, se construye uniendo `firstName` y `lastName` con lo que llegue; si envías `fullName`, manda ese. Sin ninguno de los tres, la respuesta es 400.

**Tus datos en `metadata`.** El objeto se mezcla en la raíz de la metadata del lead y se aplica el último, así que una clave tuya llamada `tracking`, `apiKey` o `leadForm` sustituiría los datos que guarda el servidor. Anida siempre lo tuyo bajo una clave propia: `{"metadata": {"miIntegracion": {"formId": 42}}}`.

**Campañas de correo.** Esta API acepta las etiquetas `utm`, pero no el identificador del destinatario de una campaña (`_ur`) que sí acepta el [envío de un formulario](https://os.utilia.ai/docs/developers/lead-forms/public-api). Un lead creado por aquí se atribuye a la campaña por sus etiquetas, nunca al envío concreto que abrió esa persona.

## Lo que guarda el servidor

Con cada lead, el servidor añade a `metadata` un bloque `tracking`:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `tracking.ip` | `string` | Sí | IP de origen de la petición. 'unknown' si no se puede saber. |
| `tracking.userAgent` | `string \| null` | Sí | Cabecera user-agent de la petición. |
| `tracking.referer` | `string \| null` | Sí | Cabecera referer. |
| `tracking.pageUrl` | `string \| null` | Sí | El pageUrl que enviaste. |
| `tracking.utm` | `object` | Sí | Las cinco claves source, medium, campaign, term y content, siempre presentes; las que no envíes valen null. |
| `tracking.clientTimestamp` | `string \| null` | Sí | El clientTimestamp que enviaste. |
| `tracking.serverTimestamp` | `string` | Sí | Momento en que el servidor creó el lead, en ISO 8601. |

Junto a `tracking`, la metadata incluye `apiKey` (identificador, nombre y prefijo de la clave usada) y, si la clave pertenece a un formulario de captación, `leadForm` (identificador y nombre). Todo se ve en la ficha del lead en el CRM.

## Qué pasa con el lead creado

- **Se asigna a quien creó la clave de API**, que figura también como su creador. No hay reparto por reglas: todos los leads de una clave van a la misma persona. Desde ahí puedes reasignarlos en el [CRM](https://os.utilia.ai/docs/guides/crm/overview).
- **Nace en estado `NEW`, etapa `INITIAL_CONTACT` y prioridad `MEDIUM`.**
- **Recibe una puntuación**: 20 puntos de partida, más 10 por `phone` o `mobile`, 15 por `companyName`, 10 por `position`, 5 por `website` y 5 por `source`, hasta un máximo de 100.
- **Toma la categoría de origen** que se deduce de `source`.
- **Avisa a las personas y departamentos** configurados en la clave.
- **Dispara el evento de lead creado**, que pueden escuchar las automatizaciones y los [avisos a tu servidor](https://os.utilia.ai/docs/developers/webhooks/events).
- **Suma a las estadísticas del formulario** si la clave pertenece a uno.

## Límites de peticiones

**Límites de la API de leads**

- Peticiones por clave: 100 por hora. Se elige entre 10 y 10.000 al crear la clave.
- Tope general por dirección IP: 600 por minuto. Para todo el tráfico que llega desde esa dirección.

El límite de una clave no se puede cambiar después, ni siquiera al renovarla: si necesitas otro, crea una clave nueva. Y aunque tu clave admita muchas peticiones por hora, reparte los envíos masivos en el tiempo para no chocar con el tope por dirección IP.

Las respuestas procesadas llevan `X-RateLimit-Limit` (máximo por hora de la clave), `X-RateLimit-Remaining` (lo que queda) y `X-RateLimit-Reset` (cuándo se reinicia el contador, en segundos Unix).

Al superar el límite de la clave, la respuesta 429 **no** lleva esas cabeceras ni `Retry-After`. Lee el campo `retryAfter` del cuerpo: los segundos que faltan para que se reinicie el contador.

```json
{
  "statusCode": 429,
  "message": "Límite de peticiones superado. Inténtalo de nuevo más tarde.",
  "retryAfter": 2841
}
```

## Respuesta (201)

```json
{
  "success": true,
  "lead": {
    "id": "clx1234567890",
    "email": "juan@empresa.com",
    "fullName": "Juan Pérez",
    "source": "landing-contacto",
    "leadType": "CONTACT_FORM",
    "status": "NEW",
    "createdAt": "2026-10-08T10:30:00.000Z"
  }
}
```

## Errores

| Código | Motivo |
| - | - |
| 400 | Falta un campo obligatorio o los tres de nombre, un correo no es válido, el principal y el secundario coinciden, o un campo supera su máximo. |
| 401 | Falta la clave, no es válida o ha caducado. |
| 403 | El plan de la organización dueña de la clave no incluye el CRM (`errorCode: MODULE_NOT_ENABLED`) o la capacidad de leads (`errorCode: CAPABILITY_NOT_ENABLED`). |
| 409 | Ya hay un lead no archivado con ese correo, como principal o como secundario, en tu organización. |
| 429 | Límite de la clave superado; el cuerpo trae `retryAfter`. |

El 403 de plan no se arregla reintentando ni con permisos: hay que ampliar el plan. Se distingue de otros 403 por el `errorCode`, como se explica en [Errores de plan](https://os.utilia.ai/docs/developers/sdk/plan-errors). Los duplicados (409) puedes revisarlos y fusionarlos desde los leads del CRM.

## Ejemplos

### curl

```bash
curl -X POST 'https://os.utilia.ai/api/v1/leads' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: utl_tu_clave_de_api' \
  -d '{
    "email": "juan@empresa.com",
    "fullName": "Juan Pérez",
    "source": "landing-contacto",
    "leadType": "CONTACT_FORM",
    "phone": "+34 912 345 678",
    "companyName": "Empresa SL",
    "position": "Director Comercial",
    "tags": ["web", "interesado"],
    "utm": {
      "source": "google",
      "medium": "cpc",
      "campaign": "captacion-2026"
    }
  }'
```

### fetch

```javascript
const response = await fetch('https://os.utilia.ai/api/v1/leads', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'utl_tu_clave_de_api',
  },
  body: JSON.stringify({
    email: 'juan@empresa.com',
    fullName: 'Juan Pérez',
    source: 'landing-contacto',
    leadType: 'CONTACT_FORM',
    phone: '+34 912 345 678',
    companyName: 'Empresa SL',
    position: 'Director Comercial',
    tags: ['web', 'interesado'],
    utm: {
      source: 'google',
      medium: 'cpc',
      campaign: 'captacion-2026',
    },
  }),
});

const data = await response.json();
console.log(data);
```

### Python

```python
import requests

response = requests.post(
    'https://os.utilia.ai/api/v1/leads',
    headers={
        'Content-Type': 'application/json',
        'X-API-Key': 'utl_tu_clave_de_api',
    },
    json={
        'email': 'juan@empresa.com',
        'fullName': 'Juan Pérez',
        'source': 'landing-contacto',
        'leadType': 'CONTACT_FORM',
        'phone': '+34 912 345 678',
        'companyName': 'Empresa SL',
        'position': 'Director Comercial',
        'tags': ['web', 'interesado'],
        'utm': {
            'source': 'google',
            'medium': 'cpc',
            'campaign': 'captacion-2026',
        },
    },
    timeout=30,
)

print(response.json())
```

### PHP

```php
$apiKey = 'utl_tu_clave_de_api';
$url = 'https://os.utilia.ai/api/v1/leads';

$data = [
    'email' => 'juan@empresa.com',
    'fullName' => 'Juan Pérez',
    'source' => 'landing-contacto',
    'leadType' => 'CONTACT_FORM',
    'phone' => '+34 912 345 678',
    'companyName' => 'Empresa SL',
    'position' => 'Director Comercial',
    'tags' => ['web', 'interesado'],
    'utm' => [
        'source' => 'google',
        'medium' => 'cpc',
        'campaign' => 'captacion-2026',
    ],
];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-API-Key: ' . $apiKey,
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
print_r($result);
```

### WordPress (Contact Form 7)

```php
// Integración con Contact Form 7 de WordPress.
// Añade este código al functions.php de tu tema
// o a un plugin propio.
add_action('wpcf7_mail_sent', function ($contact_form) {
    $submission = WPCF7_Submission::get_instance();
    if (!$submission) return;

    $posted = $submission->get_posted_data();

    $lead_data = [
        'email'       => sanitize_email($posted['your-email']),
        'fullName'    => sanitize_text_field($posted['your-name']),
        'source'      => 'wordpress-cf7',
        'leadType'    => 'CONTACT_FORM',
        'phone'       => sanitize_text_field($posted['your-phone'] ?? ''),
        'companyName' => sanitize_text_field($posted['your-company'] ?? ''),
        'notes'       => sanitize_textarea_field($posted['your-message'] ?? ''),
        'pageUrl'     => wp_get_referer(),
        'metadata'    => [
            'form_id'   => $contact_form->id(),
            'form_name' => $contact_form->title(),
        ],
    ];

    wp_remote_post('https://os.utilia.ai/api/v1/leads', [
        'headers' => [
            'Content-Type' => 'application/json',
            'X-API-Key'    => 'utl_tu_clave_de_api',
        ],
        'body'    => wp_json_encode($lead_data),
        'timeout' => 15,
    ]);
});
```

El ejemplo de WordPress se ejecuta en tu servidor después de que Contact Form 7 envíe su correo, así que la clave no llega nunca al navegador.

## Siguiente paso

Cuando el lead esté en el CRM, el flujo sigue allí: califícalo, conviértelo en oportunidad y, al ganarla, en cliente. Lo tienes en [Leads](https://os.utilia.ai/docs/guides/crm/leads). Si además quieres consultar o actualizar leads desde tu aplicación, con scopes y avisos, usa los [endpoints de leads de la API externa](https://os.utilia.ai/docs/developers/api/leads).
