# Zapini API — Kanban

**Versão:** 1.0.0
**Base URL:** `https://zapini.app/api/v1`

---

## Autenticação

Todos os endpoints requerem um Bearer Token:

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

Gere tokens de API no painel admin em **API Docs → Gerenciar Tokens**.

---

## Kanban API

**Base path:** `/api/v1/kanban`
**Requisito:** `kanban_enabled` deve ser true na sua conta de tenant.

> **Prefixo canônico: `/api/v1/crm`.** O antigo `/api/v1/kanban` continua
> respondendo exatamente as mesmas rotas, com o mesmo formato — nenhuma
> integração publicada precisa mudar. Os exemplos abaixo usam ambos.


A API Kanban fornece acesso CRUD completo a quadros, colunas e leads. Use-a para criar integrações de CRM, automatizar o gerenciamento de pipeline ou sincronizar com ferramentas externas.

---

## Quadros

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

Retorna todos os quadros Kanban para a instância autenticada.

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

**Resposta:**
```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"
    }
  ]
}
```

---

### Criar Quadro
`POST /api/v1/kanban/boards`

Cria um novo quadro com colunas padrão (Novo Lead, Qualificado, Proposta, Negociação, Ganho, Perdido).

| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| name | string | Sim | Nome do quadro |
| description | string | Não | Descrição do quadro |
| is_default | boolean | Não | Definir como quadro padrão |

```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"
```

---

### Obter Quadro
`GET /api/v1/kanban/boards/{uuid}`

Retorna detalhes do quadro com estatísticas e colunas.

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

**Resposta inclui `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
  }
}
```

---

### Atualizar Quadro
`PUT /api/v1/kanban/boards/{uuid}`

| Campo | Tipo | Descrição |
|-------|------|-----------|
| name | string | Nome do quadro |
| description | string | Descrição do quadro |
| is_default | boolean | Definir como padrão (desmarca os outros) |
| auto_detect_leads_enabled | boolean | Habilitar detecção automática de leads |
| auto_detect_score_threshold | integer | Pontuação mínima (0–100) para detecção automática |

---

### Excluir Quadro
`DELETE /api/v1/kanban/boards/{uuid}`

Exclui o quadro e todas as suas colunas e leads.

---

## Colunas

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

Retorna colunas ordenadas por posição.

---

### Criar Coluna
`POST /api/v1/kanban/boards/{board_uuid}/columns`

| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| name | string | Sim | Nome da coluna |
| color | string | Não | Cor hexadecimal (ex: `#3B82F6`) |
| position | integer | Não | Posição da coluna (adicionada no final se omitida) |
| wip_limit | integer | Não | Máximo de leads nesta coluna |
| is_final | boolean | Não | Se esta é uma coluna de estágio final |

---

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

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

---

### Atualizar Coluna
`PUT /api/v1/kanban/columns/{uuid}`

| Campo | Tipo | Descrição |
|-------|------|-----------|
| name | string | Nome da coluna |
| color | string | Cor hexadecimal |
| wip_limit | integer | Limite WIP |
| is_final | boolean | Flag de estágio final |
| min_ai_score | integer | Pontuação mínima de IA para roteamento automático |
| max_ai_score | integer | Pontuação máxima de IA para roteamento automático |

---

### Excluir Coluna
`DELETE /api/v1/kanban/columns/{uuid}`

| Consulta | Tipo | Descrição |
|---------|------|-----------|
| move_leads_to_uuid | string | UUID da coluna para mover os leads (opcional) |

---

## Leads

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

| Parâmetro | Tipo | Padrão | Descrição |
|-----------|------|--------|-----------|
| status | string | open | `open` \| `won` \| `lost` \| `all` |
| priority | string | — | `low` \| `medium` \| `high` \| `urgent` |
| column_uuid | string | — | Filtrar por coluna |
| search | string | — | Pesquisar por título, telefone, e-mail, observações |

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

---

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

| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| board_uuid | string | Sim | UUID do quadro alvo |
| column_uuid | string | Não | Coluna alvo (primeira coluna se omitido) |
| title | string | Sim | Título do lead |
| phone_number | string | Sim | Número de telefone do contato |
| email | string | Não | E-mail do contato |
| value | decimal | Não | Valor do negócio |
| priority | string | Não | `low` \| `medium` \| `high` \| `urgent` |
| notes | string | Não | Observações iniciais |
| due_date | date | Não | Data de vencimento (YYYY-MM-DD) |
| custom_fields | object | Não | Campos personalizados chave-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"
```

**Resposta:**
```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"
  }
}
```

---

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

Retorna detalhes completos do lead incluindo notes, custom_fields, ai_analysis e lost_reason.

---

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

| Campo | Tipo | Descrição |
|-------|------|-----------|
| title | string | Título do lead |
| phone_number | string | Número de telefone |
| email | string | E-mail |
| value | decimal | Valor do negócio |
| priority | string | Nível de prioridade |
| notes | string | Observações |
| due_date | date | Data de vencimento |
| custom_fields | object | Campos personalizados |

---

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

Exclusão suave do lead (pode ser recuperado do banco de dados).

---

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

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

Se `position` for omitido, o lead é adicionado no final. Mover para uma coluna final "Ganho" ou "Perdido" define automaticamente o status do lead.

---

### Marcar como Ganho
`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"
}
```

---

### Adicionar Observação
`POST /api/v1/kanban/leads/{uuid}/note`

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

Observações são adicionadas com timestamp e nome do usuário.

---

### Obter Atividades
`GET /api/v1/kanban/leads/{uuid}/activities`

Retorna o histórico completo de atividades de um lead.

**Resposta:**
```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"
    }
  ]
}
```

---

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

Executa análise de IA no lead (requer integração de IA configurada).

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

---

## Busca de Leads em todo o CRM

`GET /api/v1/crm/leads`

Busca leads do tenant inteiro, sem precisar saber em que quadro estão — o caminho
para responder "este telefone já tem card?". **Sempre paginado.**

| Parâmetro | Tipo | Padrão | Descrição |
|-----------|------|--------|-----------|
| search | string | — | Título, telefone, e-mail ou observações |
| phone_number | string | — | Casa só os dígitos: `+55 11 9…`, `55119…` e `(11) 9…` encontram o mesmo lead |
| board_uuid | string | — | Restringir a um quadro |
| column_uuid | string | — | Restringir a uma coluna |
| status | string | all | `open` \| `won` \| `lost` \| `all` |
| priority | string | — | `low` \| `medium` \| `high` \| `urgent` |
| source | string | — | `api`, `conversation`, `widget`, `auto_detect`… |
| assigned_to | int | — | Id do responsável (primário ou não) |
| created_from / created_to | date | — | Período de criação |
| 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_seu_token" \
  "https://zapini.app/api/v1/crm/leads?phone_number=11999990001&status=all"
```

A resposta traz `meta.pagination` e cada lead inclui o quadro a que pertence:

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

---

## Paginação da lista do quadro

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

Sem `per_page` a resposta continua sendo a lista completa (o app monta as colunas
de uma vez). Com `per_page`, pagina — é o que evita puxar milhares de cards para
ler os primeiros. Aceita também `assigned_to`, `created_from`, `created_to`,
`sort` e `direction`.

---

## Importação em massa

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

Até **500 leads** por chamada. Cada linha percorre o mesmo caminho do
`POST /crm/leads` — valida campos customizados, numera sequenciais, resolve
fórmulas —, então importado e digitado produzem o mesmo registro.

Nunca é tudo-ou-nada: as linhas boas entram e as ruins voltam com o índice e o erro.

| Campo | Tipo | Descrição |
|-------|------|-----------|
| leads[] | array | Obrigatório, 1–500 linhas |
| leads[].title | string | Obrigatório |
| leads[].phone_number | string | Obrigatório fora do CRM modular |
| leads[].column_uuid / column_name | string | Coluna de destino (nome casa sem diferenciar maiúsculas) |
| leads[].custom_fields | object | Campos customizados do quadro |
| leads[].assignee_ids | int[] | Responsáveis |
| skip_duplicates | bool | Pula o telefone que já tem card no quadro |

```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" }]
  }
}
```

---

## Operações sobre o lead

