# Zapini API — Kanban

**Versión:** 1.0.0
**Base URL:** `https://zapini.app/api/v1`

---

## Autenticación

Todos los endpoints requieren un Bearer Token:

```
Authorization: Bearer sk_your_api_token
Accept: application/json
Content-Type: application/json
```

Genera tokens de API en el panel admin en **API Docs → Gestionar Tokens**.

---

## API Kanban

**Ruta base:** `/api/v1/kanban`
**Requisito:** `kanban_enabled` debe ser true en tu cuenta de tenant.

> **Prefijo canónico: `/api/v1/crm`.** El antiguo `/api/v1/kanban` sigue
> respondiendo exactamente las mismas rutas, con el mismo formato — ninguna
> integración publicada necesita cambiar. Los ejemplos siguientes usan ambos.


La API Kanban proporciona acceso CRUD completo a tableros, columnas y leads. Úsala para crear integraciones de CRM, automatizar la gestión de pipeline o sincronizar con herramientas externas.

---

## Tableros

### Listar Tableros
`GET /api/v1/kanban/boards`

Retorna todos los tableros Kanban para la instancia autenticada.

```bash
curl -s -H "Authorization: Bearer sk_your_token" \
  "https://zapini.app/api/v1/kanban/boards"
```

**Respuesta:**
```json
{
  "success": true,
  "data": [
    {
      "uuid": "board-uuid",
      "name": "Sales Pipeline",
      "description": null,
      "is_default": true,
      "whatsapp_instance_uuid": "instance-uuid",
      "auto_detect_leads_enabled": false,
      "auto_detect_score_threshold": 50,
      "columns_count": 6,
      "created_at": "2026-01-01T00:00:00+00:00"
    }
  ]
}
```

---

### Crear Tablero
`POST /api/v1/kanban/boards`

Crea un nuevo tablero con columnas predeterminadas (Nuevo Lead, Calificado, Propuesta, Negociación, Ganado, Perdido).

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| name | string | Sí | Nombre del tablero |
| description | string | No | Descripción del tablero |
| is_default | boolean | No | Establecer como tablero predeterminado |

```bash
curl -s -X POST \
  -H "Authorization: Bearer sk_your_token" \
  -H "Content-Type: application/json" \
  -d '{"name":"Q2 Pipeline","is_default":false}' \
  "https://zapini.app/api/v1/kanban/boards"
```

---

### Obtener Tablero
`GET /api/v1/kanban/boards/{uuid}`

Retorna detalles del tablero con estadísticas y columnas.

```bash
curl -s -H "Authorization: Bearer sk_your_token" \
  "https://zapini.app/api/v1/kanban/boards/board-uuid"
```

**Respuesta incluye `stats`:**
```json
{
  "stats": {
    "total_leads": 12,
    "open_leads": 9,
    "won_leads": 2,
    "lost_leads": 1,
    "total_value": 15000.00,
    "won_value": 5000.00,
    "conversion_rate": 66.7,
    "avg_time_to_close": 14.5
  }
}
```

---

### Actualizar Tablero
`PUT /api/v1/kanban/boards/{uuid}`

| Campo | Tipo | Descripción |
|-------|------|-------------|
| name | string | Nombre del tablero |
| description | string | Descripción del tablero |
| is_default | boolean | Establecer como predeterminado (desmarca los demás) |
| auto_detect_leads_enabled | boolean | Habilitar detección automática de leads |
| auto_detect_score_threshold | integer | Puntuación mínima (0–100) para detección automática |

---

### Eliminar Tablero
`DELETE /api/v1/kanban/boards/{uuid}`

Elimina el tablero y todas sus columnas y leads.

---

## Columnas

### Listar Columnas
`GET /api/v1/kanban/boards/{board_uuid}/columns`

Retorna columnas ordenadas por posición.

---

### Crear Columna
`POST /api/v1/kanban/boards/{board_uuid}/columns`

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| name | string | Sí | Nombre de la columna |
| color | string | No | Color hex (ej. `#3B82F6`) |
| position | integer | No | Posición de la columna (al final si se omite) |
| wip_limit | integer | No | Máximo de leads en esta columna |
| is_final | boolean | No | Si esta es una columna de etapa final |

---

### Reordenar Columnas
`POST /api/v1/kanban/boards/{board_uuid}/columns/reorder`

```json
{
  "order": ["col-uuid-1", "col-uuid-2", "col-uuid-3"]
}
```

---

### Actualizar Columna
`PUT /api/v1/kanban/columns/{uuid}`

