# Referencia de endpoints

> Generado automáticamente desde `openapi/openapi.yaml`. No se edita a mano: `pnpm docs:gen`.

## Índice

- **Messages** — Envío y consulta de mensajes. (6)
- **Conversations** — Bandeja de entrada del chat. (8)
- **Contacts** — Contactos finales y consentimiento. (7)
- **Templates** — Plantillas de mensaje y biblioteca compartida. (10)
- **Numbers** — Cuenta de WhatsApp, números y alta con Embedded Signup. (9)
- **Media** — Archivos adjuntos. (2)
- **Agents** — Agentes del chat. (3)
- **Keys** — Claves de API por tenant. (3)
- **Usage** — Costes, uso y licencia del tenant. (3)
- **Settings** — Ajustes del tenant. (2)
- **Webhooks** — Destinos de eventos salientes hacia vuestras apps. (7)
- **Embed** — Tokens y sesiones de la UI embebible. (2)
- **Admin** — API de administración (solo vuestro equipo). (26)
- **Meta** — Endpoint que recibe los webhooks de Meta (servicio `ingest`). (2)
- **Health** — Comprobaciones de salud. (2)

## Messages

Envío y consulta de mensajes.

### `GET /v1/messages`

**Listar mensajes** · `listMessages`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `messages:read` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |
| `external_ref` | query |  | string |  |
| `status` | query |  | [MessageStatus](#messagestatus) |  |
| `direction` | query |  | [MessageDirection](#messagedirection) |  |
| `conversation_id` | query |  | string (`cnv_…`) |  |
| `from` | query |  | string (date-time) |  |
| `to` | query |  | string (date-time) |  |

**Respuestas**: `200` → `MessagePage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/messages`

**Enviar un mensaje** · `sendMessage`

Valida, encola y responde `202`. Validaciones, en orden: formato del número, licencia y límite mensual, número de origen conectado y WABA sin bloqueo de pago, plantilla aprobada y variables correctas, ventana de 24 h abierta para tipos que no son plantilla, y consentimiento del contacto.

- Autenticación: `ApiKey` · alcance `messages:send` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`SendMessageRequest`):

Una de estas variantes: `SendText`, `SendTemplate`, `SendMedia`, `SendInteractive`.

Ejemplo (Plantilla con variables por posición):

```json
{
  "to": "+34 600 111 222",
  "type": "template",
  "template": {
    "name": "recordatorio_cita",
    "language": "es",
    "variables": [
      "Ana",
      "15 de octubre a las 10:30"
    ]
  },
  "contact": {
    "external_ref": "cli-9917",
    "display_name": "Ana Pérez"
  },
  "external_ref": "cita-48213"
}
```

Ejemplo (Texto libre (ventana abierta)):

```json
{
  "to": "+34600111222",
  "type": "text",
  "text": {
    "body": "Hola Ana, tu cita queda confirmada."
  }
}
```

Ejemplo (Botones de respuesta):

```json
{
  "to": "+34600111222",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": "¿Confirmas la cita del 15?",
    "buttons": [
      {
        "id": "yes",
        "title": "Sí"
      },
      {
        "id": "no",
        "title": "No"
      }
    ]
  }
}
```

**Respuestas**: `202` → `Message` · `400` → `Error` · `401` → `Error` · `402` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/messages/{message_id}`

**Consultar un mensaje** · `getMessage`

Estado, error y precio (`billable`, `pricing_category`) de un mensaje.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `messages:read` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `message_id` | path | sí | string (`msg_…`) |  |

**Respuestas**: `200` → `Message` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `POST /v1/messages/{message_id}/redact`

**Redactar un mensaje recibido** · `redactMessage`

Borra el contenido de un mensaje recibido (texto, respuesta de botón o de formulario) para que un dato sensible, como un DNI, no se quede guardado. Sustituye el cuerpo por `[dato redactado]`, vacía el `payload` y borra también las copias guardadas del evento y del aviso original de Meta. No recupera lo que ya se entregó a un receptor de eventos, ni lo que Meta o el móvil del cliente conserven. Repetirlo no cambia nada.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `message_id` | path | sí | string (`msg_…`) |  |

**Respuestas**: `200` → `Message` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `429` → `Error`

### `POST /v1/messages/{message_id}/typing`

**Mostrar «escribiendo…» al cliente** · `sendTypingIndicator`

Marca como leído un mensaje recibido y muestra al cliente el indicador de escritura, que dura hasta 25 segundos o hasta que se envía la respuesta. Úsalo cuando la respuesta vaya a tardar más de un segundo. Repetirlo lo renueva.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `messages:send` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `message_id` | path | sí | string (`msg_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `429` → `Error` · `502` → `Error`

### `POST /v1/messages/{message_id}/retry`

**Reintentar un envío fallido** · `retryMessage`

Solo si el mensaje está en `failed` y el error es recuperable (`error.retryable = true`) o es `outcome_unknown`. Un reintento de `outcome_unknown` puede duplicar el mensaje en el móvil.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `messages:send` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `message_id` | path | sí | string (`msg_…`) |  |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Respuestas**: `202` → `Message` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `429` → `Error`

## Conversations

Bandeja de entrada del chat.

### `GET /v1/conversations`

**Listar conversaciones** · `listConversations`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:read` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |
| `status` | query |  | [ConversationStatus](#conversationstatus) |  |
| `assigned_agent_id` | query |  | string | Use `none` para las sin asignar. |
| `unread` | query |  | boolean |  |
| `q` | query |  | string | Busca por nombre o teléfono. |
| `external_ref` | query |  | string |  |
| `updated_since` | query |  | string (date-time) |  |

**Respuestas**: `200` → `ConversationPage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/conversations/{conversation_id}`

**Detalle de una conversación** · `getConversation`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:read` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |

**Respuestas**: `200` → `Conversation` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `PATCH /v1/conversations/{conversation_id}`

**Cambiar estado, asignación o etiquetas** · `updateConversation`

Asignar es atómico: si la conversación ya está asignada a otro agente responde `409 already_assigned`. Un `supervisor` o `admin` puede reasignar.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:write` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |

**Cuerpo** (`ConversationUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `status` | [ConversationStatus](#conversationstatus) |  |  |
| `assigned_agent_id` | string (`agt_…`) \| null |  |  |
| `tags` | string[] |  |  |

Ejemplo (assign):

```json
{
  "assigned_agent_id": "agt_018f3c5a9b2d7e4f8a1b2c3d4e5f6a7b",
  "status": "open"
}
```

Ejemplo (close):

```json
{
  "status": "closed"
}
```

**Respuestas**: `200` → `Conversation` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/conversations/{conversation_id}/messages`

**Mensajes del hilo** · `listConversationMessages`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:read` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |

**Respuestas**: `200` → `MessagePage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/conversations/{conversation_id}/messages`

**Responder como agente** · `replyToConversation`

Mismo cuerpo que `POST /v1/messages`, sin `to` ni `phone_number_id` (salen de la conversación).

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:write` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`ReplyRequest`):

Una de estas variantes: `ReplyText`, `ReplyTemplate`, `ReplyMedia`, `ReplyInteractive`.

**Respuestas**: `202` → `Message` · `400` → `Error` · `401` → `Error` · `402` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/conversations/{conversation_id}/read`

**Marcar como leída** · `markConversationRead`

Pone `unread_count` a 0 y marca como leído en Meta el último mensaje entrante.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:write` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `GET /v1/conversations/{conversation_id}/notes`

**Notas internas** · `listConversationNotes`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:write` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |

**Respuestas**: `200` → `NotePage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/conversations/{conversation_id}/notes`

**Añadir una nota interna** · `createConversationNote`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `conversations:write` · rol mínimo `agent` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `conversation_id` | path | sí | string (`cnv_…`) |  |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`NoteCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `body` | string | sí |  |

**Respuestas**: `201` → `Note` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

## Contacts

Contactos finales y consentimiento.

### `GET /v1/contacts`

**Listar contactos** · `listContacts`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |
| `external_ref` | query |  | string |  |
| `q` | query |  | string | Busca por nombre o teléfono. |
| `opt_in_status` | query |  | [OptInStatus](#optinstatus) |  |

**Respuestas**: `200` → `ContactPage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/contacts`

**Crear un contacto** · `createContact`

Si ya existe un contacto con ese `wa_id`, lo devuelve con `200` en lugar de crearlo.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`ContactCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `phone` | string | sí | Teléfono con o sin `+` y con espacios; se normaliza a dígitos con prefijo de país. |
| `display_name` | string |  |  |
| `external_ref` | string |  |  |
| `locale` | string |  |  |
| `attributes` | object |  |  |

**Respuestas**: `200` → `Contact` · `201` → `Contact` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/contacts/{contact_id}`

**Detalle de un contacto** · `getContact`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `contact_id` | path | sí | string (`ctc_…`) |  |

**Respuestas**: `200` → `Contact` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `PATCH /v1/contacts/{contact_id}`

**Editar un contacto** · `updateContact`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `contact_id` | path | sí | string (`ctc_…`) |  |

**Cuerpo** (`ContactUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `display_name` | string |  |  |
| `external_ref` | string |  |  |
| `locale` | string |  |  |
| `attributes` | object |  |  |
| `blocked` | boolean |  |  |

**Respuestas**: `200` → `Contact` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

### `DELETE /v1/contacts/{contact_id}`

**Borrado RGPD de un contacto** · `deleteContact`

Elimina el contacto, sus conversaciones, mensajes y medios, y anonimiza su rastro en `audit_log`. No se puede deshacer.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `admin` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `contact_id` | path | sí | string (`ctc_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `POST /v1/contacts/{contact_id}/opt-in`

**Registrar consentimiento** · `optInContact`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `contact_id` | path | sí | string (`ctc_…`) |  |

**Cuerpo** (`OptInRequest`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `source` | string | sí | Origen del consentimiento (`formulario_web`, `alta_en_tienda`...). |
| `at` | string (date-time) |  |  |

Ejemplo (web):

```json
{
  "source": "formulario_web",
  "at": "2026-10-12T09:30:00Z"
}
```

**Respuestas**: `200` → `Contact` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/contacts/{contact_id}/opt-out`

**Registrar la baja de un contacto** · `optOutContact`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `contacts:write` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `contact_id` | path | sí | string (`ctc_…`) |  |

**Cuerpo** (`OptOutRequest`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `reason` | string |  |  |

**Respuestas**: `200` → `Contact` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

## Templates

Plantillas de mensaje y biblioteca compartida.

### `GET /v1/templates`

**Listar plantillas** · `listTemplates`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:read` · rol mínimo `agent` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |
| `status` | query |  | [TemplateStatus](#templatestatus) |  |
| `category` | query |  | [TemplateCategory](#templatecategory) |  |
| `language` | query |  | string |  |
| `name` | query |  | string |  |

**Respuestas**: `200` → `TemplatePage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/templates`

**Crear una plantilla** · `createTemplate`

Se guarda como `draft`. Con `submit: true` se envía a revisión de Meta y queda en `pending`. El gateway convierte este formato al de componentes de Meta. Comprueba `license.limits.max_templates` y los topes de Meta (250 plantillas por WABA sin negocio verificado, 6.000 con él, 100 creaciones por hora).

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:write` · rol mínimo `admin` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`TemplateCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `number_id` | string (`num_…`) |  |  |
| `name` | string | sí |  |
| `language` | string | sí |  |
| `category` | [TemplateCategory](#templatecategory) | sí |  |
| `parameter_format` | `positional` \| `named` |  |  |
| `header` | [TemplateHeader](#templateheader) |  |  |
| `body` | [TemplateBodyInput](#templatebodyinput) | sí |  |
| `footer` | string |  |  |
| `buttons` | [TemplateButton](#templatebutton)[] |  |  |
| `submit` | boolean |  | Enviar a revisión de Meta nada más crearla. |

Ejemplo (recordatorio):

```json
{
  "name": "recordatorio_cita",
  "language": "es",
  "category": "utility",
  "header": {
    "type": "text",
    "text": "Recordatorio de cita"
  },
  "body": {
    "text": "Hola {{1}}, te esperamos el {{2}}.",
    "examples": [
      "Ana",
      "15 de octubre a las 10:30"
    ]
  },
  "footer": "Responde a este mensaje si necesitas cambiarla",
  "buttons": [
    {
      "type": "quick_reply",
      "text": "Confirmar"
    },
    {
      "type": "quick_reply",
      "text": "Cambiar"
    }
  ],
  "submit": true
}
```

**Respuestas**: `201` → `Template` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error` · `502` → `Error` · `503` → `Error`

### `POST /v1/templates/sync`

**Resincronizar plantillas con Meta** · `syncTemplates`

Importa las creadas en WhatsApp Manager, corrige estados y marca como borradas las que ya no existen.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:write` · rol mínimo `admin` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `202` → `JobAccepted` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `429` → `Error`

### `GET /v1/templates/{template_id}`

**Detalle de una plantilla** · `getTemplate`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:read` · rol mínimo `agent` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `template_id` | path | sí | string (`tpl_…`) |  |

**Respuestas**: `200` → `Template` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `PATCH /v1/templates/{template_id}`

**Editar una plantilla** · `updateTemplate`

Crea una revisión nueva (`revision + 1`) y, si estaba enviada, la devuelve a `pending`. Meta limita las ediciones por periodo.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:write` · rol mínimo `admin` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `template_id` | path | sí | string (`tpl_…`) |  |

**Cuerpo** (`TemplateUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `category` | [TemplateCategory](#templatecategory) |  |  |
| `header` | [TemplateHeader](#templateheader) |  |  |
| `body` | [TemplateBodyInput](#templatebodyinput) |  |  |
| `footer` | string |  |  |
| `buttons` | [TemplateButton](#templatebutton)[] |  |  |
| `submit` | boolean |  |  |

**Respuestas**: `200` → `Template` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error` · `502` → `Error` · `503` → `Error`

### `DELETE /v1/templates/{template_id}`

**Borrar una plantilla** · `deleteTemplate`

Borra en Meta solo la plantilla de ese idioma (se envía `hsm_id`) y la marca `deleted`.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:write` · rol mínimo `admin` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `template_id` | path | sí | string (`tpl_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error` · `502` → `Error` · `503` → `Error`

### `POST /v1/templates/{template_id}/submit`

**Enviar un borrador a revisión** · `submitTemplate`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:write` · rol mínimo `admin` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `template_id` | path | sí | string (`tpl_…`) |  |

**Respuestas**: `200` → `Template` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `429` → `Error` · `502` → `Error` · `503` → `Error`

### `POST /v1/templates/{template_id}/preview`

**Vista previa con variables** · `previewTemplate`

Devuelve el texto ya renderizado sin enviar nada.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:read` · rol mínimo `agent` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `template_id` | path | sí | string (`tpl_…`) |  |

**Cuerpo** (`TemplatePreviewRequest`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `variables` | [TemplateVariables](#templatevariables) | sí | Valores por posición (plantillas `positional`) o por nombre (plantillas `named`). |

**Respuestas**: `200` → `TemplatePreview` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/template-library`

**Plantillas base disponibles** · `listTemplateLibrary`

Biblioteca global mantenida por vosotros. No depende del tenant.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:read` · rol mínimo `agent` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `language` | query |  | string |  |
| `category` | query |  | [TemplateCategory](#templatecategory) |  |

**Respuestas**: `200`  · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/template-library/{code}/activate`

**Activar una plantilla de la biblioteca** · `activateLibraryTemplate`

Crea la plantilla en la WABA del tenant con el contenido de la biblioteca y la envía a revisión.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `templates:write` · rol mínimo `admin` · función de licencia `templates`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `code` | path | sí | string |  |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Respuestas**: `201` → `Template` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `429` → `Error` · `502` → `Error` · `503` → `Error`

## Numbers

Cuenta de WhatsApp, números y alta con Embedded Signup.

### `GET /v1/whatsapp/account`

**Cuenta de WhatsApp del tenant** · `getWhatsAppAccount`

WABA, verificación del negocio, modo de facturación y estado de pago.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:read` · rol mínimo `agent`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `200` → `WhatsAppAccount` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `GET /v1/numbers`

**Listar números** · `listNumbers`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:read` · rol mínimo `agent`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `200`  · `400` → `Error` · `401` → `Error` · `403` → `Error` · `429` → `Error`

### `GET /v1/numbers/{number_id}`

**Detalle de un número** · `getNumber`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:read` · rol mínimo `agent`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `number_id` | path | sí | string (`num_…`) |  |

**Respuestas**: `200` → `Number` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `PATCH /v1/numbers/{number_id}`

**Marcar el número predeterminado** · `updateNumber`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `number_id` | path | sí | string (`num_…`) |  |

**Cuerpo** (`NumberUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `is_default` | `true` | sí |  |

**Respuestas**: `200` → `Number` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

### `DELETE /v1/numbers/{number_id}`

**Desconectar un número** · `disconnectNumber`

Deja de enviar y recibir por ese número. No lo elimina de la cuenta de Meta del cliente.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `number_id` | path | sí | string (`num_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `POST /v1/numbers/{number_id}/refresh`

**Releer el estado del número en Meta** · `refreshNumber`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `number_id` | path | sí | string (`num_…`) |  |

**Respuestas**: `200` → `Number` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error` · `502` → `Error` · `503` → `Error`

### `POST /v1/whatsapp/signup-sessions`

**Iniciar el alta de un número (Embedded Signup v4)** · `createSignupSession`

Devuelve `state`, `config_id` y `app_id` para abrir el Embedded Signup en una ventana propia del gateway. `state` es de un solo uso y caduca a los 30 minutos.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Cuerpo** (`SignupSessionCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `billing_mode` | [BillingMode](#billingmode) |  |  |
| `onboarding_type` | `dedicated` \| `business_app` |  | `business_app` conecta un número que ya usa la app WhatsApp Business sin quitárselo (coexistencia). |
| `return_url` | string (uri) |  |  |

**Respuestas**: `201` → `SignupSessionCreated` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/whatsapp/signup-sessions/{session_id}`

**Progreso del alta** · `getSignupSession`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:read` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `session_id` | path | sí | string (`sus_…`) |  |

**Respuestas**: `200` → `SignupSession` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `POST /v1/whatsapp/signup-sessions/{session_id}/complete`

**Cerrar el alta con el code de Meta** · `completeSignupSession`

Idempotente por `state`: repetirla devuelve la sesión existente. El `code` vale 30 segundos; el gateway lo canjea, suscribe webhooks, registra el número y lee su estado en un worker.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `numbers:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `session_id` | path | sí | string (`sus_…`) |  |

**Cuerpo** (`SignupSessionComplete`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `state` | string | sí |  |
| `code` | string | sí | Código canjeable de Meta; vale 30 segundos. |
| `waba_id` | string | sí |  |
| `phone_number_id` | string |  | Obligatorio en un alta `dedicated`. En coexistencia Meta puede no enviarlo: si se omite y la cuenta tiene un solo número, se usa ese. |
| `business_id` | string |  |  |

**Respuestas**: `202` → `SignupSession` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

## Media

Archivos adjuntos.

### `POST /v1/media`

**Subir un archivo** · `uploadMedia`

Multipart. Valida tamaño y tipo, analiza el archivo y devuelve un `med_...` para usarlo en un mensaje.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `messages:send` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Cuerpo** — `multipart/form-data`:

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `file` | string | sí |  |
| `file_name` | string |  |  |

**Respuestas**: `201` → `Media` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/media/{media_id}`

**Descargar un archivo** · `getMedia`

Redirige a una URL firmada de 5 minutos. Los tipos que no son imagen, audio o vídeo se sirven como adjunto.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `messages:read` · rol mínimo `agent` · función de licencia `api`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `media_id` | path | sí | string (`med_…`) |  |

**Respuestas**: `302` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

## Agents

Agentes del chat.

### `GET /v1/agents`

**Listar agentes** · `listAgents`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `agents:write` · rol mínimo `supervisor` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |

**Respuestas**: `200` → `AgentPage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/agents`

**Crear un agente** · `createAgent`

Comprueba `license.limits.max_agents`. Los agentes también se crean solos la primera vez que entran por la UI embebida.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `agents:write` · rol mínimo `admin` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`AgentCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `external_user_id` | string | sí |  |
| `display_name` | string | sí |  |
| `email` | string (email) |  |  |
| `role` | [AgentRole](#agentrole) |  |  |

**Respuestas**: `201` → `Agent` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `409` → `Error` · `422` → `Error` · `429` → `Error`

### `PATCH /v1/agents/{agent_id}`

**Editar o desactivar un agente** · `updateAgent`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `agents:write` · rol mínimo `admin` · función de licencia `chat`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `agent_id` | path | sí | string (`agt_…`) |  |

**Cuerpo** (`AgentUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `display_name` | string |  |  |
| `email` | string (email) |  |  |
| `role` | [AgentRole](#agentrole) |  |  |
| `active` | boolean |  |  |

**Respuestas**: `200` → `Agent` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

## Keys

Claves de API por tenant.

### `GET /v1/api-keys`

**Listar claves de API del tenant** · `listApiKeys`

Nunca devuelve el valor completo, solo el prefijo.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `keys:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `200`  · `400` → `Error` · `401` → `Error` · `403` → `Error` · `429` → `Error`

### `POST /v1/api-keys`

**Crear una clave de API para el tenant** · `createApiKey`

El valor completo se devuelve una sola vez. Los alcances no pueden superar los de la clave que la crea.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `keys:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`ApiKeyCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `name` | string |  |  |
| `scopes` | [Scope](#scope)[] | sí |  |
| `rate_limit_per_min` | integer |  |  |
| `expires_at` | string (date-time) |  |  |

**Respuestas**: `201` → `ApiKeyCreated` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `DELETE /v1/api-keys/{key_id}`

**Revocar una clave** · `revokeApiKey`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `keys:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `key_id` | path | sí | string (`key_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

## Usage

Costes, uso y licencia del tenant.

### `GET /v1/usage/costs`

**Costes de Meta** · `getUsageCosts`

Datos de `pricing_analytics` de Meta guardados en `usage_cost_daily`. Con facturación `meta_direct` es informativo: la factura la emite Meta al cliente. Con línea de crédito de un Solution Partner Meta no devuelve coste.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `usage:read` · rol mínimo `admin` · función de licencia `costs`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `from` | query | sí | string (date) |  |
| `to` | query | sí | string (date) |  |
| `group_by` | query |  | `day` \| `category` \| `pricing_type` \| `country` \| `number`[] | Dimensiones separadas por comas. |

**Respuestas**: `200` → `UsageCosts` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/usage/messages`

**Mensajes propios por día y estado** · `getUsageMessages`

Datos del propio gateway, sin retraso respecto de Meta.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `usage:read` · rol mínimo `supervisor` · función de licencia `costs`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `from` | query | sí | string (date) |  |
| `to` | query | sí | string (date) |  |
| `group_by` | query |  | `day` \| `status` \| `direction` \| `type`[] |  |

**Respuestas**: `200` → `UsageMessages` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `GET /v1/license`

**Licencia, límites y consumo del tenant** · `getLicense`

Permite a la app anfitriona ocultar entradas de menú antes de cargar una pantalla no contratada.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `usage:read` · rol mínimo `agent`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `200` → `License` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

## Settings

Ajustes del tenant.

### `GET /v1/settings`

**Ajustes del tenant** · `getSettings`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `settings:write` · rol mínimo `agent`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `200` → `Settings` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `429` → `Error`

### `PATCH /v1/settings`

**Cambiar ajustes** · `updateSettings`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `settings:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Cuerpo** (`SettingsUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `auto_assign_mode` | `manual` \| `round_robin` \| `least_busy` |  |  |
| `reopen_on_inbound` | `unassigned` \| `previous_agent` |  |  |
| `optout_keywords` | string[] |  |  |
| `enforce_opt_in` | boolean |  |  |
| `cost_alert_monthly` | number \| null |  |  |
| `notify_email` | string \| null |  |  |

**Respuestas**: `200` → `Settings` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

## Webhooks

Destinos de eventos salientes hacia vuestras apps.

### `GET /v1/webhook-subscriptions`

**Listar destinos de eventos** · `listWebhookSubscriptions`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Respuestas**: `200`  · `400` → `Error` · `401` → `Error` · `403` → `Error` · `429` → `Error`

### `POST /v1/webhook-subscriptions`

**Registrar un destino de eventos** · `createWebhookSubscription`

Solo `https`, sin redirecciones y sin direcciones privadas. La respuesta incluye `secret` una sola vez. La suscripción es siempre del tenant de la llamada. Para recibir los eventos de todos los tenants de una app se usa `POST /admin/v1/apps/{app_id}/webhook-subscriptions`.

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`WebhookSubscriptionCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `url` | string (uri) | sí | Solo `https`. |
| `events` | string[] | sí |  |

Ejemplo (erp):

```json
{
  "url": "https://erp.cliente.com/hooks/whatsapp",
  "events": [
    "whatsapp.message.*",
    "whatsapp.template.status_changed"
  ]
}
```

**Respuestas**: `201` → `WebhookSubscriptionCreated` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `PATCH /v1/webhook-subscriptions/{subscription_id}`

**Editar o reactivar una suscripción** · `updateWebhookSubscription`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `subscription_id` | path | sí | string (`whs_…`) |  |

**Cuerpo** (`WebhookSubscriptionUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `url` | string (uri) |  |  |
| `events` | string[] |  |  |
| `active` | boolean |  |  |

**Respuestas**: `200` → `WebhookSubscription` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error` · `429` → `Error`

### `DELETE /v1/webhook-subscriptions/{subscription_id}`

**Borrar una suscripción** · `deleteWebhookSubscription`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `subscription_id` | path | sí | string (`whs_…`) |  |

**Respuestas**: `204` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `POST /v1/webhook-subscriptions/{subscription_id}/test`

**Enviar un evento de prueba** · `testWebhookSubscription`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `subscription_id` | path | sí | string (`whs_…`) |  |

**Respuestas**: `200` → `WebhookDelivery` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

### `GET /v1/webhook-deliveries`

**Entregas y reintentos** · `listWebhookDeliveries`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |
| `status` | query |  | [DeliveryStatus](#deliverystatus) |  |
| `subscription_id` | query |  | string (`whs_…`) |  |

**Respuestas**: `200` → `WebhookDeliveryPage` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/webhook-deliveries/{delivery_id}/redeliver`

**Reenviar una entrega** · `redeliverWebhook`

- Autenticación: `ApiKey` o `EmbedSession` · alcance `webhooks:write` · rol mínimo `admin`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `delivery_id` | path | sí | string (`dlv_…`) |  |

**Respuestas**: `202` → `WebhookDelivery` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `429` → `Error`

## Embed

Tokens y sesiones de la UI embebible.

### `POST /v1/embed/tokens`

**Emitir un token para una pantalla embebida** · `createEmbedToken`

Lo pide el **backend** de la app anfitriona; nunca el navegador. El `role` que envía la app es el que manda. El token dura como máximo 5 minutos, es de un solo uso y va en el fragmento de la URL.

- Autenticación: `ApiKey` · alcance `embed:create`
- Con clave multi-tenant: cabecera `X-Tenant-Id` o `X-Tenant-Ref` obligatoria.

**Cuerpo** (`EmbedTokenRequest`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `screen` | [EmbedScreen](#embedscreen) | sí |  |
| `user` | [EmbedUser](#embeduser) | sí |  |
| `params` | object |  | Parámetros de la pantalla (`conversation_id`, `contact_ref`, `template_id`, `tab`, `from`, `to`). |
| `locale` | `es` \| `en` |  |  |
| `ttl_seconds` | integer |  |  |

Ejemplo (chat):

```json
{
  "screen": "chat",
  "user": {
    "external_user_id": "u-318",
    "name": "Marta López",
    "email": "marta@cliente.com",
    "role": "agent"
  },
  "params": {
    "contact_ref": "cli-9917"
  },
  "locale": "es",
  "ttl_seconds": 300
}
```

**Respuestas**: `201` → `EmbedToken` · `400` → `Error` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

### `POST /v1/embed/session`

**Canjear un token de embebido por un access token** · `createEmbedSession`

El iframe lo llama al cargar. Gasta el token de embebido y devuelve un `access_token` de 15 minutos que vive solo en memoria.

- Autenticación: ninguna (pública)

**Cuerpo** (`EmbedSessionRequest`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `token` | string | sí |  |

**Respuestas**: `200` → `EmbedSession` · `401` → `Error` · `403` → `Error` · `422` → `Error` · `429` → `Error`

## Admin

API de administración (solo vuestro equipo).

### `GET /admin/v1/tenants`

**Listar tenants** · `adminListTenants`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string | Valor `next_cursor` de la página anterior. |
| `status` | query |  | [TenantStatus](#tenantstatus) |  |
| `q` | query |  | string | Busca por nombre o `external_ref`. |

**Respuestas**: `200` → `TenantPage` · `401` → `Error` · `403` → `Error` · `422` → `Error`

### `POST /admin/v1/tenants`

**Crear un tenant** · `adminCreateTenant`

Se crea sin licencia. Hasta fijarla con `PUT .../license`, todas las llamadas del tenant devuelven `403 license_inactive`.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header |  | string | Misma clave y mismo cuerpo devuelve la respuesta original durante 24 h; mismo cuerpo distinto devuelve `409 idempotency_conflict`. |

**Cuerpo** (`TenantCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `external_ref` | string | sí |  |
| `name` | string | sí |  |
| `default_locale` | string |  |  |
| `timezone` | string |  |  |
| `data_retention_days` | integer |  |  |

Ejemplo (cliente):

```json
{
  "external_ref": "CLI-0042",
  "name": "Clínica Dental Sol",
  "default_locale": "es",
  "timezone": "Europe/Madrid"
}
```

**Respuestas**: `201` → `Tenant` · `401` → `Error` · `403` → `Error` · `409` → `Error` · `422` → `Error`

### `GET /admin/v1/tenants/{tenant_id}`

**Detalle de un tenant** · `adminGetTenant`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Respuestas**: `200` → `TenantDetail` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `PATCH /admin/v1/tenants/{tenant_id}`

**Editar un tenant** · `adminUpdateTenant`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Cuerpo** (`TenantUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `name` | string |  |  |
| `status` | [TenantStatus](#tenantstatus) |  |  |
| `default_locale` | string |  |  |
| `timezone` | string |  |  |
| `data_retention_days` | integer |  |  |

**Respuestas**: `200` → `Tenant` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error`

### `PUT /admin/v1/tenants/{tenant_id}/license`

**Fijar la licencia del módulo WhatsApp** · `adminSetLicense`

Reemplaza la licencia. Invalida la caché al instante y emite `license.updated`. Se registra en `audit_log` con el usuario de vuestro equipo.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Cuerpo** (`LicenseSet`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `status` | [LicenseStatus](#licensestatus) | sí |  |
| `valid_from` | string (date-time) |  |  |
| `valid_until` | string (date-time) \| null |  |  |
| `limits` | [LicenseLimits](#licenselimits) |  |  |
| `notes` | string |  |  |

Ejemplo (anual):

```json
{
  "status": "active",
  "valid_until": "2027-10-12T00:00:00Z",
  "limits": {
    "max_numbers": 1,
    "max_agents": 5,
    "max_templates": 50,
    "max_outbound_per_month": 20000,
    "features": [
      "chat",
      "templates",
      "api",
      "embed",
      "costs"
    ]
  }
}
```

**Respuestas**: `200` → `License` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error`

### `POST /admin/v1/tenants/{tenant_id}/suspend`

**Suspender un tenant** · `adminSuspendTenant`

Pone la licencia en `suspended`. Los envíos en cola pasan a `failed` con `license_inactive`.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Cuerpo**:

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `reason` | string |  |  |

**Respuestas**: `200` → `License` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `PUT /admin/v1/tenants/{tenant_id}/apps/{app_id}`

**Habilitar o deshabilitar una app para un tenant** · `adminSetTenantApp`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |
| `app_id` | path | sí | string (`app_…`) |  |

**Cuerpo**:

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `enabled` | boolean | sí |  |

**Respuestas**: `200` → `TenantApp` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `POST /admin/v1/tenants/{tenant_id}/whatsapp/manual-connect`

**Conectar un número a mano con un token** · `adminManualConnect`

Para desarrollo y soporte: asocia una WABA y un número ya existentes usando un token de acceso que se guarda cifrado. No sustituye al Embedded Signup. Comprueba el token con `debug_token` y lee el número y la WABA. Se desactiva con `ALLOW_MANUAL_CONNECT=false`.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Cuerpo** (`ManualConnect`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `waba_id` | string | sí |  |
| `phone_number_id` | string | sí |  |
| `access_token` | string | sí | Token con `whatsapp_business_messaging` y `whatsapp_business_management`. Se guarda cifrado. |
| `billing_mode` | [BillingMode](#billingmode) |  |  |
| `onboarding_type` | `dedicated` \| `business_app` |  | `business_app` si el número también está en la app WhatsApp Business: no se registra y se pide la sincronización de contactos e historial. |
| `two_step_pin` | string |  | PIN de 6 dígitos si el número ya estaba registrado. |
| `is_default` | boolean |  |  |

**Respuestas**: `201` → `Number` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error` · `502` → `Error` · `503` → `Error`

### `GET /admin/v1/tenants/{tenant_id}/export`

**Exportar los datos de un tenant** · `adminExportTenant`

Genera un fichero con todos sus datos para la baja. Responde `202` con el identificador del trabajo.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Respuestas**: `202` → `JobAccepted` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `501` → `Error`

### `GET /admin/v1/apps`

**Listar apps** · `adminListApps`

- Autenticación: `AdminToken`

**Respuestas**: `200`  · `401` → `Error` · `403` → `Error`

### `POST /admin/v1/apps`

**Registrar una app de vuestro ecosistema** · `adminCreateApp`

- Autenticación: `AdminToken`

**Cuerpo** (`AppCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `code` | string | sí |  |
| `name` | string | sí |  |
| `allowed_origins` | string (uri)[] |  |  |

Ejemplo (erp):

```json
{
  "code": "erp",
  "name": "ERP de gestión",
  "allowed_origins": [
    "https://erp.cliente.com"
  ]
}
```

**Respuestas**: `201` → `App` · `401` → `Error` · `403` → `Error` · `409` → `Error` · `422` → `Error`

### `PATCH /admin/v1/apps/{app_id}`

**Editar una app** · `adminUpdateApp`

Cambiar `allowed_origins` actualiza las cabeceras `frame-ancestors` de la UI embebible.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `app_id` | path | sí | string (`app_…`) |  |

**Cuerpo** (`AppUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `name` | string |  |  |
| `allowed_origins` | string (uri)[] |  |  |
| `status` | `active` \| `disabled` |  |  |

**Respuestas**: `200` → `App` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error`

### `GET /admin/v1/apps/{app_id}/api-keys`

**Listar las claves de una app** · `adminListAppApiKeys`

Nunca devuelve el valor completo, solo el prefijo. Incluye las revocadas.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `app_id` | path | sí | string (`app_…`) |  |

**Respuestas**: `200`  · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `POST /admin/v1/apps/{app_id}/api-keys`

**Crear una clave de API para una app** · `adminCreateAppApiKey`

Sin `tenant_id` la clave es multi-tenant y exige `X-Tenant-Id` o `X-Tenant-Ref` en cada llamada. El valor completo se devuelve una sola vez.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `app_id` | path | sí | string (`app_…`) |  |

**Cuerpo** (`AppApiKeyCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `name` | string |  |  |
| `tenant_id` | string (`ten_…`) |  |  |
| `scopes` | [Scope](#scope)[] | sí |  |
| `rate_limit_per_min` | integer |  |  |
| `expires_at` | string (date-time) |  |  |

**Respuestas**: `201` → `ApiKeyCreated` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error`

### `GET /admin/v1/apps/{app_id}/webhook-subscriptions`

**Listar las suscripciones de una app** · `adminListAppWebhookSubscriptions`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `app_id` | path | sí | string (`app_…`) |  |

**Respuestas**: `200`  · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `POST /admin/v1/apps/{app_id}/webhook-subscriptions`

**Suscripción de toda una app a los eventos** · `adminCreateAppWebhookSubscription`

Recibe los eventos de todos los tenants para los que la app está habilitada. El secreto se devuelve una sola vez.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `app_id` | path | sí | string (`app_…`) |  |

**Cuerpo** (`WebhookSubscriptionCreate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `url` | string (uri) | sí | Solo `https`. |
| `events` | string[] | sí |  |

**Respuestas**: `201` → `WebhookSubscriptionCreated` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `422` → `Error`

### `GET /admin/v1/system`

**Configuración de la plataforma** · `adminSystem`

Qué parámetros de Meta y de la plataforma están configurados. Nunca devuelve secretos, solo si existen.

- Autenticación: `AdminToken`

**Respuestas**: `200` → `AdminSystem` · `401` → `Error` · `403` → `Error`

### `GET /admin/v1/tenants/{tenant_id}/whatsapp`

**Cuenta de WhatsApp y números de un tenant** · `adminGetTenantWhatsApp`

Parámetros del WABA, estado del token y números conectados. El token nunca se devuelve.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |

**Respuestas**: `200` → `AdminTenantWhatsApp` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `PATCH /admin/v1/tenants/{tenant_id}/whatsapp/numbers/{number_id}`

**Fijar el número predeterminado** · `adminUpdateTenantNumber`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |
| `number_id` | path | sí | string (`num_…`) |  |

**Cuerpo** (`TenantNumberUpdate`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `is_default` | `true` | sí |  |

**Respuestas**: `200` → `Number` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `409` → `Error` · `422` → `Error`

### `DELETE /admin/v1/tenants/{tenant_id}/whatsapp/numbers/{number_id}`

**Desconectar un número** · `adminDisconnectTenantNumber`

Deja de enviar y recibir por ese número en la plataforma. No lo da de baja en Meta.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |
| `number_id` | path | sí | string (`num_…`) |  |

**Respuestas**: `204` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `POST /admin/v1/tenants/{tenant_id}/whatsapp/numbers/{number_id}/refresh`

**Releer un número desde Meta** · `adminRefreshTenantNumber`

Actualiza calidad, límite, nombre y estado con el token del tenant. Si Meta rechaza el token, queda marcado como inválido.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | path | sí | string (`ten_…`) |  |
| `number_id` | path | sí | string (`num_…`) |  |

**Respuestas**: `200` → `Number` · `401` → `Error` · `403` → `Error` · `404` → `Error` · `502` → `Error`

### `DELETE /admin/v1/api-keys/{key_id}`

**Revocar cualquier clave de API** · `adminRevokeApiKey`

Sirve también para las claves multi-tenant, que un tenant no puede revocar.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `key_id` | path | sí | string (`key_…`) |  |

**Respuestas**: `204` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `DELETE /admin/v1/apps/{app_id}/webhook-subscriptions/{subscription_id}`

**Eliminar una suscripción de una app** · `adminDeleteAppWebhookSubscription`

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `app_id` | path | sí | string (`app_…`) |  |
| `subscription_id` | path | sí | string (`whs_…`) |  |

**Respuestas**: `204` · `401` → `Error` · `403` → `Error` · `404` → `Error`

### `GET /admin/v1/audit-log`

**Registro de auditoría** · `adminAuditLog`

Cambios hechos por administración, apps y agentes. Del más reciente al más antiguo.

- Autenticación: `AdminToken`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `tenant_id` | query |  | string (`ten_…`) |  |
| `limit` | query |  | integer |  |
| `cursor` | query |  | string |  |

**Respuestas**: `200` → `AuditPage` · `401` → `Error` · `403` → `Error` · `422` → `Error`

### `GET /admin/v1/health`

**Estado operativo** · `adminHealth`

Colas, trabajos en `dead`, números con problemas y pagos bloqueados.

- Autenticación: `AdminToken`

**Respuestas**: `200` → `AdminHealth` · `401` → `Error` · `403` → `Error`

### `POST /admin/v1/webhook-events/reprocess`

**Reprocesar eventos de Meta** · `adminReprocessWebhookEvents`

Vuelve a poner en cola los eventos de Meta guardados en un rango, normalmente los `failed`.

- Autenticación: `AdminToken`

**Cuerpo** (`ReprocessRequest`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `from` | string (date-time) | sí |  |
| `to` | string (date-time) | sí |  |
| `status` | `failed` \| `received` \| `ignored` |  |  |

**Respuestas**: `202`  · `401` → `Error` · `403` → `Error` · `422` → `Error`

## Meta

Endpoint que recibe los webhooks de Meta (servicio `ingest`).

### `GET /webhooks/meta`

**Verificación del webhook de Meta** · `metaWebhookVerify`

Si `hub.mode` es `subscribe` y `hub.verify_token` coincide, devuelve `hub.challenge` en texto plano.

- Autenticación: ninguna (pública)

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `hub.mode` | query |  | string |  |
| `hub.verify_token` | query |  | string |  |
| `hub.challenge` | query |  | string |  |

**Respuestas**: `200` · `403`

### `POST /webhooks/meta`

**Recepción de eventos de Meta** · `metaWebhookReceive`

Valida `X-Hub-Signature-256` (HMAC-SHA256 del cuerpo sin parsear con el `APP_SECRET`), guarda cada elemento de `entry[].changes[]` en `inbound_webhook_event` con deduplicación y responde `200` en menos de un segundo. Una firma inválida devuelve `401`.

- Autenticación: `MetaSignature`

| Parámetro | En | Obligatorio | Tipo | Descripción |
| --- | --- | --- | --- | --- |
| `X-Hub-Signature-256` | header |  | string | sha256=<hmac>. Si falta o no coincide la respuesta es 401 (no 422). |

**Cuerpo** (`MetaWebhookPayload`):

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `object` | `whatsapp_business_account` |  |  |
| `entry` | object[] |  |  |

**Respuestas**: `200` · `401`

## Health

Comprobaciones de salud.

### `GET /healthz`

**El proceso está vivo** · `healthz`

- Autenticación: ninguna (pública)

**Respuestas**: `200` → `Health`

### `GET /readyz`

**El proceso puede atender tráfico** · `readyz`

Comprueba la conexión con la base de datos.

- Autenticación: ninguna (pública)

**Respuestas**: `200` → `Health` · `503` → `Health`

## Esquemas

Los esquemas citados arriba, tal como los define el contrato.

### MessageStatus


Valores: `queued`, `sending`, `sent`, `delivered`, `read`, `failed`, `received`

### MessageDirection


Valores: `inbound`, `outbound`

### TemplateVariables

Valores por posición (plantillas `positional`) o por nombre (plantillas `named`).

Una de estas variantes: string[], object.

### OptInStatus


Valores: `unknown`, `opted_in`, `opted_out`

### ConversationStatus


Valores: `open`, `pending`, `closed`

### TemplateCategory


Valores: `marketing`, `utility`, `authentication`

### TemplateStatus


Valores: `draft`, `pending`, `approved`, `rejected`, `paused`, `disabled`, `in_appeal`, `deleted`

### TemplateHeader


| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `type` | `text` \| `image` \| `video` \| `document` | sí |  |
| `text` | string |  |  |

### TemplateBodyInput


| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `text` | string | sí |  |
| `examples` | string[] \| object |  | Un ejemplo por variable, en el mismo orden (o por nombre en plantillas `named`). |

### TemplateButton


| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `type` | `quick_reply` \| `url` \| `phone_number` | sí |  |
| `text` | string | sí |  |
| `url` | string (uri) |  |  |
| `phone_number` | string |  |  |

### BillingMode


Valores: `meta_direct`, `platform_credit_line`

### AgentRole


Valores: `agent`, `supervisor`, `admin`

### Scope


Valores: `messages:send`, `messages:read`, `conversations:read`, `conversations:write`, `contacts:write`, `templates:read`, `templates:write`, `numbers:read`, `numbers:write`, `agents:write`, `keys:write`, `usage:read`, `settings:write`, `webhooks:write`, `embed:create`

### LicenseStatus


Valores: `trial`, `active`, `suspended`, `expired`

### LicenseLimits


| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `max_numbers` | integer |  |  |
| `max_agents` | integer |  |  |
| `max_templates` | integer |  |  |
| `max_outbound_per_month` | integer |  |  |
| `max_ai_conversations_per_month` | integer |  | Lo hace cumplir el servicio del bot, que lo lee de `GET /v1/license`; el gateway solo lo guarda. |
| `features` | [LicenseFeature](#licensefeature)[] |  |  |

### DeliveryStatus


Valores: `pending`, `delivered`, `failed`, `dead`

### EmbedScreen


Valores: `chat`, `templates`, `settings`, `costs`, `signup`

### EmbedUser


| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `external_user_id` | string | sí |  |
| `name` | string | sí |  |
| `email` | string (email) |  |  |
| `role` | [AgentRole](#agentrole) | sí |  |

### TenantStatus


Valores: `active`, `suspended`, `closed`