| Método | Endpoint | Descrição |
|--------|----------|-----------|
| POST | `/crm/leads/{uuid}/assign` | Define os responsáveis. `assignee_ids: []` remove todos; `primary_id` precisa estar na lista |
| POST | `/crm/leads/{uuid}/ignore` | Descarta o lead **e** veta o número na detecção automática. Apagar sem ignorar faz o card voltar sozinho |
| POST | `/crm/leads/{uuid}/convert-to-ticket` | Abre um ticket a partir do lead. Idempotente: um lead já convertido devolve o ticket existente com `already_existed: true` |
| POST | `/crm/leads/{uuid}/ai-suggest` | Próxima ação sugerida pela IA |
| POST | `/crm/leads/from-messages` | Cria um lead a partir de mensagens específicas (`message_ids`, até 100) |

---

## Anexos do lead

| Método | Endpoint | Descrição |
|--------|----------|-----------|
| GET | `/crm/leads/{uuid}/attachments` | Listar |
| POST | `/crm/leads/{uuid}/attachments` | Enviar (multipart, campo `file`) |
| GET | `/crm/leads/{uuid}/attachments/{attachment_uuid}` | Baixar |
| DELETE | `/crm/leads/{uuid}/attachments/{attachment_uuid}` | Excluir |

Limite: 25 MB por arquivo, 10 anexos por lead. Disco privado — o download é
servido pelo endpoint, nunca por URL pública.

---

## Quadro: estatísticas e duplicação

`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": "Novo", "leads_count": 12, "total_value": 3400 } ]
  }
}
```

`POST /api/v1/crm/boards/{board_uuid}/duplicate` — copia o quadro com suas
colunas (sem os leads). Campo obrigatório: `name`.

`POST /api/v1/crm/columns/{column_uuid}/clear` — apaga todos os leads da coluna
e devolve `deleted`. **Destrutivo e sem confirmação.**

---

## Automações