| Campo | Tipo | Descripción |
|-------|------|-------------|
| name | string | Nombre de la columna |
| color | string | Color hex |
| wip_limit | integer | Límite WIP |
| is_final | boolean | Flag de etapa final |
| min_ai_score | integer | Puntuación mínima de IA para enrutamiento automático |
| max_ai_score | integer | Puntuación máxima de IA para enrutamiento automático |

---

### Eliminar Columna
`DELETE /api/v1/kanban/columns/{uuid}`

| Consulta | Tipo | Descripción |
|---------|------|-------------|
| move_leads_to_uuid | string | UUID de columna para mover los leads (opcional) |

---

## Leads

### Listar Leads
`GET /api/v1/kanban/boards/{board_uuid}/leads`

| Parámetro | Tipo | Por Defecto | Descripción |
|-----------|------|-------------|-------------|
| status | string | open | `open` \| `won` \| `lost` \| `all` |
| priority | string | — | `low` \| `medium` \| `high` \| `urgent` |
| column_uuid | string | — | Filtrar por columna |
| search | string | — | Buscar por título, teléfono, email, notas |

```bash
curl -s -H "Authorization: Bearer sk_your_token" \
  "https://zapini.app/api/v1/kanban/boards/board-uuid/leads?status=open&priority=high"
```

---

### Crear Lead
`POST /api/v1/kanban/leads`

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| board_uuid | string | Sí | UUID del tablero destino |
| column_uuid | string | No | Columna destino (primera columna si se omite) |
| title | string | Sí | Título del lead |
| phone_number | string | Sí | Número de teléfono del contacto |
| email | string | No | Email del contacto |
| value | decimal | No | Valor del negocio |
| priority | string | No | `low` \| `medium` \| `high` \| `urgent` |
| notes | string | No | Notas iniciales |
| due_date | date | No | Fecha límite (YYYY-MM-DD) |
| custom_fields | object | No | Campos personalizados clave-valor |

```bash
curl -s -X POST \
  -H "Authorization: Bearer sk_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "board_uuid": "board-uuid",
    "title": "John Doe - Pro Plan",
    "phone_number": "5511999999999",
    "value": 299.00,
    "priority": "high"
  }' \
  "https://zapini.app/api/v1/kanban/leads"
```

**Respuesta:**
```json
{
  "success": true,
  "data": {
    "uuid": "lead-uuid",
    "title": "John Doe - Pro Plan",
    "phone_number": "5511999999999",
    "value": "299.00",
    "value_formatted": "R$ 299,00",
    "priority": "high",
    "priority_label": "High",
    "source": "api",
    "ai_score": null,
    "is_open": true,
    "is_won": false,
    "is_lost": false,
    "is_overdue": false,
    "column": {
      "uuid": "col-uuid",
      "name": "New Lead",
      "color": "#3B82F6"
    },
    "created_at": "2026-01-01T00:00:00+00:00"
  }
}
```

---

### Obtener Lead
`GET /api/v1/kanban/leads/{uuid}`

Retorna detalles completos del lead incluyendo notes, custom_fields, ai_analysis y lost_reason.

---

### Actualizar Lead
`PUT /api/v1/kanban/leads/{uuid}`

| Campo | Tipo | Descripción |
|-------|------|-------------|
| title | string | Título del lead |
| phone_number | string | Número de teléfono |
| email | string | Email |
| value | decimal | Valor del negocio |
| priority | string | Nivel de prioridad |
| notes | string | Notas |
| due_date | date | Fecha límite |
| custom_fields | object | Campos personalizados |

---

### Eliminar Lead
`DELETE /api/v1/kanban/leads/{uuid}`

Eliminación suave del lead (puede recuperarse desde la base de datos).

---

### Mover Lead
`POST /api/v1/kanban/leads/{uuid}/move`

```json
{
  "column_uuid": "target-column-uuid",
  "position": 0
}
```

Si `position` se omite, el lead se agrega al final. Mover a una columna final "Ganado" o "Perdido" establece automáticamente el estado del lead.

---

### Marcar como Ganado
`POST /api/v1/kanban/leads/{uuid}/won`

```json
{
  "value": 299.00
}
```

---

### Marcar como Perdido
`POST /api/v1/kanban/leads/{uuid}/lost`

```json
{
  "reason": "Budget constraints"
}
```

---

### Agregar Nota
`POST /api/v1/kanban/leads/{uuid}/note`

```json
{
  "note": "Interested in annual subscription. Follow up next week."
}
```

