# Atmos API — Referencia v1

> Documentación completa en texto de la API pública del CRM Atmos. Esta es la versión legible por máquinas de https://docs.atmos.com.mx/docs (la UI interactiva requiere JavaScript).

## API pública de Atmos

REST sobre `/api/v1/*`. Autenticación M2M con un **token de API** (`at-api-…`) emitido por
un administrador del portal.

### Autenticación

Cada petición lleva el token en la cabecera:

```
Authorization: Bearer at-api-XXXX.sk_YYYY
```

(También se acepta `X-Api-Key: at-api-XXXX.sk_YYYY`.) El token tiene **su propio scope**
(acciones CRUD por objeto + alcance de datos + acceso por campo), independiente del usuario
que lo creó. Solo un administrador crea, edita o revoca tokens.

### Quickstart

Listar contactos:

```
curl https://app.atmos.com.mx/api/v1/contacts \
  -H "Authorization: Bearer at-api-XXXX.sk_YYYY"
```

Crear un contacto:

```
curl -X POST https://app.atmos.com.mx/api/v1/contacts \
  -H "Authorization: Bearer at-api-XXXX.sk_YYYY" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Ada","email":"ada@example.com"}'
```

Los identificadores en URLs y payloads son **friendlyId** (C0001, N0002, …), nunca UUID.

### Paginación

Las listas y las búsquedas devuelven una página y un cursor:

```json
{
  "results": [ { "friendlyId": "C0001", "email": "ada@example.com" } ],
  "paging": { "next": { "after": "eyJjcmVhdGVkQXQiOiI…", "link": "https://app.atmos.com.mx/api/v1/contacts?after=eyJ…" } }
}
```

Se pide la siguiente con `?after={cursor}`. **Cuando ya no hay más, `paging.next` no viene** —
esa es la condición de fin, no un `results` vacío. El cursor va firmado: no se construye a mano.

### Cuota y límites

Cada respuesta trae `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset`. Para
consultarlo **sin gastar una llamada de negocio** —y no enterarte con el primer `429`:

```
GET https://app.atmos.com.mx/api/v1/usage
```

Devuelve el nombre del token, su **modo** (`live`/`test`), su **alcance de datos**, los
**objetos** a los que da acceso y `usage` con lo que queda de la cubeta de ráfaga y de la
diaria. `usage` puede venir en `null` si el contador de límites no está disponible: en ese caso
guíate por las cabeceras `X-RateLimit-*` de cualquier respuesta.

### Reintentos e idempotencia

Los lotes de **`create` y `upsert`** aceptan la cabecera **`Idempotency-Key`** (cualquier cadena
única que elijas, p.ej. un UUID por lote):

```
POST https://app.atmos.com.mx/api/v1/contacts/batch/create
Idempotency-Key: 7f1c…
```

Si se corta la red y **reintentas** con la misma clave, Atmos devuelve el resultado de la
primera ejecución en vez de crear todo dos veces. La clave se recuerda **24 horas**.

Alcance exacto, para que no te sorprenda: protege **reintentos secuenciales**, que es el caso
real (falla la red, vuelves a intentar). Dos peticiones **simultáneas** con la misma clave sí
pueden ejecutarse las dos — no hay bloqueo. No la uses para paralelizar el mismo lote.

### Tokens de prueba

Un token puede crearse en **modo prueba** (`test`). Funciona igual, con su **propia cubeta de
límites**, y sus llamadas quedan marcadas como de prueba. Sirve para desarrollar la integración
sin ensuciar las métricas del token real. El modo se elige al crear el token y se consulta en
`/v1/usage`.

### Servidor MCP (Model Context Protocol)

Además de REST, Atmos habla **MCP**: un asistente (Claude Desktop, Cursor, Cosmos) se conecta y
opera el CRM con las mismas reglas del token. No es una API paralela — por dentro corre el mismo
motor, el mismo scope, el mismo alcance de datos y los mismos límites. Una llamada MCP cuenta
como una llamada de API.

- **Endpoint:** `https://app.atmos.com.mx/api/v1/mcp` (Streamable HTTP, JSON-RPC 2.0).
- **Auth:** el mismo `Authorization: Bearer at-api-…`.
- **Métodos:** `initialize`, `ping`, `tools/list`, `tools/call`.

Las herramientas se **derivan del scope del token**: verás `list_`, `search_`, `get_`, `create_`,
`update_` y `delete_` por cada objeto que el token tenga autorizado, y nada más — un token de
solo lectura no expone herramientas de escritura, así que el asistente no puede ni intentarlo.

Configuración típica en un cliente MCP:

```json
{
  "mcpServers": {
    "atmos": {
      "url": "https://app.atmos.com.mx/api/v1/mcp",
      "headers": { "Authorization": "Bearer at-api-XXXX.sk_YYYY" }
    }
  }
}
```

Los errores usan el envelope único con `correlationId` (ver "Códigos de error"), útil para
soporte: identifica la llamada exacta.

### Integración web (pixel de seguimiento + formularios)

Lo que casi siempre se quiere al conectar un sitio con Atmos. Nada de esto usa el token de
API en el navegador.

#### 1. Pixel de seguimiento

Una etiqueta en el `<head>` de todas las páginas del sitio:

```html
<script src="https://app.atmos.com.mx/api/public/track.js"
        data-workspace="XXXXXXXXX"
        async></script>
```

`data-workspace` es el **código de portal** (9 caracteres alfanuméricos); se ve en la URL del
CRM. El script **no lleva token** y es seguro en el frontend: solo escribe, nunca lee datos.

Captura automáticamente: vistas de página (con `referrer` y parámetros `utm_*`), tiempo en
página, clicks y profundidad de scroll (mapas de calor), y un latido cada 30 s mientras la
pestaña está activa. Respeta **Do Not Track** (si el navegador lo pide, no envía nada) y
soporta **SPAs** (escucha `pushState`/`popstate`, no hace falta recargar). Los clicks sobre
`input`, `textarea` y `select` se ignoran a propósito para no capturar datos personales.

API de JavaScript disponible después de cargar:

```js
// Vincula la sesión anónima con un contacto (llamar tras un login o un envío de formulario)
window.Atmos.identify({ email: 'ada@example.com' });

// Evento propio
window.Atmos.track('demo_solicitada', { plan: 'growth' });
```

También se identifica solo si la URL trae `?atmos_ct={token}` (los enlaces de campañas de
Atmos lo añaden), lo que atribuye la visita al contacto correcto.

#### 2. Formularios

Dos caminos, en orden de preferencia:

**a) Incrustar el formulario de Atmos** — una línea, sin backend propio. El formulario se
crea en el CRM (Formularios) y ya trae anti-bot, Turnstile, campos condicionales, deduplicado
de contactos y disparo de automatizaciones:

```html
<div data-form="{slug-del-formulario}" data-workspace="XXXXXXXXX"></div>
<script src="https://app.atmos.com.mx/embed.js" async></script>
```

`data-form` es el **slug** del formulario (el mismo de su URL pública
`app.atmos.com.mx/{portal}/forms/{slug}`). Se pueden incrustar varios en una misma página:
el script monta todos los contenedores que tengan `data-form` + `data-workspace`.

**b) Formulario propio del sitio** — cuando el diseño del formulario debe ser del sitio. El
envío va **a tu servidor**, y tu servidor llama a esta API:

```
POST https://app.atmos.com.mx/api/v1/contacts
Authorization: Bearer at-api-XXXX.sk_YYYY
Content-Type: application/json

{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","phone":"+52...","source":"web"}
```

⚠️ **El token `at-api-…` nunca va en el navegador.** Es un secreto M2M con el scope completo
que le diste: publicado en el HTML o en un bundle de JavaScript, cualquiera puede leer y
escribir en el portal. Debe vivir en una variable de entorno del servidor (route handler de
Next.js, función serverless, backend propio) y el formulario postea a ese endpoint interno.

Después de un envío exitoso, llama a `window.Atmos.identify({ email })` en el cliente para
que la navegación anónima previa quede atribuida al contacto recién creado.

### Campos personalizados

Un campo personalizado se usa **por su nombre interno**, igual que uno de fábrica. Si en
Configuración → Campos creaste `servicio`, en la API es `servicio`. Sin prefijos ni envoltorios.

Consulta los campos de un objeto —de fábrica y personalizados— con:

```
GET /api/v1/properties/contact
```

El `key` de cada campo es **exactamente** el nombre que se usa para escribirlo y leerlo:

```
POST /api/v1/contacts
{"email":"ada@example.com","servicio":"Consultoría","presupuesto":15000}

GET /api/v1/contacts/C0001
→ {"friendlyId":"C0001","email":"ada@example.com","servicio":"Consultoría","presupuesto":15000,…}
```

Funcionan igual en `?fields=` (`?fields=email,servicio`), en la **búsqueda** y en la
**importación**:

```
POST /api/v1/contacts/search
{"filterGroups":[{"filters":[{"field":"servicio","operator":"eq","value":"Consultoría"}]}]}
```

Detalles a tener en cuenta:

