API de ConvoChat
Una API REST para gestionar enlaces, QR, páginas bio, dominios, proyectos y catálogos de presentación desde tu propio código, y leer sus estadísticas.
La API requiere un plan de pago.
El plan Free no la incluye: sin ella toda petición responde 403 api_access_denied, y ni el panel deja emitir API keys.
Autenticación
Crea una API key en /app/api-keys (menú «Desarrollo» → «API keys»), elige sus scopes y envíala en cada petición como cabecera Authorization: Bearer <token>. El token se muestra una sola vez al crearlo; cópialo entonces. Todas las respuestas son JSON.
curl -s https://convo.chat/api/v1/links \
-H "Authorization: Bearer TU_API_KEY" \
-H "Accept: application/json" Scopes
Hay 19 scopes, agrupados por recurso. Cada API key concede solo los que elijas; si a un token le falta el scope de la ruta, esta responde 403 missing_ability.
| Enlaces cortos links:read · links:write | Listar, crear, actualizar y eliminar enlaces cortos. |
| Analítica stats:read | Leer las estadísticas de un enlace. También destapa clicks y unique_clicks dentro del propio recurso enlace. |
| Códigos QR qr:read · qr:write | Listar, crear, actualizar y eliminar códigos QR dinámicos. |
| Páginas bio bio:read · bio:write | Listar, crear, actualizar y eliminar páginas bio. |
| Dominios domains:read · domains:write | Listar, dar de alta, verificar y eliminar tus dominios de marca. |
| Proyectos projects:read · projects:write | Las carpetas con las que agrupas tus enlaces. |
| Píxeles pixels:read · pixels:write | Píxeles de seguimiento. |
| Páginas de espera splash:read · splash:write | Splash pages intersticiales previas al destino. |
| Overlays overlays:read · overlays:write | Barras overlay que se muestran sobre el destino. |
| Datos capturados data:read · data:write | Leer y eliminar los envíos de tus formularios bio. El alta llega desde la página pública, así que data:write solo borra. |
Endpoints
Base: https://convo.chat/api/v1. Son 45 endpoints repartidos en 9 recursos; despliega el que te interese. Todas las listas se paginan con per_page (1–100, por defecto 15) y devuelven data, links y meta. Cada DELETE responde 204 sin cuerpo.
Enlaces cortos 6 endpoints
El {id} es el public_id del enlace.
/links
Lista tus enlaces cortos, paginado (per_page entre 1 y 100; por defecto 15).
links:read
/links
Crea un enlace (201) o te devuelve uno igual que ya tienes (200). Lee «Creación y reutilización».
links:write
/links/{id}
Obtiene un enlace.
links:read
/links/{id}
Actualiza un enlace. El slug no se puede cambiar.
links:write
/links/{id}
Elimina un enlace.
links:write
/links/{id}/stats
Totales, serie diaria y horaria y desgloses (days: 7, 30 o 90).
stats:read
Códigos QR 5 endpoints
El {id} es el public_id del QR.
/qr-codes
Lista tus códigos QR, paginado.
qr:read
/qr-codes
Crea un código QR: dinámico (tipo url, con destino editable) o con el contenido dentro del dibujo.
qr:write
/qr-codes/{id}
Obtiene un código QR, con su content y su design.
qr:read
/qr-codes/{id}
Actualiza un código QR: solo lo que mandas, incluido el destino de uno dinámico.
qr:write
/qr-codes/{id}
Elimina un código QR.
qr:write
Páginas bio 5 endpoints
El {id} es el public_id de la página.
/bio-pages
Lista tus páginas bio, paginado.
bio:read
/bio-pages
Crea una página bio.
bio:write
/bio-pages/{id}
Obtiene una página bio.
bio:read
/bio-pages/{id}
Actualiza una página bio.
bio:write
/bio-pages/{id}
Elimina una página bio.
bio:write
Dominios 6 endpoints
El {id} es el id numérico. El dominio principal no está en la lista: en un enlace o una página es domain_id 0. Con GET /domains?include=shared la lista añade, al final, los dominios compartidos del operador que tu plan deja usar —solo id, host, is_shared y status—, y su id vale como domain_id; no se pueden abrir, cambiar ni borrar. default_link_id es el id (public_id) del enlace exclusivo. Cada dominio trae su ciclo de verificación: verification_last_checked_at y verification_last_result (found, missing, mismatch o dns_error) dicen qué vimos la última vez; con mismatch, verification_last_values trae hasta 3 valores TXT que encontramos en lugar del token, recortados a 100 caracteres y con cada carácter invisible marcado con �, y fuera de mismatch es una lista vacía; verification_failed_at con estado pending es que ya no lo comprobamos solos, y disabled_by (owner u operator) dice quién lo desactivó. not_found_url tiene que ser de otro dominio y root_redirect_url no puede ser la portada del propio dominio (sí una ruta suya, como /mi-bio); con default_link_id, root_redirect_url se guarda vacío.
/domains
Lista tus dominios propios, paginado. Con ?include=shared, también los compartidos que puedes usar.
domains:read
/domains
Da de alta un dominio; queda en estado pending.
domains:write
/domains/{id}
Obtiene un dominio y su token de verificación.
domains:read
/domains/{id}
Actualiza un dominio.
domains:write
/domains/{id}/verify
Comprueba ahora el TXT del dominio y devuelve el dominio. 429 si agotaste las comprobaciones a mano; 409 si lo desactivó el operador.
domains:write
/domains/{id}
Elimina un dominio. Devuelve 409 si aún conserva enlaces o si lo desactivó el operador.
domains:write
Proyectos 5 endpoints
El {id} es el id numérico.
/projects
Lista tus proyectos, paginado.
projects:read
/projects
Crea un proyecto.
projects:write
/projects/{id}
Obtiene un proyecto.
projects:read
/projects/{id}
Actualiza un proyecto.
projects:write
/projects/{id}
Elimina un proyecto.
projects:write
Píxeles, páginas de espera y overlays 15 endpoints
Catálogos de presentación. Un enlace los usa con splash_page_id, overlay_id y pixel_ids, en POST y PATCH /links. El {id} es el id numérico.
/pixels
Lista tus píxeles de seguimiento.
pixels:read
/pixels
Crea un píxel.
pixels:write
/pixels/{id}
Obtiene un píxel.
pixels:read
/pixels/{id}
Actualiza un píxel.
pixels:write
/pixels/{id}
Elimina un píxel.
pixels:write
/splash-pages
Lista tus páginas de espera.
splash:read
/splash-pages
Crea una página de espera.
splash:write
/splash-pages/{id}
Obtiene una página de espera.
splash:read
/splash-pages/{id}
Actualiza una página de espera.
splash:write
/splash-pages/{id}
Elimina una página de espera.
splash:write
/overlays
Lista tus overlays.
overlays:read
/overlays
Crea un overlay.
overlays:write
/overlays/{id}
Obtiene un overlay.
overlays:read
/overlays/{id}
Actualiza un overlay.
overlays:write
/overlays/{id}
Elimina un overlay.
overlays:write
Datos capturados 3 endpoints
Los envíos entran desde tus páginas bio, no por API. El {id} es el id numérico; link_id y block_id son los id (public_id) de la página y del bloque que los recogió. Salen los mismos que ves en el panel.
/data-submissions
Lista los envíos recibidos. Filtro opcional link_id: el id (public_id) de la página, el de GET /bio-pages.
data:read
/data-submissions/{id}
Obtiene un envío.
data:read
/data-submissions/{id}
Elimina un envío.
data:write
Qué se manda en cada petición
Los POST y PATCH reciben un objeto JSON (Content-Type: application/json). En un PATCH solo cambia lo que mandas. Un campo que no está en estas tablas se ignora: los de solo lectura que devuelve un GET, como id, short_url o clicks, no estorban si los devuelves en un PATCH.
POST
/links
Crear un enlace corto
Si ya tienes uno igual y en servicio, te devuelve ese (lee «Creación y reutilización»).
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| destination_url | URL (http o https) | obligatorio | A dónde lleva el enlace. |
| domain_id | número | opcional | El dominio de la dirección corta. 0, o no mandarlo, es el principal; si no, el id de un dominio tuyo activo o de uno compartido de tu plan (los da GET /domains?include=shared). |
| slug | texto | opcional | El alias, lo que va tras el dominio. Requiere un plan con alias propios; si no lo mandas, se genera uno. |
| password | texto | opcional | Contraseña para abrirlo, hasta 72 caracteres. Requiere un plan que la incluya. |
| status | active o paused | opcional | Por defecto active. |
| starts_at | fecha ISO 8601 | opcional | Desde cuándo funciona. Con zona horaria (2026-11-01T09:00:00-06:00); se guarda en UTC. |
| ends_at | fecha ISO 8601 | opcional | Hasta cuándo funciona. Tiene que ser posterior a starts_at. |
| click_limit | número | opcional | Tope de clics. |
| expiration_url | URL (http o https) | opcional | A dónde lleva cuando vence o llega al tope. |
| is_sensitive | booleano | opcional | Avisa de contenido sensible antes de abrir el destino. |
| http_status | 301, 302, 307 o 308 | opcional | El tipo de redirección. Por defecto 301. |
| method | direct, frame, splash, overlay | opcional | Cómo se abre: directo, dentro de un marco con barra, tras una página de espera o con un overlay encima. Por defecto direct. |
| splash_page_id | número | opcional | La página de espera (id de GET /splash-pages). Solo cuenta con method=splash; sin ella se usa la predeterminada. |
| overlay_id | número | opcional | El overlay (id de GET /overlays). Obligatorio con method=overlay; con otro método no se guarda. |
| pixel_ids | lista de números | opcional | Los píxeles que se ejecutan en sus páginas intermedias (ids de GET /pixels), hasta 50. Una lista vacía o null los quita. |
| project_id | número | opcional | El proyecto (id de GET /projects); null lo deja sin proyecto. |
| ads_enabled | booleano | opcional | Anuncios en las páginas intermedias. Si tu plan los quita, no salen aunque esté activo. |
| forward_query | booleano | opcional | Pasa al destino los parámetros que trae la visita. Por defecto true. |
| block_bots | booleano | opcional | Responde 403 a los bots detectados. Por defecto false. |
| social_breakout | booleano | opcional | Ayuda a abrir el destino fuera de las redes sociales. Por defecto false. |
| parameters | objeto | opcional | Hasta 20 pares nombre → valor que se añaden al destino (ver «Parámetros y apertura de enlaces»). |
| utm | objeto | opcional | Las cinco UTM de abajo. Se añaden al destino y ganan a cualquier otro parámetro con el mismo nombre. |
| utm.utm_source | texto | opcional | Fuente, hasta 128 caracteres. |
| utm.utm_medium | texto | opcional | Medio, hasta 128 caracteres. |
| utm.utm_campaign | texto | opcional | Campaña, hasta 128 caracteres. |
| utm.utm_term | texto | opcional | Término, hasta 128 caracteres. |
| utm.utm_content | texto | opcional | Contenido, hasta 128 caracteres. |
Ejemplo:
POST https://convo.chat/api/v1/links
{
"destination_url": "https://ejemplo.com/rebajas",
"slug": "rebajas-otono",
"utm": {
"utm_source": "instagram",
"utm_campaign": "otono"
},
"method": "splash",
"splash_page_id": 12,
"pixel_ids": [
3,
7
],
"project_id": 5
}
PATCH
/links/{id}
Cambiar un enlace corto
Los mismos campos que el alta, todos opcionales: solo cambia lo que mandas.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| destination_url | URL (http o https) | opcional | A dónde lleva el enlace. |
| domain_id | número | opcional | El dominio de la dirección corta. 0, o no mandarlo, es el principal; si no, el id de un dominio tuyo activo o de uno compartido de tu plan (los da GET /domains?include=shared). |
| slug | texto | no se puede cambiar | Se fija al crear. Si lo mandas, tiene que ser el que ya tiene; otro responde 422. |
| password | texto | opcional | Contraseña para abrirlo, hasta 72 caracteres. Requiere un plan que la incluya. |
| status | active o paused | opcional | Por defecto active. |
| starts_at | fecha ISO 8601 | opcional | Desde cuándo funciona. Con zona horaria (2026-11-01T09:00:00-06:00); se guarda en UTC. |
| ends_at | fecha ISO 8601 | opcional | Hasta cuándo funciona. Tiene que ser posterior a starts_at. |
| click_limit | número | opcional | Tope de clics. |
| expiration_url | URL (http o https) | opcional | A dónde lleva cuando vence o llega al tope. |
| is_sensitive | booleano | opcional | Avisa de contenido sensible antes de abrir el destino. |
| http_status | 301, 302, 307 o 308 | opcional | El tipo de redirección. Por defecto 301. |
| method | direct, frame, splash, overlay | opcional | Cómo se abre: directo, dentro de un marco con barra, tras una página de espera o con un overlay encima. Por defecto direct. |
| splash_page_id | número | opcional | La página de espera (id de GET /splash-pages). Solo cuenta con method=splash; sin ella se usa la predeterminada. |
| overlay_id | número | opcional | El overlay (id de GET /overlays). Obligatorio con method=overlay; con otro método no se guarda. |
| pixel_ids | lista de números | opcional | Los píxeles que se ejecutan en sus páginas intermedias (ids de GET /pixels), hasta 50. Una lista vacía o null los quita. |
| project_id | número | opcional | El proyecto (id de GET /projects); null lo deja sin proyecto. |
| ads_enabled | booleano | opcional | Anuncios en las páginas intermedias. Si tu plan los quita, no salen aunque esté activo. |
| forward_query | booleano | opcional | Pasa al destino los parámetros que trae la visita. Por defecto true. |
| block_bots | booleano | opcional | Responde 403 a los bots detectados. Por defecto false. |
| social_breakout | booleano | opcional | Ayuda a abrir el destino fuera de las redes sociales. Por defecto false. |
| parameters | objeto | opcional | Hasta 20 pares nombre → valor que se añaden al destino (ver «Parámetros y apertura de enlaces»). |
| utm | objeto | opcional | Las cinco UTM de abajo. Se añaden al destino y ganan a cualquier otro parámetro con el mismo nombre. |
| utm.utm_source | texto | opcional | Fuente, hasta 128 caracteres. |
| utm.utm_medium | texto | opcional | Medio, hasta 128 caracteres. |
| utm.utm_campaign | texto | opcional | Campaña, hasta 128 caracteres. |
| utm.utm_term | texto | opcional | Término, hasta 128 caracteres. |
| utm.utm_content | texto | opcional | Contenido, hasta 128 caracteres. |
| remove_password | booleano | opcional | Con true quita la contraseña. |
Ejemplo:
PATCH https://convo.chat/api/v1/links/{id}
{
"status": "paused",
"ends_at": "2026-12-31T23:59:00-06:00"
}
POST
/qr-codes
Crear un código QR
Qué va en content depende de content_type: lo dice «Tipos de contenido del QR».
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | obligatorio | Para reconocerlo en tu lista, hasta 64 caracteres. |
| content_type | texto | obligatorio | Qué lleva el código: uno de los tipos de la tabla de tipos. No se puede cambiar después. |
| content | objeto | opcional | Los campos del tipo elegido. Casi todos los tipos tienen alguno obligatorio. |
| design | objeto | opcional | Colores, forma y tamaño (ver «Diseño del QR»). Lo que no mandes toma su valor por defecto. |
| project_id | número | opcional | El proyecto (id de GET /projects). |
Ejemplo:
POST https://convo.chat/api/v1/qr-codes
{
"name": "Carta del restaurante",
"content_type": "url",
"content": {
"url": "https://ejemplo.com/carta"
},
"design": {
"foregroundColor": "#1F2937",
"size": 600
}
}
PATCH
/qr-codes/{id}
Cambiar un código QR
Solo cambia lo que mandas. content y design se mandan enteros: lo que devuelve GET /qr-codes/{id}, con tus cambios.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | opcional | Hasta 64 caracteres. |
| content | objeto | opcional | Sustituye el contenido entero. En un QR dinámico (url) cambia el destino sin reimprimir; en uno estático cambia el dibujo. |
| design | objeto | opcional | Sustituye el diseño entero. |
| project_id | número | opcional | El proyecto (id de GET /projects); null lo quita. |
| remove_logo | booleano | opcional | Con true quita el logotipo que se subió en el panel. |
Ejemplo:
PATCH https://convo.chat/api/v1/qr-codes/{id}
{
"content": {
"url": "https://ejemplo.com/carta-invierno"
}
}
POST
/bio-pages
Crear una página bio
Los bloques de la página se editan en el panel.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| slug | texto | opcional | La dirección de la página. Requiere un plan con alias propios; si no lo mandas, se genera. |
| domain_id | número | opcional | Como en los enlaces: 0 es el dominio principal. |
| password | texto | opcional | Contraseña para verla. Requiere un plan que la incluya. |
| status | active o paused | opcional | Por defecto active. |
| is_sensitive | booleano | opcional | Avisa de contenido sensible antes de mostrarla. |
| seo_noindex | booleano | opcional | Pide a los buscadores que no la indexen. |
| project_id | número | opcional | El proyecto (id de GET /projects). |
| theme_id | número | opcional | El tema, el que devuelve GET /bio-pages/{id}. Los temas se eligen en el panel. |
| display_branding | booleano | opcional | Con false no se muestra la marca de ConvoChat en la página. Solo vale si tu plan lo permite; si no, se muestra igual. |
| leap_url | URL (http o https) | opcional | Si la pones, la página lleva directamente a esta dirección en vez de mostrarse. |
| custom_css | texto | opcional | CSS propio para la página. |
| meta | objeto | opcional | El título y la descripción para buscadores y al compartir. |
| meta.title | texto | opcional | Hasta 200 caracteres. |
| meta.description | texto | opcional | Hasta 500 caracteres. |
| design | objeto | opcional | El fondo, la tipografía, la disposición y los colores (background, typography, layout, colors), con la misma forma que devuelve GET /bio-pages/{id}. |
Ejemplo:
POST https://convo.chat/api/v1/bio-pages
{
"slug": "estudio-norte",
"meta": {
"title": "Estudio Norte"
}
}
PATCH
/bio-pages/{id}
Cambiar una página bio
Los mismos campos que el alta, todos opcionales: solo cambia lo que mandas.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| slug | texto | no se puede cambiar | Se fija al crear. Si lo mandas, tiene que ser el que ya tiene; otro responde 422. |
| domain_id | número | opcional | Como en los enlaces: 0 es el dominio principal. |
| password | texto | opcional | Contraseña para verla. Requiere un plan que la incluya. |
| status | active o paused | opcional | Por defecto active. |
| is_sensitive | booleano | opcional | Avisa de contenido sensible antes de mostrarla. |
| seo_noindex | booleano | opcional | Pide a los buscadores que no la indexen. |
| project_id | número | opcional | El proyecto (id de GET /projects). |
| theme_id | número | opcional | El tema, el que devuelve GET /bio-pages/{id}. Los temas se eligen en el panel. |
| display_branding | booleano | opcional | Con false no se muestra la marca de ConvoChat en la página. Solo vale si tu plan lo permite; si no, se muestra igual. |
| leap_url | URL (http o https) | opcional | Si la pones, la página lleva directamente a esta dirección en vez de mostrarse. |
| custom_css | texto | opcional | CSS propio para la página. |
| meta | objeto | opcional | El título y la descripción para buscadores y al compartir. |
| meta.title | texto | opcional | Hasta 200 caracteres. |
| meta.description | texto | opcional | Hasta 500 caracteres. |
| design | objeto | opcional | El fondo, la tipografía, la disposición y los colores (background, typography, layout, colors), con la misma forma que devuelve GET /bio-pages/{id}. |
| remove_password | booleano | opcional | Con true quita la contraseña. |
Ejemplo:
PATCH https://convo.chat/api/v1/bio-pages/{id}
{
"status": "paused"
}
POST
/domains
Dar de alta un dominio
Queda pendiente hasta que publiques el registro TXT que devuelve y lo verifiques.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| host | texto | obligatorio | El dominio, sin protocolo ni ruta: links.tumarca.com. |
| default_link_id | texto | opcional | El enlace exclusivo: el id (public_id) de un enlace o página tuya, que se abre en la portada del dominio; null lo quita. Con él, root_redirect_url se guarda vacío. El id numérico de antes se sigue aceptando. |
| root_redirect_url | URL (http o https) | opcional | A dónde lleva la portada del dominio. No puede ser la propia portada (sí una ruta suya, como /mi-bio). |
| not_found_url | URL (http o https) | opcional | A dónde lleva una dirección del dominio que no existe. Tiene que ser de otro dominio. |
Ejemplo:
POST https://convo.chat/api/v1/domains
{
"host": "links.tumarca.com",
"not_found_url": "https://tumarca.com"
}
PATCH
/domains/{id}
Cambiar un dominio
Solo el enrutamiento: para usar otro dominio, bórralo y da de alta el nuevo.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| default_link_id | texto | opcional | El enlace exclusivo: el id (public_id) de un enlace o página tuya, que se abre en la portada del dominio; null lo quita. Con él, root_redirect_url se guarda vacío. El id numérico de antes se sigue aceptando. |
| root_redirect_url | URL (http o https) | opcional | A dónde lleva la portada del dominio. No puede ser la propia portada (sí una ruta suya, como /mi-bio). |
| not_found_url | URL (http o https) | opcional | A dónde lleva una dirección del dominio que no existe. Tiene que ser de otro dominio. |
Ejemplo:
PATCH https://convo.chat/api/v1/domains/{id}
{
"default_link_id": "01JA2B3C4D5E6F7G8H9J0K1M2N"
}
POST
/projects
Crear un proyecto
Un enlace entra en él con project_id.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | obligatorio | Hasta 255 caracteres, sin repetir el de otro proyecto tuyo. |
| color | color #RRGGBB | obligatorio | El color con el que se distingue en el panel. |
Ejemplo:
POST https://convo.chat/api/v1/projects
{
"name": "Campaña de otoño",
"color": "#FF5A36"
}
PATCH
/projects/{id}
Cambiar un proyecto
Solo cambia lo que mandas.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | opcional | Hasta 255 caracteres, sin repetir el de otro proyecto tuyo. |
| color | color #RRGGBB | opcional | El color con el que se distingue en el panel. |
Ejemplo:
PATCH https://convo.chat/api/v1/projects/{id}
{
"color": "#2563EB"
}
POST
/pixels
Crear un píxel
Un enlace lo usa con pixel_ids.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | obligatorio | Para reconocerlo, hasta 255 caracteres. |
| provider | facebook, google_analytics, gtm, google_ads, linkedin, x, pinterest, quora, tiktok, snapchat, reddit, bing, adroll | obligatorio | De quién es el píxel. |
| pixel_id | texto | obligatorio | El identificador que te da el proveedor, con el formato de ese proveedor. |
Ejemplo:
POST https://convo.chat/api/v1/pixels
{
"name": "Meta de la tienda",
"provider": "facebook",
"pixel_id": "123456789012345"
}
PATCH
/pixels/{id}
Cambiar un píxel
Solo cambia lo que mandas.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | opcional | Para reconocerlo, hasta 255 caracteres. |
| provider | facebook, google_analytics, gtm, google_ads, linkedin, x, pinterest, quora, tiktok, snapchat, reddit, bing, adroll | opcional | De quién es el píxel. |
| pixel_id | texto | opcional | El identificador que te da el proveedor, con el formato de ese proveedor. |
Ejemplo:
PATCH https://convo.chat/api/v1/pixels/{id}
{
"name": "Meta (tienda nueva)"
}
POST
/splash-pages
Crear una página de espera
Un enlace la usa con method=splash y splash_page_id.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | obligatorio | Para reconocerla, hasta 255 caracteres. |
| title | texto | obligatorio | El título que ve la visita. |
| content | texto | opcional | El texto, hasta 4000 caracteres. |
| unlock_seconds | número | obligatorio | Segundos hasta poder seguir, de 0 a 300. |
| auto_redirect | booleano | opcional | Seguir sola al acabar la cuenta atrás. Por defecto true. |
| design | objeto | opcional | Los tres colores de abajo. |
| design.background_color | color #RRGGBB | opcional | Fondo. |
| design.text_color | color #RRGGBB | opcional | Texto. |
| design.accent_color | color #RRGGBB | opcional | Botón y detalles. |
Ejemplo:
POST https://convo.chat/api/v1/splash-pages
{
"name": "Aviso de salida",
"title": "Vas a salir de nuestra web",
"unlock_seconds": 5
}
PATCH
/splash-pages/{id}
Cambiar una página de espera
Solo cambia lo que mandas; design se sustituye entero.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | opcional | Para reconocerla, hasta 255 caracteres. |
| title | texto | opcional | El título que ve la visita. |
| content | texto | opcional | El texto, hasta 4000 caracteres. |
| unlock_seconds | número | opcional | Segundos hasta poder seguir, de 0 a 300. |
| auto_redirect | booleano | opcional | Seguir sola al acabar la cuenta atrás. Por defecto true. |
| design | objeto | opcional | Los tres colores de abajo. |
| design.background_color | color #RRGGBB | opcional | Fondo. |
| design.text_color | color #RRGGBB | opcional | Texto. |
| design.accent_color | color #RRGGBB | opcional | Botón y detalles. |
Ejemplo:
PATCH https://convo.chat/api/v1/splash-pages/{id}
{
"unlock_seconds": 3
}
POST
/overlays
Crear un overlay
Un enlace lo usa con method=overlay y overlay_id.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | obligatorio | Para reconocerlo, hasta 255 caracteres. |
| type | contact, poll, message, newsletter, image, coupon | obligatorio | Qué muestra la barra sobre el destino. |
| config | objeto | opcional | Los textos de abajo. Sin < ni >. |
| config.headline | texto | opcional | Titular, hasta 160 caracteres. |
| config.body | texto | opcional | Texto, hasta 1000 caracteres. |
| config.cta_label | texto | opcional | Texto del botón, hasta 80 caracteres. |
| config.image_url | URL (http o https) | opcional | Imagen. |
| config.coupon_code | texto | opcional | Código de cupón: letras, números, espacios, _ . y -, hasta 64. |
Ejemplo:
POST https://convo.chat/api/v1/overlays
{
"name": "Cupón de bienvenida",
"type": "coupon",
"config": {
"headline": "10 % de descuento",
"coupon_code": "HOLA10"
}
}
PATCH
/overlays/{id}
Cambiar un overlay
Solo cambia lo que mandas; config se sustituye entero.
| Campo | Tipo | Hace falta | Para qué |
|---|---|---|---|
| name | texto | opcional | Para reconocerlo, hasta 255 caracteres. |
| type | contact, poll, message, newsletter, image, coupon | opcional | Qué muestra la barra sobre el destino. |
| config | objeto | opcional | Los textos de abajo. Sin < ni >. |
| config.headline | texto | opcional | Titular, hasta 160 caracteres. |
| config.body | texto | opcional | Texto, hasta 1000 caracteres. |
| config.cta_label | texto | opcional | Texto del botón, hasta 80 caracteres. |
| config.image_url | URL (http o https) | opcional | Imagen. |
| config.coupon_code | texto | opcional | Código de cupón: letras, números, espacios, _ . y -, hasta 64. |
Ejemplo:
PATCH https://convo.chat/api/v1/overlays/{id}
{
"config": {
"headline": "15 % de descuento",
"coupon_code": "HOLA15"
}
} Tipos de contenido del QR
content_type dice qué lleva el código y content trae sus campos. Solo url es dinámico: el dibujo lleva una dirección corta y el destino se cambia sin reimprimir. Los que van a una página tuya —bio, evento, archivo, tarjeta— se mandan con link_id, el id de esa página. El resto lleva su contenido dentro del dibujo.
| content_type | Campos de content |
|---|---|
| url URL dinámica |
|
| text Texto |
|
| vcard vCard |
|
| event Evento |
|
| file Archivo |
|
| bio Página bio |
|
| wifi Wi-Fi |
|
| phone Teléfono |
|
| sms SMS |
|
| email Correo electrónico |
|
| whatsapp WhatsApp |
|
| geo Ubicación |
|
| crypto Criptomoneda |
|
| paypal PayPal.Me |
|
| upi UPI |
|
| epc Transferencia SEPA |
|
| pix Pix |
|
| deep_link Abrir una app |
|
Diseño del QR
Las claves de design. Lo que no mandes al crear toma su valor por defecto. El color del código necesita un contraste de 4,5:1 con el fondo para que la cámara lo lea. El logotipo se sube desde el panel.
| Clave | Valores | Por defecto |
|---|---|---|
| foregroundColor | color #RRGGBB | #000000 |
| backgroundColor | color #RRGGBB | #FFFFFF |
| gradientEnabled | true o false | false |
| gradientColor | color #RRGGBB | #1D4ED8 |
| gradientType | horizontal, vertical, diagonal, inverse_diagonal, radial | diagonal |
| eyeStyle | square, rounded | square |
| frameEnabled | true o false | false |
| frameText | texto | ESCANÉAME |
| moduleStyle | square, rounded, dots | square |
| size | número de 128 a 2048 | 300 |
| errorCorrection | L, M, Q, H | M |
| logoSizePercent | número de 5 a 30 | 20 |
Parámetros y apertura de enlaces
POST /links y PATCH /links/{id} aceptan parameters, un objeto de hasta 20 pares de nombre y valor.
Los nombres admiten letras ASCII, números y los signos ._~-, hasta 128 caracteres; los valores son texto de hasta 2048 caracteres, sin caracteres de control.
Un valor vacío o null produce nombre=. No se aceptan objetos anidados ni los nombres reservados preview, signature, expires, password, gate, confirm.
La precedencia es: consulta del destino, parámetros entrantes si forward_query está activo, parameters guardados y, al final, los cinco valores utm configurados.
En un PATCH, omitir parameters conserva los valores; enviar null o un objeto vacío los elimina.
block_bots y social_breakout son booleanos y empiezan en false.
El primero responde 403 a bots detectados, incluidos los rastreadores de vistas previas sociales, antes de contar una visita. Las vistas previas firmadas del titular siguen disponibles.
El segundo añade ayuda para abrir el destino fuera de las redes sociales; con método directo muestra una página manual con los mismos metadatos para humanos y bots permitidos.
Android ofrece un intento de apertura en Chrome para destinos HTTPS; copiar el enlace y continuar al destino siguen disponibles. La app y el dispositivo deciden si permiten abrir otro navegador.
En un PATCH, omitir estos booleanos conserva su estado; enviar false los desactiva.
Creación y reutilización
POST /links no siempre crea. Si ya tienes un enlace corto igual al que pides y en servicio, la API te devuelve ese en lugar de crear otro: responde 200 (no 201), añade "meta": {"reused": true} y no gasta cupo de tu plan. Así, pedir dos veces el mismo enlace no te llena la lista de copias.
- Igual es que abre lo mismo: el mismo
destination_urlen el mismodomain_id, con las mismasutmyparameters, el mismohttp_statusymethod, la misma página de espera u overlay y los mismos interruptores. Y sin reglas de destino —por país, dispositivo, idioma o reparto entre destinos, las que se ponen en el panel—: un enlace con reglas no abre lo mismo para todos y nunca se reutiliza.project_idypixel_idscuentan si los mandas. - En servicio es activo, ya empezado, sin fecha de fin, sin tope de clics y sin contraseña. Uno pausado, vencido, agotado o programado no se reutiliza: se crea otro.
- Si tu petición trae
password,starts_at,ends_at,click_limito"status": "paused", pides un enlace propio y siempre se crea. - Si mandas
slug, cuenta: solo se reutiliza el enlace que ya tiene ese alias. Si tienes uno igual con otro alias, se crea uno nuevo con el tuyo; si el alias ya lo usa otro enlace que no se puede reutilizar, responde422. Sinslug, recibes el que ya tienes, con el suyo. Para tener dos enlaces al mismo destino, pide otro alias o cambia algo que se note al abrirlo, como unautm_sourcepor canal. - Si el igual que te devolvería lo deshabilitó el equipo de ConvoChat o está en revisión, no se hace una copia: responde
409 duplicate_destination_unavailable, condetails.idydetails.state. Uno retirado que no es igual —en otro dominio o con otrasutm— no impide crear.
HTTP/1.1 200 OK
{
"data": { "id": "...", "slug": "slug-original", "destination_url": "https://ejemplo.com" },
"meta": { "reused": true }
} Enlaces que respaldan un QR
Los enlaces cortos que un código QR dinámico usa por dentro no se exponen en esta API: no aparecen en GET /links y cualquier GET, PATCH, DELETE o GET /links/{id}/stats sobre ellos responde 404 resource_not_found, aunque sean tuyos. Gestiónalos por /qr-codes o desde el panel. Tampoco cuenta con ellos si comparas totales con lo que ves en /app.
Repetir una petición sin duplicar
Si una petición se corta y no sabes si llegó, repítela con la misma cabecera Idempotency-Key: no se hace dos veces. Vale en todos los POST. Usa una clave distinta por operación —un UUID, por ejemplo—, de hasta 255 caracteres visibles.
- La misma clave con la misma petición devuelve la respuesta de la primera vez, con la cabecera
Idempotent-Replayed: true, durante 24 horas. - La misma clave con otra petición —otra ruta u otros datos— responde
422 idempotency_key_reused. - Si la primera todavía no ha terminado,
409 idempotency_request_in_progress: espera un momento y repítela. - Solo se guarda una respuesta correcta (2xx). Si la primera falló, repetirla con la misma clave lo vuelve a intentar.
- Cada credencial tiene sus propias claves: la misma clave desde otra API key u otra app es otra petición.
curl -s https://convo.chat/api/v1/links \
-H "Authorization: Bearer TU_API_KEY" \
-H "Idempotency-Key: 6f1c2a9e-1b7d-4f3a-9c2e-8d5b7a1e4c90" \
-H "Content-Type: application/json" \
-d '{"destination_url": "https://ejemplo.com/rebajas"}' Límites
60 peticiones por minuto por cuenta, el mismo número para todos los planes de pago (el límite por plan todavía no está diferenciado). Es el techo de todas tus credenciales juntas —tus API keys y las apps que hayas autorizado—, así que crear más claves no da más cupo.
Si solo una credencial está usando la API, tiene el techo entero. Cuando hay varias a la vez, el minuto se reparte a partes iguales: cada una puede gastar como mucho el techo dividido entre las que están activas —con dos, 30 cada una; con tres, 20—. Así una integración que insiste se frena sola y no deja sin servicio a las demás. Cuenta como activa la credencial que hizo alguna petición en los últimos 60 segundos; la que deja de llamar libera su parte al cabo de ese tiempo. Si una credencial estaba sola y gastó el techo entero, la que llega después puede tener que esperar a que empiece el minuto siguiente; desde entonces cada una tiene su parte.
Cada respuesta trae X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset del límite que te frenará antes: el techo de la cuenta o, con varias credenciales activas, la parte de la tuya. Al pasarte recibes 429 rate_limit_exceeded con Retry-After, y details.scope dice cuál te frenó: account si fue el techo de la cuenta, credential si fue la parte de tu credencial, y entonces details.active_credentials dice entre cuántas se reparte.
Errores
Todo error llega con esta forma. error.code es estable: enrútate por él, no por el mensaje. details solo aparece cuando hay contexto.
{
"error": {
"code": "validation_failed",
"message": "Los datos proporcionados no son válidos.",
"details": { "destination_url": ["El campo destino es obligatorio."] }
}
} | HTTP | error.code | Cuándo |
|---|---|---|
| 400 | invalid_idempotency_key | La cabecera Idempotency-Key no es válida: entre 1 y 255 caracteres visibles, sin espacios. |
| 401 | unauthenticated | Falta el token, es inválido o fue revocado. |
| 403 | api_access_denied | Tu plan no incluye acceso a la API. |
| 403 | account_unavailable | Tu cuenta no está activa. |
| 403 | missing_ability | La API key no trae el scope que exige la ruta. details.required_abilities lo dice. |
| 403 | forbidden | No tienes permiso para esa acción. |
| 403 | plan_limit_exceeded | Alcanzaste un límite de tu plan. details.feature y details.current. |
| 403 | feature_not_available | Pediste una función que tu plan no incluye. details.feature y details.field. |
| 404 | resource_not_found | El recurso no existe, no es tuyo, o está oculto a la API. |
| 405 | method_not_allowed | Ese método HTTP no existe en la ruta. El mensaje y la cabecera Allow dicen cuáles admite. |
| 409 | domain_has_links | El dominio todavía conserva enlaces; muévelos antes de eliminarlo. |
| 409 | domain_disabled_by_operator | El dominio lo desactivó el equipo: no se puede verificar, reactivar ni eliminar. |
| 409 | duplicate_destination_unavailable | Ya tienes un enlace igual al que pides, y el equipo lo deshabilitó o lo está revisando: no se hace una copia. details.id y details.state dicen cuál y cómo está. |
| 409 | idempotency_request_in_progress | Otra petición con la misma Idempotency-Key no ha terminado todavía. Repítela en un momento. |
| 422 | validation_failed | Datos inválidos. details trae los errores campo por campo. |
| 422 | idempotency_key_reused | Esa Idempotency-Key ya se usó con otra petición (otra ruta u otros datos). |
| 429 | rate_limit_exceeded | Superaste un límite por minuto: el techo de tu cuenta o, con varias credenciales activas, la parte de la tuya. details.limit, details.retry_after y details.scope, que vale account o credential según cuál fue; con credential, details.active_credentials. |
| 429 | domain_verification_rate_limited | Demasiadas comprobaciones a mano del TXT de un dominio en una hora. details.retry_after, en segundos, y cabecera Retry-After. |
| 500 | server_error | Error interno. |
OAuth 2.0 para apps de terceros
Una API key sirve para tu propio código. Si construyes una app que actúa en nombre de otros usuarios de ConvoChat, registra una app OAuth en /app/oauth-apps y usa el flujo authorization_code con PKCE. Las redirect URIs deben ser https (http solo en 127.0.0.1, [::1] o localhost).
https://convo.chat/oauth/authorize
Pantalla de consentimiento.
https://convo.chat/oauth/token
Canje de código y refresh.
https://convo.chat/api/oauth/v1/user
Identidad del usuario que autorizó.
Con el access token llamas a los mismos 45 endpoints, con los mismos 19 scopes, cambiando la base a https://convo.chat/api/oauth/v1. Si no pides scopes, se conceden links:read y stats:read. Solo se acepta un bearer token real: una sesión de navegador no vale. El plan del usuario que autoriza es el que manda: si no incluye API, la llamada responde 403 api_access_denied.
-
PKCE es obligatorio para todas las apps: manda
code_challenge(el SHA-256 delcode_verifier, en base64url sin relleno) concode_challenge_method=S256;plainno se acepta. Sin eso, la persona vuelve a tu app conerror=invalid_request. Manda tambiénstatey compruébalo al volver. -
El token de acceso vale una hora (
expires_in= 3600) y el de renovación, 60 días; cada renovación devuelve uno nuevo de 60 días y anula el anterior. Un token caducado o retirado responde401 unauthenticatedconWWW-Authenticate: Bearer error="invalid_token": renuévalo. Si la renovación respondeinvalid_grant, la persona tiene que volver a autorizar tu app. -
Las apps de móvil, escritorio o navegador se registran sin secreto. En el mismo equipo usa
http://127.0.0.1:{puerto}ohttp://[::1]:{puerto}, y el puerto puede cambiar en cada arranque; conlocalhostel puerto tiene que ser exactamente el registrado. -
El
subdel token es interno y opaco: el identificador de la persona es elpublic_idde/api/oauth/v1/user. -
Un token sin el permiso de la ruta responde
403 missing_abilitycondetails.required_abilities, igual que una API key. - Quien conectó tu app puede quitarle el acceso cuando quiera en Cuenta → Apps conectadas. Si añades una dirección de vuelta, la siguiente autorización vuelve a pedir consentimiento.
Avisos por webhook
Un aviso con el canal Webhook (en el panel: Cuenta → Notificaciones) hace un POST con un cuerpo JSON a tu dirección cada vez que ocurre uno de los eventos que elegiste. La dirección tiene que ser HTTPS en el puerto 443 y de un servidor público; no seguimos redirecciones. Responde con cualquier 2xx en menos de 8 segundos; si tienes que hacer algo largo, guárdalo y hazlo después.
Cabeceras
| Cabecera | Qué lleva |
|---|---|
| X-ConvoChat-Signature | La firma: v1= seguido del HMAC-SHA256, en hexadecimal, de «marca de tiempo» + «.» + el cuerpo tal cual llega. |
| X-ConvoChat-Timestamp | Cuándo se firmó, en segundos desde 1970 (UTC). Cada reintento lleva una marca nueva. |
| X-ConvoChat-Event | El evento, el mismo que event en el cuerpo. |
| X-ConvoChat-Delivery | La entrega: el mismo que delivery_id en el cuerpo. Se repite en los reintentos de esa entrega. |
El cuerpo
Siempre el mismo sobre. id es el del evento: lo comparten todos los avisos que dispara ese evento. delivery_id es el de esta entrega y se repite si la reintentamos: deduplica por delivery_id. occurred_at va en UTC. data trae los datos del evento y sus claves dependen del tipo: puede ganar claves nuevas, así que ignora las que no conozcas.
{
"id": "prueba:01K0KSKAN07AW2YNH65B4Y9B2M",
"delivery_id": "01k0kskan07aw2ynh65b4y9b2m",
"event": "test",
"occurred_at": "2026-07-19T20:15:30.000000Z",
"subject": {
"type": "user",
"id": 17
},
"data": {
"title": "Notificación de prueba",
"message": "Tu aviso de ConvoChat está configurado correctamente."
}
}
Comprobar la firma
Con el secreto que pusiste en el aviso, calcula el HMAC-SHA256 de la marca de tiempo, un punto y el cuerpo sin tocar (los bytes que llegaron, no el JSON vuelto a escribir), y compáralo en tiempo constante con lo que va detrás de v1=. Rechaza los envíos cuya marca de tiempo tenga más de cinco minutos: así nadie puede repetirte uno viejo.
$marca = $_SERVER['HTTP_X_CONVOCHAT_TIMESTAMP'];
$firma = $_SERVER['HTTP_X_CONVOCHAT_SIGNATURE'];
$cuerpo = file_get_contents('php://input');
$esperada = 'v1=' . hash_hmac('sha256', $marca . '.' . $cuerpo, $secreto);
if (! hash_equals($esperada, $firma) || abs(time() - (int) $marca) > 300) {
http_response_code(401);
exit;
}
Para probar tu código: con el secreto un-secreto-de-ejemplo-de-al-menos-32-caracteres, la marca 1784492130 y este cuerpo, tal cual, en una sola línea:
{"id":"prueba:01K0KSKAN07AW2YNH65B4Y9B2M","delivery_id":"01k0kskan07aw2ynh65b4y9b2m","event":"test","occurred_at":"2026-07-19T20:15:30.000000Z","subject":{"type":"user","id":17},"data":{"title":"Notificación de prueba","message":"Tu aviso de ConvoChat está configurado correctamente."}} la cabecera de la firma tiene que ser:
X-ConvoChat-Signature: v1=9fe1ecd01a52485861deb2e71d8bdf2b19e605a9da83260951acb6eb635c77fa
Reintentos y pausas
-
Si no responde, tarda más de 8 segundos o contesta con un
5xx,408o429, lo intentamos 4 veces en total, esperando 15 s, 60 s, 300 s entre uno y otro. -
Cualquier otro
4xxno se reintenta: esperar no lo arregla. -
Un
3xxtampoco: no seguimos redirecciones, así que tu dirección volvería a redirigir. Pega en el aviso la dirección final, la que queda después de redirigir. - Tras 5 entregas seguidas que no llegan, pausamos el aviso y te lo decimos por correo y en el panel. En la pestaña «Entregas» del aviso ves cada envío y por qué falló.
Eventos
| event | Cuándo llega |
|---|---|
| new_click | Hito de clics |
| form_submission | Nueva respuesta de formulario o captura |
| form_limit_reached | Formulario al tope de respuestas |
| booking_created | Nueva cita reservada |
| booking_cancelled | Cita cancelada por quien reservó |
| payment | Pago recibido |
| affiliate_commission | Comisión de afiliado |
| ai_generation_done | Generación IA terminada |
| dunning_payment_failed | Pago de suscripción fallido |
| dunning_recovered | Pago de suscripción recuperado |
| dunning_grace_expired | Periodo de gracia vencido |
| subscription_canceled | Suscripción cancelada |
| subscription_expired | Suscripción expirada |
| social_post_failed | Publicación en redes que no salió |
| social_handoff | Conversación pasada a una persona |
| email_campaign_failed | Campaña de correo que no terminó de enviarse |
| test | Notificación de prueba (el botón «Enviar prueba») |
Cambios
Lo que cambia en la v1 y puede afectar a una integración que ya funciona se anota aquí, con su fecha.
link_idyblock_iddeGET /data-submissions, ydefault_link_iddeGET /domains, pasan de número a texto: ahora son el id público de la página, del bloque o del enlace, el mismo que da el resto de la API. Antes eran ids internos que no se podían cruzar con nada. Al mandarlos —el filtro?link_id,default_link_iden POST y PATCH /domains ycontent.link_idde un QR— vale el id público, y el número de antes se sigue aceptando.POST /linksya no te devuelve como reutilizado (200) un enlace que no está en servicio o que no es igual al que pides —otrasutm, otro alias, reglas de destino—: crea otro (201). Si el igual lo retiró el equipo de ConvoChat, responde409 duplicate_destination_unavailable. Lee «Creación y reutilización».- Con OAuth, un token sin el permiso de la ruta responde
403 missing_abilitycondetails.required_abilities, igual que una API key; antes era403 forbidden. - Con OAuth, el token de acceso vale una hora y el de renovación 60 días (antes, un año los dos), y PKCE con
S256es obligatorio para todas las apps, también las que tienen secreto. - El límite por minuto se reparte entre las credenciales que lo usan a la vez (lee «Límites»). Con una sola credencial no cambia nada.
PATCH /qr-codes/{id}cambia solo lo que mandas: antes, no mandardesignlo devolvía a sus valores por defecto y no mandarproject_idsacaba el QR de su proyecto.POSTyPATCH /linksaplicanproject_id,method,splash_page_id,overlay_idypixel_ids, que antes se descartaban sin avisar, yGETlos devuelve.
Nuevo ese día: la cabecera Idempotency-Key en todos los POST —si ya la mandabas, ahora se respeta, y una mal formada responde 400 invalid_idempotency_key—, GET /domains?include=shared, y content y design en los códigos QR.
Empieza a construir
Crea tu cuenta, activa un plan con acceso a la API y haz tu primera petición en minutos.
Crear cuentaRegistrarte es gratis. La API no entra en el plan Free: la activas desde /app/billing cuando quieras.