Las notas se agregan con marca de tiempo y nombre de usuario.

---

### Obtener Actividades
`GET /api/v1/kanban/leads/{uuid}/activities`

Retorna el historial completo de actividades de un lead.

**Respuesta:**
```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "type": "created",
      "description": null,
      "metadata": { "column_name": "New Lead" },
      "user": { "id": 1, "name": "John" },
      "created_at": "2026-01-01T00:00:00+00:00",
      "created_at_human": "2 days ago"
    }
  ]
}
```

---

### Calificar Lead con IA
`POST /api/v1/kanban/leads/{uuid}/ai-qualify`

Ejecuta análisis de IA en el lead (requiere integración de IA configurada).

**Respuesta:**
```json
{
  "success": true,
  "data": {
    "lead": { "uuid": "...", "ai_score": 82 },
    "ai_result": {
      "score": 82,
      "analysis": { "summary": "High-intent lead...", "strengths": [] }
    }
  }
}
```

---

## Búsqueda de leads en todo el CRM

`GET /api/v1/crm/leads`

Busca leads en todo el tenant, sin necesidad de saber en qué tablero están — la
forma de responder "¿este teléfono ya tiene tarjeta?". **Siempre paginado.**

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|----------------|-------------|
| search | string | — | Título, teléfono, correo u observaciones |
| phone_number | string | — | Compara solo los dígitos: `+55 11 9…`, `55119…` y `(11) 9…` encuentran el mismo lead |
| board_uuid | string | — | Restringir a un tablero |
| column_uuid | string | — | Restringir a una columna |
| status | string | all | `open` \| `won` \| `lost` \| `all` |
| priority | string | — | `low` \| `medium` \| `high` \| `urgent` |
| source | string | — | `api`, `conversation`, `widget`, `auto_detect`… |
| assigned_to | int | — | Id del responsable (primario o no) |
| created_from / created_to | date | — | Período de creación |
| sort | string | created_at | `created_at`, `updated_at`, `value`, `ai_score`, `due_date`, `title`, `last_activity_at`, `position` |
| direction | string | desc | `asc` \| `desc` |
| per_page | int | 50 | 1–200 |

```bash
curl -s -H "Authorization: Bearer sk_tu_token" \
  "https://zapini.app/api/v1/crm/leads?phone_number=11999990001&status=all"
```

La respuesta trae `meta.pagination` y cada lead incluye su tablero:

```json
{
  "success": true,
  "data": [
    { "uuid": "…", "title": "Juan", "board": { "uuid": "…", "name": "Ventas" } }
  ],
  "meta": { "pagination": { "total": 137, "per_page": 50, "current_page": 1, "last_page": 3 } }
}
```

---

## Paginación de la lista del tablero

`GET /api/v1/crm/boards/{board_uuid}/leads`

Sin `per_page` la respuesta sigue siendo la lista completa (la app arma todas las
columnas de una vez). Con `per_page` pagina — eso es lo que evita traer miles de
tarjetas para leer las primeras. También acepta `assigned_to`, `created_from`,
`created_to`, `sort` y `direction`.

---

## Importación masiva

`POST /api/v1/crm/boards/{board_uuid}/leads/import`

Hasta **500 leads** por llamada. Cada fila recorre el mismo camino que
`POST /crm/leads` — valida campos personalizados, numera secuenciales, resuelve
fórmulas —, así que lo importado y lo escrito a mano producen el mismo registro.

Nunca es todo o nada: las filas buenas entran y las malas vuelven con su índice y
el error.

| Campo | Tipo | Descripción |
|-------|------|-------------|
| leads[] | array | Obligatorio, 1–500 filas |
| leads[].title | string | Obligatorio |
| leads[].phone_number | string | Obligatorio fuera del CRM modular |
| leads[].column_uuid / column_name | string | Columna destino (el nombre no distingue mayúsculas) |
| leads[].custom_fields | object | Campos personalizados del tablero |
| leads[].assignee_ids | int[] | Responsables |
| skip_duplicates | bool | Omite el teléfono que ya tiene tarjeta en el tablero |

```json
{
  "success": true,
  "data": {
    "created_count": 2, "skipped_count": 1, "error_count": 1,
    "created": [{ "index": 0, "uuid": "…", "title": "Importado 1" }],
    "errors":  [{ "index": 3, "error": "phone_number is required" }]
  }
}
```

---