| 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` |

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

**Ações:** `move_column`, `assign_user`, `send_message`, `add_tag`, `ai_qualify`,
`ai_summarize`, `notify`, `webhook`.

A configuração é conferida na criação: `move_column` exige
`action_config.column_id` de uma coluna **deste** quadro e `assign_user` exige
`action_config.user_id` de um usuário **desta** conta — senão a automação só
falharia meses depois, no primeiro disparo.

`POST /crm/automations/{uuid}/test` recebe `lead_uuid` e roda a automação contra
um lead real, para conferir a configuração antes de deixá-la disparar sozinha.

---

## Detecção automática de leads

| Método | Endpoint | Descrição |
|--------|----------|-----------|
| GET | `/crm/boards/{uuid}/auto-detection` | Regras atuais + etiquetas disponíveis |
| PUT | `/crm/boards/{uuid}/auto-detection` | Salvar as regras |
| POST | `/crm/boards/{uuid}/auto-detection/preview` | O que **seria** importado, sem criar nada |
| POST | `/crm/boards/{uuid}/auto-detection/sync` | Criar os leads dos itens escolhidos (`items[]`, até 100) |
| GET | `/crm/boards/{uuid}/ignored-contacts` | Contatos vetados |
| POST | `/crm/boards/{uuid}/ignored-contacts` | Devolver um contato à detecção (`contact_number`) |

---

## Campos customizados (CRM modular)

| Método | Endpoint | Descrição |
|--------|----------|-----------|
| GET | `/crm/field-definitions` | Esquema para montar o formulário (passe `board_uuid`) |
| GET | `/crm/boards/{uuid}/fields` | Gerência: inclui campo desligado e quantos cadastros já têm valor em cada |
| POST | `/crm/boards/{uuid}/fields` | Criar campo |
| POST | `/crm/boards/{uuid}/fields/reorder` | Reordenar (`ids[]` na ordem desejada) |
| PUT | `/crm/fields/{id}` | Atualizar |
| DELETE | `/crm/fields/{id}` | Remover do formulário |

**Herança:** um quadro sem campos próprios usa o conjunto global do tenant. O
primeiro campo próprio COPIA o conjunto global antes de gravar — a resposta traz
`copied_from_global` com quantos vieram junto. Sem isso o formulário encolheria
para o único campo recém-criado.

**`name` não é editável:** é a chave dentro de `custom_fields` de cada lead.
Renomeá-la deixaria órfão todo valor já gravado. Remover um campo tira-o do
formulário mas **mantém** os valores — a resposta devolve `kept_values`, e
recriar um campo com o mesmo `name` traz tudo de volta.

---

## Configuração e apoio

| Método | Endpoint | Descrição |
|--------|----------|-----------|
| GET | `/crm/config` | `crm_layout` (`standard`, `tms` ou `vc`) e as chaves que ligam pedaços da tela. **Leia antes de desenhar qualquer interface** |
| GET | `/crm/users` | Usuários que podem ser responsáveis por um lead |
| GET | `/crm/tags` | Etiquetas do tenant, usadas nos filtros de detecção |

---

## Webhooks de eventos do CRM

Uma **integração customizada** (`/integrations/custom`) com URL de webhook e o
escopo `kanban.read` recebe, em tempo real:

| Evento | Quando |
|--------|--------|
| `crm.lead.created` | Lead criado |
| `crm.lead.updated` | Título, telefone, e-mail, valor, prioridade, observações, vencimento, campos customizados, responsável ou pontuação de IA mudaram |
| `crm.lead.moved` | Lead trocou de coluna (traz `from_column_id`) |
| `crm.lead.won` | Lead ganho |
| `crm.lead.lost` | Lead perdido |
| `crm.lead.deleted` | Lead excluído individualmente |
| `crm.board.deleted` | Quadro apagado — traz `leads_count` e `columns_count` |

Disparam por **qualquer** caminho: painel, API, formulário de site, importação,
detecção automática, automação ou IA. São eventos do tenant e não carregam
`instance_uuid`. Movimentação, ganho e perda têm evento próprio e **não** se
repetem como `crm.lead.updated`; carimbos de rotina (`last_activity_at`,
`position`, contadores de IA) não geram evento nenhum.

**Apagar um quadro é o único caso sem evento por lead.** Os leads e as colunas
somem pela cascata do banco, que não passa pelo Eloquent — um quadro com 5 mil
cards viraria 5 mil entregas por uma única ação de operador. Em vez disso sai um
`crm.board.deleted` só, com `leads_count` e `columns_count`: quem espelha o CRM
descarta o quadro inteiro e segue.

Cada entrega é assinada em `X-Zapini-Signature: sha256=<hmac>` sobre o corpo
bruto — igual aos demais webhooks documentados em `/developers`.

---

## Códigos de Erro

| Código | HTTP | Descrição |
|--------|------|-----------|
| `NOT_FOUND` | 404 | Recurso não encontrado |
| `FORBIDDEN` | 403 | Kanban não habilitado para esta conta |
| `VALIDATION_ERROR` | 422 | Falha de validação — verifique `error.details` |
| `MOVE_FAILED` | 422 | Não é possível mover o lead (ex: limite WIP atingido) |
| `NO_AI_PROVIDER` | 422 | Nenhuma integração de IA configurada |
| `INSTANCE_REQUIRED` | 400 | `instance_uuid` necessário para autenticação Sanctum |
| `NO_BOARD` | 422 | Nenhum quadro Kanban encontrado para o tenant |
| `UNAUTHORIZED` | 401 | Token inválido ou ausente |
| `SERVER_ERROR` | 500 | Erro interno do servidor |
| `INSUFFICIENT_SCOPE` | 403 | A chave `zpk_` não tem `kanban.read`/`kanban.write` |
| `INVALID_AUTOMATION_CONFIG` | 422 | A ação aponta para coluna ou usuário de fora desta conta |
| `INVALID_FIELD_REFERENCE` | 422 | `show_if`/`label_if`/fórmula apontam para um campo inexistente |
| `GLOBAL_FIELD_NOT_REMOVABLE` | 422 | Campo global do tenant não é removível por quadro |
| `INVALID_TAG_IDS` | 422 | Etiqueta de fora do tenant nos filtros de detecção |
| `TICKETS_NOT_ENABLED` | 422 | Integração de Tickets desativada — não dá para converter |
| `LEAD_BOARD_MISMATCH` | 422 | O lead do teste não é do quadro da automação |
| `LEAD_CREATE_FAILED` | 422 | Não foi possível criar o lead a partir das mensagens |

---

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