- **Tipos.** Un campo numérico devuelve un número; los de fecha, texto en formato `YYYY-MM-DD`.
- **Operadores de búsqueda.** Texto y desplegables: `eq`, `neq`, `in`, `nin`, `contains`,
  `startsWith`. Números y fechas: además `gt`, `gte`, `lt`, `lte`, `between`. Todos aceptan
  `isNull`/`isNotNull`, que también cubren al registro que **no tiene** ese campo lleno.
- **Nombres reservados.** No se puede crear un campo personalizado con el nombre de uno de
  fábrica del mismo objeto (`email`, `phone`, `status`…): un nombre identifica a un solo campo.
- **Acceso.** Un campo restringido por permisos no se devuelve (o llega como `****` si así se
  configuró), y **filtrar por él responde 403** en vez de ignorar el filtro en silencio.

## Convenciones

- Base de la API: `https://app.atmos.com.mx/api/v1`
- OpenAPI 3.1 (JSON): `https://docs.atmos.com.mx/api/docs/openapi.json`
- Servidor MCP (Streamable HTTP, JSON-RPC 2.0): `https://app.atmos.com.mx/api/v1/mcp`
- Identificadores: **friendlyId** (`C0001` contacto, `E` empresa, `N` negocio, `A` actividad, `T` ticket, `P` proyecto, `F` factura). Nunca UUID.
- Paginación por cursor: `?limit=&after=`; la respuesta trae `paging.next.after`.
- Selección de campos: `?fields=firstName,email`.
- Errores: envelope único `{ status, category, message, correlationId, errors[] }`.
- Límites de tasa: cabeceras `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.

## Endpoints

### CRM

#### `GET /api/v1/{object}`

Lista registros de un objeto. Uno de: contacts, companies, deals, activities, tickets, projects, invoices, products, quotes, lists, forms, o el api_name de un objeto personalizado (#84) del portal.

- Parámetros de ruta: `object`
- Query: `limit`, `after`, `fields`
- operationId: `listRecords`

#### `POST /api/v1/{object}`

Crea un registro. Los CAMPOS válidos dependen del portal: consúltalos en GET /v1/properties/{objectType} — el `key` de cada uno es el nombre que va en el body (los personalizados, por su nombre interno, sin prefijo). Objetos escribibles: contacts, companies, deals, activities, tickets, projects, invoices.

- Parámetros de ruta: `object`
- operationId: `createRecord`

#### `GET /api/v1/{object}/{id}`

Obtiene un registro por friendlyId.

- Parámetros de ruta: `object`, `id`
- Query: `fields`
- operationId: `getRecord`

#### `PATCH /api/v1/{object}/{id}`

Actualiza un registro por friendlyId (parcial: solo los campos enviados). Los campos válidos son los mismos de GET /v1/properties/{objectType}.

- Parámetros de ruta: `object`, `id`
- operationId: `updateRecord`

#### `DELETE /api/v1/{object}/{id}`

Envía un registro a la papelera (soft delete).

- Parámetros de ruta: `object`, `id`
- operationId: `deleteRecord`

#### `POST /api/v1/{object}/search`

Búsqueda con filterGroups (OR entre grupos, AND dentro).

- Parámetros de ruta: `object`
- Body: `filterGroups`, `sorts`, `fields`, `limit`, `after`
- operationId: `searchRecords`

#### `POST /api/v1/{object}/batch/{op}`

Operación en lote (op: create|read|update|archive|upsert). 207 Multi-Status en éxito parcial.

- Parámetros de ruta: `object`, `op`
- Body: `inputs`, `idProperty`
- operationId: `batchRecords`

#### `POST /api/v1/{object}/{id}/permanent-delete`

Borrado permanente / derecho al olvido (irreversible). Requiere { confirm: true }.

- Parámetros de ruta: `object`, `id`
- Body: `confirm`
- operationId: `permanentDeleteRecord`

#### `PUT /api/v1/{object}/{id}/associations/{toType}/{toId}`

Vincula dos registros por su pivote nativo.

- Parámetros de ruta: `object`, `id`, `toType`, `toId`
- operationId: `associate`

#### `DELETE /api/v1/{object}/{id}/associations/{toType}/{toId}`

Elimina el vínculo entre dos registros.

- Parámetros de ruta: `object`, `id`, `toType`, `toId`
- operationId: `dissociate`

### Soporte

#### `GET /api/v1/users`

Lista los miembros del portal.

- operationId: `listUsers`

#### `GET /api/v1/teams`

Lista los equipos del portal.

- operationId: `listTeams`

#### `GET /api/v1/owners`

Lista los usuarios asignables (owners).

- operationId: `listOwners`

#### `GET /api/v1/pipelines`

Lista los pipelines de negocios.

- operationId: `listPipelines`

#### `GET /api/v1/pipelines/{id}/stages`

Lista las etapas de un pipeline.

- Parámetros de ruta: `id`
- operationId: `listStages`

### Properties

#### `GET /api/v1/properties/{objectType}`

Lista las definiciones de campo (default + custom) de un objeto.

- Parámetros de ruta: `objectType`
- operationId: `listProperties`

#### `POST /api/v1/properties/{objectType}`

Crea un campo personalizado.

- Parámetros de ruta: `objectType`
- operationId: `createProperty`

#### `PATCH /api/v1/properties/{objectType}/{fieldKey}`

Edita un campo personalizado.

- Parámetros de ruta: `objectType`, `fieldKey`
- operationId: `updateProperty`

#### `DELETE /api/v1/properties/{objectType}/{fieldKey}`

Elimina un campo personalizado.

- Parámetros de ruta: `objectType`, `fieldKey`
- operationId: `deleteProperty`

### Webhooks

#### `GET /api/v1/webhooks`

Lista las suscripciones de webhooks.

- operationId: `listWebhooks`

#### `POST /api/v1/webhooks`

Crea una suscripción de webhook. Eventos válidos: contact.created, contact.updated, contact.deleted, company.created, company.updated, company.deleted, deal.created, deal.updated, deal.deleted, activity.created, activity.updated, activity.deleted, ticket.created, ticket.updated, ticket.deleted, project.created, project.updated, project.deleted, invoice.created, invoice.updated, invoice.deleted, product.created, product.updated, product.deleted, quote.created, quote.updated, quote.deleted, list.created, list.updated, list.deleted, form.created, form.updated, form.deleted.

- Body: `url`, `events`, `active`
- operationId: `createWebhook`

#### `GET /api/v1/webhooks/{id}`

Obtiene una suscripción.

- Parámetros de ruta: `id`
- operationId: `getWebhook`

#### `PATCH /api/v1/webhooks/{id}`

Edita una suscripción.

- Parámetros de ruta: `id`
- Body: `url`, `events`, `active`
- operationId: `updateWebhook`

#### `DELETE /api/v1/webhooks/{id}`

Elimina una suscripción.

- Parámetros de ruta: `id`
- operationId: `deleteWebhook`

#### `GET /api/v1/webhooks/{id}/deliveries`

Log de entregas de una suscripción.

- Parámetros de ruta: `id`
- operationId: `listDeliveries`

#### `POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay`

Reintenta una entrega.

- Parámetros de ruta: `id`, `deliveryId`
- operationId: `replayDelivery`

### Import

#### `POST /api/v1/imports`

Inicia una importación (JSON rows). Objetos: contacts, companies, deals, activities, tickets, projects, invoices.

- Body: `objectType`, `rows`, `csv`, `mode`, `dateFormat`
- operationId: `startImport`

#### `GET /api/v1/imports`

Lista las importaciones.

- operationId: `listImports`

#### `GET /api/v1/imports/{id}`

Estado/progreso de una importación.

- Parámetros de ruta: `id`
- operationId: `getImport`

#### `DELETE /api/v1/imports/{id}`

Cancela una importación en curso.

- Parámetros de ruta: `id`
- operationId: `cancelImport`

### Token

#### `GET /api/v1/usage`

Consumo y cuota RESTANTE del token que hace la llamada: ráfaga y diaria, modo (live/test) y objetos a los que da acceso. Sirve para anticipar un 429 en vez de descubrirlo.

- operationId: `getUsage`

#### `POST /api/v1/mcp`

Servidor MCP (Model Context Protocol) sobre Streamable HTTP, JSON-RPC 2.0: `initialize`, `ping`, `tools/list`, `tools/call`. Mismo token, mismo scope y mismos límites que REST. Ver la sección "Servidor MCP" de esta doc.

- operationId: `mcpEndpoint`

## Códigos de error

- `400 VALIDATION_ERROR` — datos inválidos (detalle por campo en `errors[]`).
- `401 UNAUTHORIZED` — token ausente, inválido o revocado.
- `403 FORBIDDEN` — el token no tiene esa acción, ese objeto o ese campo en su scope.
- `404 NOT_FOUND` — el friendlyId no existe en el portal del token.
- `409 CONFLICT` — conflicto de estado (p.ej. duplicado en upsert).
- `429 RATE_LIMITED` — límite de tasa excedido; reintentar según `X-RateLimit-Reset`.