## Operaciones sobre el lead

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| POST | `/crm/leads/{uuid}/assign` | Define los responsables. `assignee_ids: []` los quita; `primary_id` debe estar en la lista |
| POST | `/crm/leads/{uuid}/ignore` | Descarta el lead **y** veta el número en la detección automática. Borrar sin ignorar hace que la tarjeta vuelva sola |
| POST | `/crm/leads/{uuid}/convert-to-ticket` | Abre un ticket desde el lead. Idempotente: un lead ya convertido devuelve el ticket existente con `already_existed: true` |
| POST | `/crm/leads/{uuid}/ai-suggest` | Próxima acción sugerida por la IA |
| POST | `/crm/leads/from-messages` | Crea un lead a partir de mensajes específicos (`message_ids`, hasta 100) |

---

## Adjuntos del lead

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/crm/leads/{uuid}/attachments` | Listar |
| POST | `/crm/leads/{uuid}/attachments` | Subir (multipart, campo `file`) |
| GET | `/crm/leads/{uuid}/attachments/{attachment_uuid}` | Descargar |
| DELETE | `/crm/leads/{uuid}/attachments/{attachment_uuid}` | Eliminar |

Límites: 25 MB por archivo, 10 adjuntos por lead. Disco privado — la descarga la
sirve el endpoint, nunca una URL pública.

---

## Tablero: estadísticas y duplicación

`GET /api/v1/crm/boards/{board_uuid}/stats`

```json
{
  "success": true,
  "data": {
    "board_uuid": "…",
    "stats": { "total_leads": 120, "open_leads": 80, "won_leads": 30, "lost_leads": 10,
               "total_value": 45000, "won_value": 22000, "conversion_rate": 25 },
    "by_column": [ { "uuid": "…", "name": "Nuevo", "leads_count": 12, "total_value": 3400 } ]
  }
}
```

`POST /api/v1/crm/boards/{board_uuid}/duplicate` — copia el tablero con sus
columnas (sin los leads). Campo obligatorio: `name`.

`POST /api/v1/crm/columns/{column_uuid}/clear` — elimina todos los leads de la
columna y devuelve `deleted`. **Destructivo y sin confirmación.**

---

## Automatizaciones

| Método | Endpoint |
|--------|----------|
| GET | `/crm/boards/{board_uuid}/automations` |
| POST | `/crm/boards/{board_uuid}/automations` |
| PUT | `/crm/automations/{uuid}` |
| DELETE | `/crm/automations/{uuid}` |
| POST | `/crm/automations/{uuid}/test` |

**Disparadores:** `lead_created`, `lead_moved`, `lead_idle`, `message_received`,
`scheduled`, `ai_score_changed`.

**Acciones:** `move_column`, `assign_user`, `send_message`, `add_tag`,
`ai_qualify`, `ai_summarize`, `notify`, `webhook`.

La configuración se verifica al escribir: `move_column` exige un
`action_config.column_id` de **este** tablero y `assign_user` un
`action_config.user_id` de **esta** cuenta — de lo contrario la automatización
solo fallaría meses después, en su primer disparo.

`POST /crm/automations/{uuid}/test` recibe `lead_uuid` y ejecuta la
automatización contra un lead real, para verificar la configuración antes de
dejarla disparar sola.

---

## Detección automática de leads

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/crm/boards/{uuid}/auto-detection` | Reglas actuales + etiquetas disponibles |
| PUT | `/crm/boards/{uuid}/auto-detection` | Guardar las reglas |
| POST | `/crm/boards/{uuid}/auto-detection/preview` | Lo que **se importaría**, sin crear nada |
| POST | `/crm/boards/{uuid}/auto-detection/sync` | Crear los leads de los elementos elegidos (`items[]`, hasta 100) |
| GET | `/crm/boards/{uuid}/ignored-contacts` | Contactos vetados |
| POST | `/crm/boards/{uuid}/ignored-contacts` | Devolver un contacto a la detección (`contact_number`) |

---

