
# API de Ubicual

Referencia de la API REST de Ubicual (email y SMS marketing) para desarrolladores y agentes de IA.
Todo lo que aparece aquí actúa en nombre de la cuenta dueña del token. También está como página
(https://www.ubicual.com/docs/api) y como especificación OpenAPI (https://www.ubicual.com/docs/openapi.yaml).

> ¿Usas ChatGPT, Claude u otro asistente compatible con MCP? Es más sencillo conectar el servidor MCP
> `https://subscribers.ubicual.com/mcp` (OAuth o el mismo token como `Authorization: Bearer`), que ya expone estas acciones
> como herramientas. Guía: https://www.ubicual.com/ia

## Conceptos

- **Cuenta**: el usuario de Ubicual. Las cuentas de **agencia** tienen además **clientes**; listas,
  plantillas y envíos pueden ser de la cuenta principal (`client_id` vacío) o de un cliente (`client_id`).
- **Contacto**: email y/o móvil, con nombre, fecha de nacimiento, ID externo y **campos personalizados**.
  Un contacto no se duplica dentro de la cuenta.
- **Lista**: grupo de contactos. La suscripción es **por lista y por canal** (email, SMS): darse de baja
  de una lista no afecta a las demás. Los envíos se hacen a listas.
- **Plantilla**: el HTML de un email. Tiene que incluir un enlace de baja.
- **Créditos**: cada email gasta 1 crédito; cada SMS, 1 crédito por segmento (160 caracteres) y destinatario.
  Sin créditos suficientes el envío se rechaza.
- **Remitente**: los emails solo pueden salir de una dirección o dominio verificado en el panel (https://email.ubicual.com/emailVerify).

## Servidores

La API está repartida en tres servicios con el mismo token:

| Servicio | URL base | Para |
|---|---|---|
| Contactos | `https://subscribers.ubicual.com/api` | Contactos, listas, campos personalizados, campañas |
| Email | `https://email.ubicual.com/api` | Plantillas, envíos de email y sus estadísticas |
| SMS | `https://sms.ubicual.com/api` | Envíos de SMS y sus estadísticas |
| Landing | `https://landing.ubicual.com/api` | Landing pages y capturas (formularios y WordPress) |

## Autenticación

1. Crea un token en el panel: **Mi cuenta → Tokens de API** (https://www.ubicual.com/tokens). Solo se muestra una vez.
2. Envíalo en cada petición:

```
Authorization: Bearer TU_TOKEN
Accept: application/json
```

- Envía siempre `Accept: application/json`: sin él, el servicio de Email redirige al login en vez de
  devolver JSON.
- **El servicio Landing no admite la cabecera `Authorization`**: envía el token como parámetro
  `api_token` (en el cuerpo o en la URL). Los demás servicios también lo aceptan así.
- Los cuerpos pueden ir como JSON (`Content-Type: application/json`) o como formulario.
- Token incorrecto o ausente: `401 {"error": "Unauthenticated."}`.
- Fechas y horas en la zona horaria de España peninsular (Europe/Madrid).

## Límites

- Contactos: 150 peticiones por minuto. Email y SMS: 60 por minuto.
- Crear envíos y plantillas: 30 por minuto. Consultar estadísticas: 60 por minuto.
- Al superarlos: `429 Too Many Requests`. Espera y reintenta.

## Errores

- Email y SMS: `422 {"message": "...", "errors": {"campo": ["..."]}}` para datos no válidos;
  `422 {"message": "..."}` para errores de negocio (sin créditos, sin contactos…); `404` si el recurso no es tuyo.
- Contactos: `400 {"message": "Error. ..."}`. **Atención:** varias acciones de Contactos responden
  `400 {"message": "Ok"}` cuando han ido bien (se indica en cada una). Comprueba `message`, no solo el código.

---

## Flujo típico para enviar un email

1. `GET https://subscribers.ubicual.com/api/contact/fields` (opcional): campos personalizados disponibles.
2. `POST https://subscribers.ubicual.com/api/list/add`: crea la lista (o usa una existente con `POST https://subscribers.ubicual.com/api/list`).
3. `POST https://subscribers.ubicual.com/api/contact/add`, una vez por contacto, con `list_id`.
4. `POST https://email.ubicual.com/api/v1/templates`: sube el HTML (o usa una plantilla creada en el panel).
5. `POST https://email.ubicual.com/api/v1/emails`: envía la plantilla a las listas.
6. `GET https://email.ubicual.com/api/v1/emails?id=ID`: consulta entregas, aperturas, clics y bajas.

Solo añade contactos que hayan dado su **consentimiento expreso** para recibir comunicaciones (RGPD).

---

## Contactos y listas · `https://subscribers.ubicual.com/api`

### Listar las listas

`POST /list` → `200 [{"id": 12, "name": "Clientes", "created_at": "...", "updated_at": "..."}]`

Devuelve todas las listas de la cuenta, también las de sus clientes de agencia.

### Crear o renombrar una lista

`POST /list/add`

| Parámetro | Tipo | |
|---|---|---|
| `name` | texto | Obligatorio |
| `list_id` | entero | Opcional: si se indica, renombra esa lista |

→ `200` con la lista `{"id", "name", ...}`. Las listas creadas por API son de la cuenta principal.
Errores: `Error. You must provide name field`, `Error. You are not list owner`.

### Crear o actualizar un contacto

`POST /contact/add`

| Parámetro | Tipo | |
|---|---|---|
| `email` | texto | Email válido |
| `phone` | texto | Móvil de 9 dígitos, solo números y sin prefijo (`612345678`): se guarda tal cual |
| `name`, `surname` | texto | |
| `birthdate` | fecha | `AAAA-MM-DD` o `DD/MM/AAAA` |
| `external_id` | texto | Tu identificador del contacto |
| `list_id` | entero | Lista a la que añadirlo (una por llamada) |
| `campaign_id` | entero | Campaña a la que asociarlo |
| `gender_id` | entero | 1 mujer, 2 hombre |
| `city_id` | entero | Rellena también provincia, región y país |
| `source` | texto | Origen libre (p. ej. `web`, `crm`) |
| `custom_fields` | objeto | Campos personalizados `{"clave": valor}` (ver abajo) |

Hace falta al menos uno de `name`, `surname`, `email` o `phone`.

**Si el contacto ya existe** (se busca, en este orden, por `external_id`, por `phone` y por `email`) no se
duplica: se completan sus datos **vacíos**, se añade a `list_id`, se actualizan los `custom_fields` enviados y
se marca como activo (aunque estuviera desactivado).

```json
POST https://subscribers.ubicual.com/api/contact/add
{"email": "laura@example.com", "name": "Laura", "list_id": 12,
 "custom_fields": {"numero_socio": 1042, "fecha_alta": "2024-03-15", "vip": true}}
```

Respuestas:
- Nuevo: `200 {"message": "success", "contact": {"id": 881, "email": "...", "custom_fields": {...}, "lists": [...], ...}}`
- Ya existía: `200 {"message": "success", "contacts": [ ... ]}`
- Errores `400`: `Invalid email address`, `Error. You are not list owner`, `Error. You are not campaign owner`,
  `Error. Date must have "Y-m-d" or "d/m/Y" format`,
  `Error. You must provide at least one field ("name","surname","email","phone")`, y los de campos personalizados.

### Campos personalizados

Se crean en el panel (https://subscribers.ubicual.com/contact-fields). Cada uno tiene una **clave** fija, que es la que se usa en la API.

`GET /contact/fields` → `200 [{"key": "numero_socio", "name": "N.º de socio", "type": "number"}]`

| Tipo | Valores aceptados | Se guarda como |
|---|---|---|
| `text` | Texto de hasta 500 caracteres | Texto |
| `number` | `12`, `12.5`, `"1.250,5"` | Número |
| `date` | `AAAA-MM-DD`, `DD/MM/AAAA`, `DD-MM-AAAA` | `AAAA-MM-DD` |
| `boolean` | `true`/`false`, `1`/`0`, `"sí"`/`"no"` | `true`/`false` |

- En `custom_fields` envía solo los campos que quieras cambiar: los demás se conservan.
- `null` o `""` borra el valor de ese campo.
- Una clave que no existe o un valor no válido rechaza la petición entera con `400`
  (p. ej. `Error. El campo personalizado «nivel» no existe. Créalo antes en Campos personalizados.`).
- También se rellenan en la importación de archivos, en los formularios de landing (`cf_{clave}`, ver
  [Formularios propios](#formularios-propios)) y desde el plugin de WordPress.
- En los emails se usan como variable `__{clave}__` (ver [Variables de los emails](#variables-de-los-emails)).

### Consultar un contacto

`POST /contact` con `contact_id` (entero) **o** `external_id`.

- Por `contact_id`: `200` con el contacto, sus `lists`, `campaigns`, ubicación y `custom_fields`.
- Por `external_id`: `200` con la lista de contactos que tienen ese ID externo.
- Errores `400`: `Error. Contact not found`, `Error. You are not contact owner`.

### Contactos de una lista

`GET /api_contacts?list_id=12` → `200 {"status": true, "contacts": [...]}`. Lista ajena o inexistente: `404`.

### Añadir o quitar un contacto de listas

- `POST /list/contact/add` con `contact_id` y `lists` (array de enteros). Responde **`400 {"message": "Ok"}` si ha ido bien**.
- `POST /list/contact/delete` con los mismos parámetros. Responde **`400 {"message": "Ok"}` si ha ido bien**.

### Suscripción por canal en una lista

`POST /contact/channels`

| Parámetro | Tipo | |
|---|---|---|
| `contact_id`, `list_id` | entero | Obligatorios |
| `active_email` | 0/1 | Opcional: recibir emails de esa lista |
| `active_sms` | 0/1 | Opcional: recibir SMS de esa lista |

Solo cambia los canales enviados y queda en el registro de consentimiento. Responde **`400 {"message": "Ok"}` si ha ido bien**.
Para dar de baja a alguien que lo pide, usa `active_email=0` (o `active_sms=0`) en esa lista.

### Activar, desactivar o borrar un contacto

- `POST /contact/disable` con `contact_id` y `status` (`1` activo, `0` inactivo; **envía siempre `status`**).
  Un contacto inactivo no recibe nada. Responde **`400 {"message": "Ok"}` si ha ido bien**.
- `POST /contact/delete` con `contact_id`: **borra el contacto definitivamente** de todas las listas.
  Responde **`400 {"message": "Ok"}` si ha ido bien**.

### Campañas

Agrupan contactos captados por una acción (no son envíos).

- `POST /campaign` → `200` con las campañas de la cuenta.
- `POST /campaign/add` con `name`, `start_date` y `end_date` (`AAAA-MM-DD HH:MM:SS` o `DD/MM/AAAA HH:MM:SS`) → `200` con la campaña.

---

## Email · `https://email.ubicual.com/api`

### Crear una plantilla

`POST /v1/templates`

| Parámetro | Tipo | |
|---|---|---|
| `name` | texto | Obligatorio, hasta 191 caracteres |
| `html` | texto | Obligatorio, hasta 1 MB |
| `client_id` | entero | Opcional: plantilla de un cliente de agencia |
| `analytics` | booleano | Añade `utm_source=ubicual.com&utm_medium=email` a los enlaces |

El HTML **tiene que incluir un enlace de baja**: escribe `*|UNSUB|*` como `href` y se sustituye por el
enlace de baja de cada contacto: `<a href="*|UNSUB|*">Darse de baja</a>`. Usa HTML de email (tablas,
estilos en línea, ancho de 600 px, imágenes con URL absoluta y sin WebP).

→ `201 {"id": 77, "name": "...", "client_id": null, "preview_url": "..."}`

Errores `422`: `Su correo debe contener un enlace para desuscribirse.`, enlaces a dominios no seguros, `El HTML no puede superar 1 MB.`

### Variables de los emails

Se sustituyen en cada email por los datos del destinatario (vacías si no tiene el dato):

| Variable | Dato |
|---|---|
| `__nombre__`, `__apellidos__` | Nombre y apellidos |
| `__telefono__` | Teléfono |
| `__idexterno__` | ID externo |
| `__{clave}__` | Campo personalizado, p. ej. `__numero_socio__`. Fechas como `DD/MM/AAAA`, sí/no como «Sí»/«No» |
| `*\|UNSUB\|*` | Enlace de baja (obligatorio) |

Las variables se sustituyen en el cuerpo, no en el asunto.

### Actualizar una plantilla

`PUT /v1/templates/{id}` con `html` (obligatorio), `name` y `analytics` opcionales → `200`, igual que al crear.
No se pueden editar las plantillas hechas con el editor visual del panel, ni las que tienen un envío pendiente.

### Plantilla con imágenes (ZIP)

1. `POST /v1/templates/upload-link` con `name` y `client_id` opcionales →
   `201 {"upload_url": "...", "expires_at": "AAAA-MM-DD HH:MM"}` (válido 2 horas).
2. `POST` multipart a `upload_url` con `file` = un `.zip` (máx. 20 MB) que contenga `index.html` y sus
   imágenes con rutas relativas, y `Accept: application/json` →
   `201 {"id", "name", "client_id", "preview_url", "skipped"}`. Esa URL no necesita token.

### Enviar un email

`POST /v1/emails`

| Parámetro | Tipo | |
|---|---|---|
| `template_id` | entero | Obligatorio, plantilla de la cuenta |
| `subject` | texto | Obligatorio, hasta 255 caracteres |
| `from` | email | Obligatorio: dirección verificada, o cualquiera de un dominio verificado |
| `from_name` | texto | Obligatorio, hasta 100 caracteres |
| `list_ids` | array de enteros | Obligatorio, de 1 a 50 listas, todas de la cuenta principal o todas del `client_id` |
| `client_id` | entero | Opcional: envío de un cliente de agencia (pie legal con sus datos) |
| `campaign_id` | entero | Opcional |
| `reason` | texto | Opcional: por qué recibe este email (hasta 500 caracteres) |
| `web_version` | booleano | Enlace «Ver en el navegador» al principio (por defecto `true`) |
| `send_at` | fecha y hora | Opcional, futura (`AAAA-MM-DD HH:MM`): programa el envío |

Llega a los contactos de las listas suscritos al email, **una sola vez por dirección** aunque estén en varias listas.

```json
POST https://email.ubicual.com/api/v1/emails
{"template_id": 77, "subject": "Novedades de octubre", "from": "hola@tienda.es",
 "from_name": "Tienda", "list_ids": [12, 15]}
```

→ `201 {"id": 5120, "status": "queued", "send_at": null, "recipients": 1840, "credits_used": 1840}`
(`status` es `scheduled` si se programó).

Errores `422`: `Créditos insuficientes`, `No existe ningún contacto en la selección.`,
`El remitente tiene que ser una dirección verificada de tu cuenta.`, `La plantilla no existe o no es tuya.`,
`Alguna lista no existe, no es tuya o no es del cliente indicado.`, `La fecha de envío tiene que ser futura.`

### Estadísticas de email

`GET /v1/emails` con `id` (un envío), o con `limit` (1–25, por defecto 10) para los últimos; `client_id` para los de un cliente.

```json
{"emails": [{"id": 5120, "subject": "...", "status": "Completado", "send_at": "...",
  "sent": 1840, "delivered": 1802, "opens": 950, "unique_opens": 701, "open_rate": 38.1,
  "clicks": 210, "unique_clicks": 160, "clickers": 133, "click_rate": 18.97,
  "unsubscribes": 4, "bounces_permanent": 12, "bounces_temporary": 6, "bounce_rate": 0.65, "complaints": 0}]}
```

- `open_rate` = aperturas únicas / enviados (%). `click_rate` = contactos con clic / aperturas únicas (%).
- No cuentan aperturas ni clics automáticos (escáneres, Apple Mail Privacy Protection).
- Envío ajeno o inexistente: `404 {"message": "El envío no existe o no es tuyo."}`

---

## SMS · `https://sms.ubicual.com/api`

### Enviar un SMS

`POST /v1/sms`

| Parámetro | Tipo | |
|---|---|---|
| `text` | texto | Obligatorio, hasta 1600 caracteres (cada 160 es un segmento) |
| `list_ids` | array de enteros | Obligatorio, de 1 a 50 listas |
| `country` | `34` o `351` | España (por defecto) o Portugal |
| `client_id` | entero | Opcional |
| `campaign_id` | entero | Opcional |
| `short_urls` | booleano | Acorta los enlaces del texto |
| `analytics` | booleano | Añade parámetros UTM a los enlaces |
| `unsubscribe_link` | booleano | Añade «Baja: https://ubi.lt/…» personal al final (cuenta en la longitud) |
| `sender_prefix` | texto | Hasta 20 caracteres, se antepone: «TIENDA: texto» |
| `send_at` | fecha y hora | Opcional, futura: programa el envío |

Llega a los contactos de las listas con móvil y suscritos al SMS. Gasta segmentos × destinatarios créditos
del país. → `201 {"id": 930, "status": "queued", "send_at": null, "credits_used": 412, "country": "34"}`

Errores `422`: `Créditos insuficientes`, `No existe ningún contacto en la selección.`,
`Estas listas no existen, no son tuyas o no son del cliente indicado: …`, `El país tiene que ser 34 (España) o 351 (Portugal).`

### Estadísticas de SMS

`GET /v1/sms` con `id`, o `limit` (1–25) y `client_id`.

`{"sms": [{"id", "from", "text", "country", "send_at", "sent", "delivered", "pending", "failed", "delivery_rate", "segments"}]}`

`sent` son créditos (segmentos × destinatarios). `failed` agrupa caducados, no entregables y rechazados.

---

## Landing pages y capturas · `https://landing.ubicual.com/api`

Las landing pages y las **capturas** (formularios para incrustar o para el plugin de WordPress) dan de alta a
quien las rellena en sus listas y campaña. Autenticación con el parámetro `api_token` (no con `Authorization`).

- `POST /landings?api_token=TOKEN` → `200` con todas las landings y capturas de la cuenta (`id`, `name`, `type`
  —1 landing page, 2 captura de WordPress—, fechas de inicio y fin, `client_id` y `token`, el token público del formulario).
- `POST /landing/{id}?api_token=TOKEN` → `200` con la landing, sus `lists` y los datos del titular para el aviso
  legal (`user.policy_link`). De otra cuenta: `401 {"message": "Unauthorized"}`; inexistente: `404`.

### Formularios propios

Para dar de alta contactos desde un formulario hecho por ti con las listas, la campaña y el aviso legal de una
landing o captura. **No usa el token de API** (iría en el navegador), sino el `token` público de la landing:

`POST https://subscribers.ubicual.com/api/addcontact`

| Parámetro | |
|---|---|
| `token` | Obligatorio: el `token` público de la landing o captura |
| `email`, `phone`, `name`, `surname` | Al menos uno |
| `birthdate` | `DD/MM/AAAA` o `AAAA-MM-DD` |
| `gender_id` | 1 mujer, 2 hombre |
| `cf_{clave}` | Campo personalizado, p. ej. `cf_numero_socio=1042` (o un objeto `custom_fields`) |

→ `200 {"status": true}` o `200 {"status": false, "message": "..."}` (p. ej. `Formulario no válido`,
`Esta landing page no está activa actualmente. Gracias`, un campo personalizado no válido).

- Las listas y la campaña salen siempre de la landing, nunca de la petición.
- Si el contacto ya existe (mismo teléfono o email) solo se completan sus datos vacíos y no se reactiva si estaba desactivado.
- Pide la casilla de aceptación de la política de privacidad antes de enviar (`user.policy_link` de la landing).

### WordPress y WooCommerce

Lo usa el plugin de Ubicual para WordPress; sirve para cualquier integración que quiera avisar de registros o pedidos.

`POST https://subscribers.ubicual.com/api/v1/wordpress/events` (con el token de API)

| Parámetro | Tipo | |
|---|---|---|
| `type` | `wp_register` o `wc_order` | Obligatorio: registro de usuario o pedido |
| `token` | texto | Obligatorio: `token` público de una captura de la cuenta |
| `email` | email | Obligatorio |
| `name`, `surname` | texto | Hasta 255 caracteres |
| `phone` | texto | Hasta 30 caracteres |
| `consent` | booleano | `true` si la persona aceptó recibir comunicaciones: se da de alta en las listas de la captura |
| `ip`, `user_agent` | texto | De la persona (no del servidor), para el registro de consentimiento |
| `custom_fields` | objeto | Campos personalizados `{"clave": valor}` |
| `order` | objeto | Pedido: `id`, `number`, `total`, `currency` (3 letras) e `items` (hasta 100, cada uno con `name` y `quantity`) |

- Con `consent: true` se da de alta (o se completa) el contacto. Sin él, el evento solo se apunta a quien ya era
  contacto con ese email.
- El evento queda en el contacto y puede iniciar las automatizaciones «Se registra en tu WordPress» y
  «Hace un pedido en WooCommerce».

→ `200 {"status": true, "contacts": 1}`. Errores `422`: `Captura no válida` (token de otra cuenta o inexistente),
`Lista no válida`, `Campaña no válida`, y los de validación de los campos.

---

## Buenas prácticas para agentes

- Confirma con el usuario antes de **enviar** (gasta créditos y no se puede deshacer) o de **borrar** contactos.
- Envía primero a una lista de prueba pequeña cuando el usuario no haya revisado la plantilla.
- Respeta las bajas: no vuelvas a suscribir con `contact/channels` a quien se dio de baja.
- Usa `external_id` con tu identificador para encontrar el contacto después sin guardar el id de Ubicual.
- Ante `429`, espera unos segundos; ante `422`, lee `message` y corrige los datos en vez de reintentar igual.
