Qué es
La API de Dotly (https://www.dotly.com.py/api/v1) tiene dos familias que comparten autenticación, permisos, límites y auditoría:
- API pública: tu sistema usa Dotly (contactos, conversaciones, mensajes, plantillas, equipo, programados, estadísticas, Dot AI).
- API de integración: tu CRM, ERP o tienda mantiene sus datos en Dotly por su identificador (
external_id): contactos, productos, servicios, precios, stock, zonas y promociones.
Todo usa las mismas reglas que el panel de Dotly: la API nunca permite algo que la persona no podría hacer en Dotly.
Claves y permisos
Authorization: Bearer dtly_live_…. Las claves se crean en API & SDK → Aplicaciones, se muestran una sola vez y Dotly solo guarda su huella. Se pueden revocar al instante, rotar (la anterior sigue funcionando el tiempo que elijas), restringir a ciertas IPs y hacer vencer.
Cada clave actúa en nombre de una persona del equipo para contactos, conversaciones, mensajes, plantillas, equipo, estadísticas, Dot AI y la cuenta: sus permisos efectivos son los del rol de esa persona ∩ los permisos de la clave, y ve solo lo que esa persona ve (números asignados, conversaciones). Si la persona deja el equipo o la suspenden, esas operaciones se frenan. El catálogo, los eventos y los webhooks son de la organización. Solo se puede crear una clave a nombre propio o de alguien con un rol menor. Lo que se hace por la API queda en la auditoría a nombre de esa persona, marcado con la clave.
| Permiso | Qué permite | Actúa como una persona |
|---|---|---|
| catalog:read | Leer productos, servicios, categorías, zonas y promociones | No |
| catalog:write | Crear, modificar y eliminar el catálogo (incluye stock y sincronizaciones) | No |
| contacts:read | Ver contactos, etiquetas y el embudo | Sí |
| contacts:write | Crear, modificar y sincronizar contactos (etapa, valor y etiquetas) | Sí |
| contacts:delete | Eliminar contactos | Sí |
| conversations:read | Ver conversaciones y sus mensajes | Sí |
| conversations:write | Asignar, cerrar y reabrir conversaciones | Sí |
| messages:send | Enviar y programar mensajes de WhatsApp | Sí |
| templates:read | Ver plantillas y respuestas rápidas | Sí |
| team:read | Ver integrantes, equipos y números de WhatsApp | Sí |
| automations:read | Ver automatizaciones y sus ejecuciones | Sí |
| analytics:read | Ver estadísticas | Sí |
| ai:use | Usar Dot AI (sugerir, resumir, mejorar y traducir; consume créditos) | Sí |
| account:read | Ver el plan, su estado y los créditos de Dot AI | Sí |
| embed:sessions | Abrir Dotly dentro de tu sistema (sesiones embebidas cortas para tu equipo) | Sí |
| events:read | Leer el feed de eventos | No |
| webhooks:manage | Administrar destinos de webhooks | No |
Convenciones
- JSON en UTF-8. Las listas responden
{"object": "list", "data": [...], "has_more", "next_cursor"}: pedí la siguiente con?cursor=<next_cursor>(limithasta 200). El orden porupdated_atsirve para sincronizar de forma incremental conupdated_since. - Idempotencia: mandá
Idempotency-Keyen cada escritura (los SDK lo hacen solos). Repetirla devuelve la misma respuesta 24 h sin volver a ejecutar (ni enviar ni cobrar). Si la primera sigue en curso: 409idempotency_in_progress. - Diagnóstico: cada respuesta trae
X-Request-ID; dáselo a soporte. Los registros (sin contenido) se ven en API & SDK → Registros. - Límites: por clave, por organización y por persona; con claves inválidas, por IP. 429 con
Retry-After. Dot AI suma los límites de uso que fije tu organización para cada persona. - Versiones:
/api/v1. Los cambios compatibles (campos o endpoints nuevos) no cambian la versión: leé lo opcional sin asumir que no existe. Un cambio incompatible llegaría como/api/v2, conviviendo con v1. Los eventos traenschema_version. - Seguridad: solo https; nunca pongas la clave en el navegador ni en una app móvil. La organización siempre sale de la clave, nunca de lo que envíes.
Endpoints
Cuenta y clave
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET | / | — | La clave: integración, integrante en cuyo nombre actúa, permisos efectivos, familias de eventos y límites. |
| GET | /account | account:read | Plan, estado de la suscripción, funciones incluidas, créditos de Dot AI y límites de uso de la persona. |
Contactos y embudo
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET | /contacts | contacts:read | Lista con cursor. Filtros: search, status, stage, tag, phone, email, external_id, linked, updated_since; order=updated_at|-created_at. |
| POST | /contacts | contacts:write | Crea un contacto (phone obligatorio). 409 contact_exists si el teléfono ya está. |
| GET | /contacts/{id} | contacts:read | Un contacto. |
| PATCH | /contacts/{id} | contacts:write | Cambio parcial: solo lo enviado. La etapa sigue las reglas del embudo (ganado → cliente). |
| DELETE | /contacts/{id} | contacts:delete | Elimina el contacto y sus conversaciones. |
| GET | /contacts/{id}/conversations | conversations:read | Conversaciones del contacto. |
| GET | /tags | contacts:read | Etiquetas con su color (gray, green, teal, blue, purple, pink, red, orange, yellow). |
| POST | /tags | contacts:write | Crea una etiqueta con color (o devuelve la que ya existe con ese nombre). Viven en Dotly: WhatsApp no deja manejar sus etiquetas desde un dispositivo vinculado. |
Integración de contactos (tu CRM/ERP)
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| PUT | /contacts/external/{external_id} | contacts:write | Crea o actualiza por tu id (vincula por teléfono el chat que ya existía). |
| PATCH | /contacts/external/{external_id} | contacts:write | Solo actualiza (404 si no hay vínculo). |
| GET | /contacts/external/{external_id} | contacts:read | El contacto vinculado. |
| DELETE | /contacts/external/{external_id}?mode=unlink|delete | contacts:write / contacts:delete | Quita el vínculo (por defecto) o elimina el contacto. |
| POST | /contacts/batch | contacts:write | Hasta 100 operaciones upsert, unlink o delete, con resultado por operación. |
Conversaciones y mensajes
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET | /conversations | conversations:read | Lista con cursor (chats y grupos juntos). Filtros: status, assigned (me|none|id), whatsapp_account_id, contact_id, tag_id, kind (person|group), updated_since. |
| GET | /conversations/{id} | conversations:read | Una conversación. |
| PATCH | /conversations/{id} | conversations:write | Cambia status (open, pending, closed) o assigned_member_id. |
| GET | /conversations/{id}/messages | conversations:read | Mensajes con cursor (más nuevos primero; order=created_at para orden cronológico; since). |
| POST | /conversations/{id}/messages | messages:send | Envía texto (template_id y reply_to_message_id opcionales). Asíncrono: vuelve pending. |
| POST | /messages | messages:send | Escribe a un número (to): abre el chat si no existía. |
| GET | /messages/{id} | conversations:read | Un mensaje (con su estado de envío y los plazos edit_until y delete_until). |
| PATCH | /messages/{id} | messages:send | Edita en WhatsApp un texto enviado por Dotly, hasta edit_until (15 minutos). |
| DELETE | /messages/{id} | messages:send | Lo elimina para todos en WhatsApp, hasta delete_until (2 días y 12 horas). Emite message.deleted. |
| POST | /messages/{id}/reaction | messages:send | Reacciona con un emoji (emoji vacío la quita). |
| PUT | /conversations/{id}/labels | contacts:write + conversations:read | Etiquetas del chat (persona o grupo) por nombre; las que no existen se crean. |
Grupos de WhatsApp
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| POST | /groups/join/preview | conversations:write | Qué grupo es un enlace de invitación (nombre, integrantes, descripción y si ya sos parte). |
| POST | /groups/join | conversations:write | El número se une con el enlace de invitación; devuelve la conversación del grupo. |
| POST | /groups/import | conversations:write | Trae todos los grupos del número (los que ya estaban no cambian). |
| POST | /conversations/{id}/group/reset | conversations:write | Repara un grupo con mensajes en «Esperando el mensaje». |
Plantillas, equipo, programados y estadísticas
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET | /templates | templates:read | Plantillas (search, category, active). |
| GET | /templates/{id} | templates:read | Una plantilla, con sus opciones (hasta 10) y el submenú que abre cada una (next_template_id). |
| GET | /templates/{id}/flow | templates:read | El flujo de menús desde esa plantilla: cada opción, qué hace y el submenú que abre, sin límite de niveles. Enviar la plantilla (template_id) arranca el flujo; el cliente avanza eligiendo y puede escribir «0» (volver), «menú» (inicio) o «salir». La conversación trae el menú pendiente en menu. |
| POST | /templates/{id}/render | templates:read | Texto con las variables completas para un contacto (no envía). |
| GET | /members | team:read | Integrantes del equipo. |
| GET | /teams | team:read | Equipos. |
| GET | /whatsapp-accounts | team:read | Números de WhatsApp y su conexión. |
| GET | /scheduled-messages | conversations:read | Programados (view=upcoming|history). |
| POST | /scheduled-messages | messages:send | Programa un mensaje a uno o varios chats (mode=ai: lo escribe Dot AI al enviar). |
| DELETE | /scheduled-messages/{id} | messages:send | Cancela un programado. |
| GET | /automations | automations:read | Automatizaciones. |
| GET | /automations/runs | automations:read | Últimas ejecuciones. |
| GET | /stats?days=7|30|90 | analytics:read | Resumen de atención y ventas. |
Dot AI
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| POST | /ai/suggest | ai:use | Respuesta sugerida para una conversación (draft y avoid opcionales). |
| POST | /ai/summarize | ai:use | Resumen con intención, urgencia, tono, etapa sugerida y próximo paso. |
| POST | /ai/rewrite | ai:use | Mejora la redacción (style: mejorar, formal, cercano, breve). |
| POST | /ai/translate | ai:use | Traduce un texto (target: es, en, pt, gn...). |
| GET | /ai/usage | ai:use | Disponibilidad y límites de uso de la persona. |
Eventos y webhooks
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET | /events | events:read | Feed de eventos (after, types, limit): solo las familias que la clave puede leer. |
| GET/POST | /webhooks | webhooks:manage | Destinos: listar y crear (devuelve el secreto una sola vez). |
| GET/PATCH/DELETE | /webhooks/{id} | webhooks:manage | Ver, cambiar (url, events, is_active) o eliminar. |
| POST | /webhooks/{id}/test | webhooks:manage | Envía un ping firmado. |
| POST | /webhooks/{id}/rotate-secret | webhooks:manage | Secreto nuevo. |
Dotly embebido
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| POST | /embed/sessions | embed:sessions | Sesión corta para mostrar Dotly en tu sistema: devuelve una URL de un solo uso. |
| GET | /embed/sessions | embed:sessions | Sesiones de la aplicación. |
| GET/DELETE | /embed/sessions/{id} | embed:sessions | Ver o cerrar una sesión al instante. |
Catálogo (ver la guía del catálogo más abajo)
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET/PUT/PATCH/DELETE | /items/{ref} · /categories/{ref} · /zones/{ref} · /discounts/{ref} | catalog:read / catalog:write | Productos y servicios, categorías, zonas de envío y promociones por external_id. |
| POST | /inventory · /batch · /sync-runs | catalog:write | Stock, lotes y sincronizaciones completas. |
Especificación completa: https://www.dotly.com.py/api/v1/openapi.json (OpenAPI 3.1).
Integración de contactos
PUT /contacts/external/{external_id} crea o actualiza el contacto que en tu sistema tiene ese id:
- Si ya está vinculado, lo actualiza. Si no, busca por teléfono: el cliente que ya escribió por WhatsApp se vincula, sin duplicarse. Si no existe, lo crea.
- Quién manda en cada campo: lo que enviás se aplica; lo que no enviás queda como está en Dotly (la etapa o las etiquetas que cambió tu equipo no se pisan).
tagsreemplaza las etiquetas;custom_fieldsse combina por clave (null borra esa clave; requiere el plan con campos personalizados);notesyassigned_member_idsolo si los mandás. - El teléfono es la identidad de WhatsApp: si el contacto ya tiene conversaciones, uno distinto no se aplica (
warnings: ["phone_not_updated"]). - Cambios fuera de orden: mandá
source_updated_at(fecha de modificación en tu sistema); uno más viejo que el aplicado se descarta (result: "stale"). - Un contacto vinculado a otra aplicación no se re-vincula (409
linked_to_other_integration). Los lotes (POST /contacts/batch) aplican cada operación por separado y devuelven el resultado de cada una.
Las sincronizaciones completas del catálogo nunca tocan contactos. Para traer los cambios hechos en Dotly, usá los eventos contact.* (feed o webhooks), con origin.source para descartar los tuyos.
Eventos y webhooks
Cada cambio genera un evento en la misma transacción que el cambio (si el cambio no se confirma, no hay evento). Se leen por el feed (GET /events?after=<sequence>) o llegan por webhook.
| Familia | Eventos | Permiso | Además |
|---|---|---|---|
| Catálogo | catalog.item.created, catalog.item.updated, catalog.item.deleted, catalog.category.created, catalog.category.updated, catalog.category.deleted, catalog.zone.created, catalog.zone.updated, catalog.zone.deleted, catalog.discount.created, catalog.discount.updated, catalog.discount.deleted | catalog:read | |
| Sincronizaciones | sync.completed, sync.failed | catalog:read | |
| Contactos | contact.created, contact.updated, contact.stage_changed, contact.deleted | contacts:read | La persona de la clave (o quien crea el destino) tiene que ver todos los contactos. |
| Conversaciones | conversation.created, conversation.updated | conversations:read | La persona de la clave (o quien crea el destino) tiene que ver todas las conversaciones. La aplicación que originó el cambio también lo recibe. |
| Mensajes | message.received, message.sent, message.status_updated, message.failed, message.edited, message.deleted | conversations:read | La persona de la clave (o quien crea el destino) tiene que ver todas las conversaciones. La aplicación que originó el cambio también lo recibe. |
| Dot AI | ai.request.completed, ai.request.failed | account:read | La persona de la clave (o quien crea el destino) tiene que ver la facturación. La aplicación que originó el cambio también lo recibe. |
| Automatizaciones | automation.executed, automation.failed | automations:read | La persona de la clave (o quien crea el destino) tiene que ver las automatizaciones. La aplicación que originó el cambio también lo recibe. |
| Suscripción | subscription.updated, subscription.suspended | account:read | La persona de la clave (o quien crea el destino) tiene que ver la facturación. La aplicación que originó el cambio también lo recibe. |
- Firma:
Dotly-Signature: t=<unix>,v1=<hex>= HMAC-SHA256(secreto,"<t>." + cuerpo crudo). Verificá con el cuerpo crudo, en tiempo constante, y rechazá marcas de tiempo de más de 5 minutos. - Encabezados:
Dotly-Event-Id,Dotly-Event-Type,Dotly-Delivery-Id,Dotly-Delivery-Attempt. - Entrega "al menos una vez": guardá el id del evento y descartá los repetidos. Reintentos con espera creciente (60, 300, 1800, 7200, 21600, 43200, 86400, 86400 segundos); después la entrega queda sin entregar y se puede reenviar desde API & SDK → Webhooks. Un destino con 50 fallas seguidas se pausa solo.
- Contenido: los mensajes traen su texto en
data.text; Dotly lo guarda cifrado mientras espera la entrega. Los eventos de Dot AI no traen contenido (solo tipo, resultado y costo). - Las URL de destino tienen que ser https y públicas (Dotly rechaza redes privadas, localhost, metadatos de la nube y redirecciones hacia ellas).
Python (SDK)
from dotly.webhooks import construct_event, WebhookVerificationError
@app.post("/webhooks/dotly")
def dotly_webhook():
try:
event = construct_event(request.get_data(), request.headers.get("Dotly-Signature"), SECRET)
except WebhookVerificationError:
return "", 400
if ya_procesado(event["id"]): # la entrega es "al menos una vez"
return "", 200
procesar(event) # rápido: lo pesado, en tu cola
return "", 200
Python (sin SDK)
import hashlib, hmac, time
def verificar(cuerpo_crudo: bytes, encabezado: str, secreto: str) -> bool:
partes = dict(p.split("=", 1) for p in encabezado.split(","))
t = int(partes["t"])
if abs(time.time() - t) > 300: # repeticiones viejas: afuera
return False
esperado = hmac.new(secreto.encode(), f"{t}.".encode() + cuerpo_crudo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, partes["v1"])
Node.js
import { constructEvent } from "dotly-sdk/webhooks";
app.post("/webhooks/dotly", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await constructEvent(req.body, req.get("Dotly-Signature"), process.env.DOTLY_WEBHOOK_SECRET);
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});
Dot AI
Antes de gastar, Dotly verifica en el servidor que la persona de la clave tenga permiso de Dot AI, que la suscripción esté activa, que el plan lo incluya, los límites de uso de esa persona y la frecuencia. Después aparta el costo máximo estimado del uso de forma atómica: dos usos simultáneos no pueden gastar el mismo saldo, y si no alcanza se rechaza antes de llamar a la IA (402 ai_credits_exhausted). Al terminar cobra el costo real y devuelve el resto. La API nunca recibe ni calcula saldos, precios ni planes.
Reintentá con la misma Idempotency-Key: un uso que ya terminó devuelve la misma respuesta sin cobrarse otra vez; uno que falló (503 ai_unavailable) no se cobró y se puede reintentar. Cada uso queda registrado con su canal (API, embebido o panel), la clave y el costo, sin guardar contenido.
Dotly embebido
- En API & SDK → la aplicación → Dotly dentro de tu sistema autorizá tus dominios (https) y los módulos (conversaciones, contactos, embudo, programados, plantillas, automatizaciones, catálogo, estadísticas).
- Tu servidor pide una sesión con una clave que tenga
embed:sessions:origin(uno de tus dominios),modules, la persona (member_idomember_email; la de la clave o alguien con un rol menor),start,themeyttl_minutes(5 a 480; 60 por defecto). Recibe una URL de un solo uso que vence en 2 minutos. - Tu página la abre en un iframe (o con
mount()dedotly-sdk/embed).
# Tu servidor (la clave nunca va al navegador)
session = client.embed_sessions.create(
origin="https://crm.tuempresa.com",
modules=["inbox", "contacts"],
member_email="ana@tuempresa.com",
start={"module": "inbox", "conversation_id": "…"},
theme={"accent": "#ff6600", "radius": 8, "font": "system"},
)
return {"url": session["url"]} # un solo uso, vence en 2 minutos
import { mount } from "dotly-sdk/embed";
const { url } = await fetch("/mi-backend/dotly-session").then((r) => r.json());
const dotly = mount(document.querySelector("#dotly"), {
url,
onEvent: (e) => { if (e.type === "dotly:session.expired") pedirOtraSesion(); },
});
dotly.navigate("inbox", { conversation_id: "…" });
- Permisos: los de la persona recortados a los módulos pedidos. Personalizar o elegir módulos nunca da más permisos que en Dotly. Configuración, equipo, facturación, cerrar sesión y cambiar de organización no están disponibles.
- Dominio: cada pantalla solo se deja mostrar en el dominio de la sesión (
frame-ancestors). El token va en el fragmento de la URL (no queda en registros ni en el Referer). - Sesión: cookie propia
SameSite=None; Secure; Partitioned(guardada aparte por sitio anfitrión; no depende de cookies de terceros ni se mezcla con la sesión de Dotly de la misma persona). Vence sola, se cierra conDELETE /embed/sessions/{id}y deja de valer al revocar la clave, pausar la aplicación, quitar el dominio, suspender a la persona o detenerse la suscripción. Si el navegador bloquea incluso esa cookie, llegadotly:errorconcookies_blocked. - Tema:
accent,accent_strong,background,surface,text,muted,border(#RRGGBB),radius(0–24),font(system, dotly, serif, mono),density(comfortable, compact),modeyhide(navigation, header). - Mensajes con tu página (
postMessage, solo con tu dominio): Dotly envíadotly:ready,dotly:navigated,dotly:resize,dotly:session.expired,dotly:error; aceptadotly:navigate({module, conversation_id, contact_id}) ydotly:ping. Nunca envía datos de clientes.
SDK de Python
pip install dotly
from dotly import Dotly
client = Dotly() # DOTLY_API_KEY
client.contacts.create(phone="595981123456", name="Juan Pérez", tags=["vip"])
for conversation in client.conversations.iter(status="open"):
print(conversation["contact"]["name"])
client.integration.contacts.upsert("crm-1042", {"name": "Juan Pérez", "phone": "595981123456"})
suggestion = client.ai.suggest_reply(conversation_id)
Clientes Dotly y AsyncDotly (asyncio) con los mismos métodos; recursos para todo lo de esta página (client.contacts, client.integration, client.ai, client.embed_sessions...); excepciones específicas con request_id; reintentos seguros con Idempotency-Key automática; dotly.webhooks.construct_event. Python 3.9+.
SDK de JavaScript / TypeScript
npm install dotly-sdk
import { Dotly } from "dotly-sdk";
const dotly = new Dotly(); // DOTLY_API_KEY (solo en el servidor)
await dotly.contacts.create({ phone: "595981123456", name: "Juan Pérez" });
for await (const c of dotly.conversations.iter({ status: "open" })) console.log(c.contact?.name);
dotly-sdk es para servidores (Node.js 18+, Deno, Bun) y se niega a correr en el navegador; dotly-sdk/webhooks verifica firmas (WebCrypto); dotly-sdk/embed monta Dotly en tu página con la URL de sesión, sin claves.
Errores
Siempre {"error": {"code", "message", "details"}} con el código HTTP que corresponde; nunca detalles internos.
| HTTP | Código | Qué significa |
|---|---|---|
| 401 | invalid_api_key | Falta la clave, no es válida, venció o fue revocada. |
| 403 | insufficient_scope | La clave no tiene el permiso (details.required_scope). |
| 403 | key_member_inactive | La persona en cuyo nombre actúa la clave ya no está activa. |
| 403 | ip_not_allowed | La clave no acepta solicitudes desde esa IP. |
| 403 | subscription_inactive · plan_required · feature_not_available | La suscripción o el plan no lo permiten. |
| 402 | ai_credits_exhausted | Los créditos de Dot AI disponibles no alcanzan (se rechaza antes de gastar). |
| 404 | not_found | No existe o la persona de la clave no lo ve. |
| 409 | idempotency_in_progress | Otra solicitud con la misma Idempotency-Key sigue en curso: reintentá en segundos. |
| 409 | contact_exists · linked_to_other_integration · phone_linked_to_other_record | Conflictos de contactos. |
| 422 | validation_error · idempotency_key_reused · invalid_cursor | Datos no válidos (details.field). |
| 429 | rate_limited · ai_member_limit | Límite de solicitudes (Retry-After) o límite de uso de Dot AI de la persona. |
| 503 | ai_unavailable | Dot AI no pudo responder: reintentá con la misma Idempotency-Key. |