## Campos personalizados (CRM modular)

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/crm/field-definitions` | Esquema para armar el formulario (pasa `board_uuid`) |
| GET | `/crm/boards/{uuid}/fields` | Gestión: incluye campos desactivados y cuántos registros ya tienen valor en cada uno |
| POST | `/crm/boards/{uuid}/fields` | Crear campo |
| POST | `/crm/boards/{uuid}/fields/reorder` | Reordenar (`ids[]` en el orden deseado) |
| PUT | `/crm/fields/{id}` | Actualizar |
| DELETE | `/crm/fields/{id}` | Quitar del formulario |

**Herencia:** un tablero sin campos propios usa el conjunto global del tenant. El
primer campo propio COPIA el conjunto global antes de guardar — la respuesta trae
`copied_from_global` con cuántos vinieron. Sin eso el formulario se encogería al
único campo recién creado.

**`name` no es editable:** es la clave dentro de `custom_fields` de cada lead.
Renombrarla dejaría huérfano todo valor ya guardado. Quitar un campo lo saca del
formulario pero **mantiene** los valores — la respuesta devuelve `kept_values`, y
recrear un campo con el mismo `name` lo trae todo de vuelta.

---

## Configuración y apoyo

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/crm/config` | `crm_layout` (`standard`, `tms` o `vc`) y los interruptores de partes de la pantalla. **Léelo antes de dibujar cualquier interfaz** |
| GET | `/crm/users` | Usuarios que pueden ser responsables de un lead |
| GET | `/crm/tags` | Etiquetas del tenant, usadas en los filtros de detección |

---

## Webhooks de eventos del CRM

Una **integración personalizada** (`/integrations/custom`) con URL de webhook y
el ámbito `kanban.read` recibe, en tiempo real:

| Evento | Cuándo |
|--------|--------|
| `crm.lead.created` | Lead creado |
| `crm.lead.updated` | Cambió título, teléfono, correo, valor, prioridad, observaciones, vencimiento, campos personalizados, responsable o puntuación de IA |
| `crm.lead.moved` | El lead cambió de columna (trae `from_column_id`) |
| `crm.lead.won` | Lead ganado |
| `crm.lead.lost` | Lead perdido |
| `crm.lead.deleted` | Lead eliminado individualmente |
| `crm.board.deleted` | Tablero eliminado — trae `leads_count` y `columns_count` |

Se disparan por **cualquier** camino: panel, API, formulario de sitio,
importación, detección automática, automatización o IA. Son eventos del tenant y
no llevan `instance_uuid`. Movimiento, ganado y perdido tienen evento propio y
**no** se repiten como `crm.lead.updated`; los sellos de rutina
(`last_activity_at`, `position`, contadores de IA) no generan ningún evento.

**Eliminar un tablero es el único caso sin evento por lead.** Sus leads y
columnas se van por la cascada de la base de datos, que nunca pasa por Eloquent
— un tablero con 5.000 tarjetas se convertiría en 5.000 entregas por una sola
acción del operador. En su lugar se envía un único `crm.board.deleted`, con
`leads_count` y `columns_count`: quien espeja el CRM descarta el tablero entero
y sigue.

Cada entrega se firma en `X-Zapini-Signature: sha256=<hmac>` sobre el cuerpo
crudo — igual que los demás webhooks documentados en `/developers`.

---

## Códigos de Error

| Código | HTTP | Descripción |
|--------|------|-------------|
| `NOT_FOUND` | 404 | Recurso no encontrado |
| `FORBIDDEN` | 403 | Kanban no habilitado para esta cuenta |
| `VALIDATION_ERROR` | 422 | Error de validación — verifica `error.details` |
| `MOVE_FAILED` | 422 | No se puede mover el lead (ej: límite WIP alcanzado) |
| `NO_AI_PROVIDER` | 422 | No hay integración de IA configurada |
| `INSTANCE_REQUIRED` | 400 | `instance_uuid` requerido para autenticación Sanctum |
| `NO_BOARD` | 422 | No se encontró tablero Kanban para el tenant |
| `UNAUTHORIZED` | 401 | Token inválido o faltante |
| `SERVER_ERROR` | 500 | Error interno del servidor |
| `INSUFFICIENT_SCOPE` | 403 | La clave `zpk_` no tiene `kanban.read`/`kanban.write` |
| `INVALID_AUTOMATION_CONFIG` | 422 | La acción apunta a una columna o usuario fuera de esta cuenta |
| `INVALID_FIELD_REFERENCE` | 422 | `show_if`/`label_if`/fórmula apuntan a un campo inexistente |
| `GLOBAL_FIELD_NOT_REMOVABLE` | 422 | Un campo global del tenant no se puede quitar por tablero |
| `INVALID_TAG_IDS` | 422 | Etiqueta de otro tenant en los filtros de detección |
| `TICKETS_NOT_ENABLED` | 422 | Integración de Tickets desactivada — no se puede convertir |
| `LEAD_BOARD_MISMATCH` | 422 | El lead de prueba no pertenece al tablero de la automatización |
| `LEAD_CREATE_FAILED` | 422 | No se pudo crear el lead a partir de los mensajes |

---

*Generado por Zapini — https://zapini.app*
