Dotly API & SDK Desarrolladores OpenAPI

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.

PermisoQué permiteActúa como una persona
catalog:readLeer productos, servicios, categorías, zonas y promocionesNo
catalog:writeCrear, modificar y eliminar el catálogo (incluye stock y sincronizaciones)No
contacts:readVer contactos, etiquetas y el embudoSí
contacts:writeCrear, modificar y sincronizar contactos (etapa, valor y etiquetas)Sí
contacts:deleteEliminar contactosSí
conversations:readVer conversaciones y sus mensajesSí
conversations:writeAsignar, cerrar y reabrir conversacionesSí
messages:sendEnviar y programar mensajes de WhatsAppSí
templates:readVer plantillas y respuestas rápidasSí
team:readVer integrantes, equipos y números de WhatsAppSí
automations:readVer automatizaciones y sus ejecucionesSí
analytics:readVer estadísticasSí
ai:useUsar Dot AI (sugerir, resumir, mejorar y traducir; consume créditos)Sí
account:readVer el plan, su estado y los créditos de Dot AISí
embed:sessionsAbrir Dotly dentro de tu sistema (sesiones embebidas cortas para tu equipo)Sí
events:readLeer el feed de eventosNo
webhooks:manageAdministrar destinos de webhooksNo

Convenciones

  • JSON en UTF-8. Las listas responden {"object": "list", "data": [...], "has_more", "next_cursor"}: pedí la siguiente con ?cursor=<next_cursor> (limit hasta 200). El orden por updated_at sirve para sincronizar de forma incremental con updated_since.
  • Idempotencia: mandá Idempotency-Key en 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: 409 idempotency_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 traen schema_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étodoRutaPermisoQué hace
GET/—La clave: integración, integrante en cuyo nombre actúa, permisos efectivos, familias de eventos y límites.
GET/accountaccount:readPlan, estado de la suscripción, funciones incluidas, créditos de Dot AI y límites de uso de la persona.

Contactos y embudo

MétodoRutaPermisoQué hace
GET/contactscontacts:readLista con cursor. Filtros: search, status, stage, tag, phone, email, external_id, linked, updated_since; order=updated_at|-created_at.
POST/contactscontacts:writeCrea un contacto (phone obligatorio). 409 contact_exists si el teléfono ya está.
GET/contacts/{id}contacts:readUn contacto.
PATCH/contacts/{id}contacts:writeCambio parcial: solo lo enviado. La etapa sigue las reglas del embudo (ganado → cliente).
DELETE/contacts/{id}contacts:deleteElimina el contacto y sus conversaciones.
GET/contacts/{id}/conversationsconversations:readConversaciones del contacto.
GET/tagscontacts:readEtiquetas con su color (gray, green, teal, blue, purple, pink, red, orange, yellow).
POST/tagscontacts:writeCrea 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étodoRutaPermisoQué hace
PUT/contacts/external/{external_id}contacts:writeCrea o actualiza por tu id (vincula por teléfono el chat que ya existía).
PATCH/contacts/external/{external_id}contacts:writeSolo actualiza (404 si no hay vínculo).
GET/contacts/external/{external_id}contacts:readEl contacto vinculado.
DELETE/contacts/external/{external_id}?mode=unlink|deletecontacts:write / contacts:deleteQuita el vínculo (por defecto) o elimina el contacto.
POST/contacts/batchcontacts:writeHasta 100 operaciones upsert, unlink o delete, con resultado por operación.

Conversaciones y mensajes

MétodoRutaPermisoQué hace
GET/conversationsconversations:readLista 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:readUna conversación.
PATCH/conversations/{id}conversations:writeCambia status (open, pending, closed) o assigned_member_id.
GET/conversations/{id}/messagesconversations:readMensajes con cursor (más nuevos primero; order=created_at para orden cronológico; since).
POST/conversations/{id}/messagesmessages:sendEnvía texto (template_id y reply_to_message_id opcionales). Asíncrono: vuelve pending.
POST/messagesmessages:sendEscribe a un número (to): abre el chat si no existía.
GET/messages/{id}conversations:readUn mensaje (con su estado de envío y los plazos edit_until y delete_until).
PATCH/messages/{id}messages:sendEdita en WhatsApp un texto enviado por Dotly, hasta edit_until (15 minutos).
DELETE/messages/{id}messages:sendLo elimina para todos en WhatsApp, hasta delete_until (2 días y 12 horas). Emite message.deleted.
POST/messages/{id}/reactionmessages:sendReacciona con un emoji (emoji vacío la quita).
PUT/conversations/{id}/labelscontacts:write + conversations:readEtiquetas del chat (persona o grupo) por nombre; las que no existen se crean.

Grupos de WhatsApp

MétodoRutaPermisoQué hace
POST/groups/join/previewconversations:writeQué grupo es un enlace de invitación (nombre, integrantes, descripción y si ya sos parte).
POST/groups/joinconversations:writeEl número se une con el enlace de invitación; devuelve la conversación del grupo.
POST/groups/importconversations:writeTrae todos los grupos del número (los que ya estaban no cambian).
POST/conversations/{id}/group/resetconversations:writeRepara un grupo con mensajes en «Esperando el mensaje».

Plantillas, equipo, programados y estadísticas

MétodoRutaPermisoQué hace
GET/templatestemplates:readPlantillas (search, category, active).
GET/templates/{id}templates:readUna plantilla, con sus opciones (hasta 10) y el submenú que abre cada una (next_template_id).
GET/templates/{id}/flowtemplates:readEl 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}/rendertemplates:readTexto con las variables completas para un contacto (no envía).
GET/membersteam:readIntegrantes del equipo.
GET/teamsteam:readEquipos.
GET/whatsapp-accountsteam:readNúmeros de WhatsApp y su conexión.
GET/scheduled-messagesconversations:readProgramados (view=upcoming|history).
POST/scheduled-messagesmessages:sendPrograma un mensaje a uno o varios chats (mode=ai: lo escribe Dot AI al enviar).
DELETE/scheduled-messages/{id}messages:sendCancela un programado.
GET/automationsautomations:readAutomatizaciones.
GET/automations/runsautomations:readÚltimas ejecuciones.
GET/stats?days=7|30|90analytics:readResumen de atención y ventas.

Dot AI

MétodoRutaPermisoQué hace
POST/ai/suggestai:useRespuesta sugerida para una conversación (draft y avoid opcionales).
POST/ai/summarizeai:useResumen con intención, urgencia, tono, etapa sugerida y próximo paso.
POST/ai/rewriteai:useMejora la redacción (style: mejorar, formal, cercano, breve).
POST/ai/translateai:useTraduce un texto (target: es, en, pt, gn...).
GET/ai/usageai:useDisponibilidad y límites de uso de la persona.

Eventos y webhooks

MétodoRutaPermisoQué hace
GET/eventsevents:readFeed de eventos (after, types, limit): solo las familias que la clave puede leer.
GET/POST/webhookswebhooks:manageDestinos: listar y crear (devuelve el secreto una sola vez).
GET/PATCH/DELETE/webhooks/{id}webhooks:manageVer, cambiar (url, events, is_active) o eliminar.
POST/webhooks/{id}/testwebhooks:manageEnvía un ping firmado.
POST/webhooks/{id}/rotate-secretwebhooks:manageSecreto nuevo.

Dotly embebido

MétodoRutaPermisoQué hace
POST/embed/sessionsembed:sessionsSesión corta para mostrar Dotly en tu sistema: devuelve una URL de un solo uso.
GET/embed/sessionsembed:sessionsSesiones de la aplicación.
GET/DELETE/embed/sessions/{id}embed:sessionsVer o cerrar una sesión al instante.

Catálogo (ver la guía del catálogo más abajo)

MétodoRutaPermisoQué hace
GET/PUT/PATCH/DELETE/items/{ref} · /categories/{ref} · /zones/{ref} · /discounts/{ref}catalog:read / catalog:writeProductos y servicios, categorías, zonas de envío y promociones por external_id.
POST/inventory · /batch · /sync-runscatalog:writeStock, 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:

  1. 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.
  2. 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). tags reemplaza las etiquetas; custom_fields se combina por clave (null borra esa clave; requiere el plan con campos personalizados); notes y assigned_member_id solo si los mandás.
  3. El teléfono es la identidad de WhatsApp: si el contacto ya tiene conversaciones, uno distinto no se aplica (warnings: ["phone_not_updated"]).
  4. 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").
  5. 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.

FamiliaEventosPermisoAdemás
Catálogocatalog.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.deletedcatalog:read
Sincronizacionessync.completed, sync.failedcatalog:read
Contactoscontact.created, contact.updated, contact.stage_changed, contact.deletedcontacts:read La persona de la clave (o quien crea el destino) tiene que ver todos los contactos.
Conversacionesconversation.created, conversation.updatedconversations: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.
Mensajesmessage.received, message.sent, message.status_updated, message.failed, message.edited, message.deletedconversations: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 AIai.request.completed, ai.request.failedaccount: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.
Automatizacionesautomation.executed, automation.failedautomations: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ónsubscription.updated, subscription.suspendedaccount: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

  1. 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).
  2. Tu servidor pide una sesión con una clave que tenga embed:sessions: origin (uno de tus dominios), modules, la persona (member_id o member_email; la de la clave o alguien con un rol menor), start, theme y ttl_minutes (5 a 480; 60 por defecto). Recibe una URL de un solo uso que vence en 2 minutos.
  3. Tu página la abre en un iframe (o con mount() de dotly-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 con DELETE /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, llega dotly:error con cookies_blocked.
  • Tema: accent, accent_strong, background, surface, text, muted, border (#RRGGBB), radius (0–24), font (system, dotly, serif, mono), density (comfortable, compact), mode y hide (navigation, header).
  • Mensajes con tu página (postMessage, solo con tu dominio): Dotly envía dotly:ready, dotly:navigated, dotly:resize, dotly:session.expired, dotly:error; acepta dotly:navigate ({module, conversation_id, contact_id}) y dotly: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.

HTTPCódigoQué significa
401invalid_api_keyFalta la clave, no es válida, venció o fue revocada.
403insufficient_scopeLa clave no tiene el permiso (details.required_scope).
403key_member_inactiveLa persona en cuyo nombre actúa la clave ya no está activa.
403ip_not_allowedLa clave no acepta solicitudes desde esa IP.
403subscription_inactive · plan_required · feature_not_availableLa suscripción o el plan no lo permiten.
402ai_credits_exhaustedLos créditos de Dot AI disponibles no alcanzan (se rechaza antes de gastar).
404not_foundNo existe o la persona de la clave no lo ve.
409idempotency_in_progressOtra solicitud con la misma Idempotency-Key sigue en curso: reintentá en segundos.
409contact_exists · linked_to_other_integration · phone_linked_to_other_recordConflictos de contactos.
422validation_error · idempotency_key_reused · invalid_cursorDatos no válidos (details.field).
429rate_limited · ai_member_limitLímite de solicitudes (Retry-After) o límite de uso de Dot AI de la persona.
503ai_unavailableDot AI no pudo responder: reintentá con la misma Idempotency-Key.

Catálogo: guía completa

Qué es el conector

El conector mantiene el catálogo de Dotly (la sección Catálogo, donde lo ves y lo editás) igual al de tu sistema: CRM, ERP, tienda online o planilla. Trae productos y servicios, variantes, categorías, precios en varias monedas, precios por cantidad o mayoristas, costo, stock total y por sucursal, preventa y encargos, fotos, marca, código de barras, peso y medidas, impuestos, adicionales, combos, suscripciones, disponibilidad y modalidad de servicios, zonas de envío y retiro, y promociones. Dot AI responde a tus clientes con esos datos, así que lo que cambia en tu sistema cambia en las respuestas.

Hay tres formas de conectar un sistema; las tres usan las mismas reglas de validación y deduplicación:

  • API propia: tu sistema envía los cambios a la API REST (este documento). Sirve para cualquier lenguaje o tecnología.
  • Integraciones listas: Shopify, Odoo y WooCommerce. Dotly consulta tu sistema solo y trae lo que cambió, sin programar nada.
  • Feed: un CSV, Excel, JSON o XML en una URL (una planilla de Google Sheets publicada, la exportación programada de tu ERP o el feed de Google Merchant o Meta que ya genera tu tienda).
  • Archivo: exportás el catálogo de tu sistema y lo subís en Dotly, sin programar ni publicar nada.

En el otro sentido, Dotly avisa los cambios hechos en su web con webhooks firmados o con el feed de eventos.

Inicio rápido

  1. En Dotly, Conector > Conectar un sistema > API propia, y creá una clave con los permisos que necesites.
  2. Guardá la clave como secreto en tu servidor (por ejemplo, la variable de entorno DOTLY_API_KEY).
  3. Probá la conexión y enviá tu primer producto con los ejemplos de abajo.

URL base: https://www.dotly.com.py/api/v1 · Especificación OpenAPI 3.1: https://www.dotly.com.py/api/v1/openapi.json (importala en Postman, Insomnia o un generador de clientes).

Traer lo que ya tenés cargado

El conector no arranca vacío: al conectar, primero se trae todo lo que ya existe en tu sistema y después se mantiene al día con cada cambio.

  • Shopify, Odoo y WooCommerce: la primera sincronización es completa (todos los productos con sus variantes, sin límite de páginas; en Odoo y WooCommerce también las categorías con su jerarquía; en WooCommerce, las zonas de envío y los cupones; en Odoo, los métodos de envío). Las siguientes traen solo lo que cambió y una vez por día se repite la completa.
  • Archivo o feed: CSV (coma, punto y coma o tabulación), Excel (.xlsx), JSON o XML, hasta 10.000 productos. Sirven, tal como salen, las exportaciones de Shopify, WooCommerce, Tiendanube y los feeds de Google Merchant y Meta (que generan VTEX, PrestaShop, Magento, Wix y muchas otras tiendas). Las filas que comparten handle, item_group_id, parent o «Identificador de URL» son variantes del mismo producto.
  • API propia: recorré tu sistema una vez con una sincronización completa; los lotes de hasta 500 operaciones la hacen rápida.

Lo que ya cargaste a mano en Dotly con el mismo código (SKU) se vincula en vez de duplicarse. Columnas que reconoce la importación (sin importar mayúsculas, acentos ni el idioma):

GrupoColumnas
Identificaciónid, sku o código, handle, item_group_id o parent (agrupa variantes), código de barras (gtin, ean, upc), mpn
Datosnombre o title, descripción (también HTML), categoría ("Ropa > Remeras" toma la última), marca o vendor, estado (nuevo, usado), etiquetas, enlace, imagen e imágenes adicionales
Preciosprecio ("450.000", "12.500,50" o "450000.00 PYG"), precio promocional o sale_price, precio de comparación, costo, moneda, precio mayorista y cantidad mayorista, IVA (10, 5, exento)
Stockstock o cantidad, disponibilidad (in stock, out of stock, preorder, backorder), fecha de disponibilidad, permite pedidos pendientes, cantidad mínima y máxima, múltiplo de venta, días de entrega
Envíopeso (kg, g, lb u oz), largo, ancho y alto (cm), envío gratis, requiere envío, digital o descargable
Variantesvariante, sku y precio y stock de la variante, Option1/2/3 Name y Value, Attribute 1 name y value, Nombre y Valor de propiedad 1, color, talle o size, material
Otrostipo (producto o servicio), duración en minutos, unidad, activo o publicado, destacado, combo, solo adultos, garantía, notas para Dot AI

Suscripción y conservación de datos

La API, las sincronizaciones automáticas, los avisos entrantes y los webhooks funcionan solo con la suscripción al día. Sin plan, con el pago pendiente, vencida o en pausa, todo queda detenido (la API responde 403 subscription_inactive) pero no se borra nada: al renovar, sigue donde quedó.

  • Días 0 a 30 desde el vencimiento sin pagar: período de gracia, todo se conserva.
  • Días 30 a 60: se conserva igual y se avisa por correo al dueño de la cuenta (al empezar y una semana antes).
  • Día 60: se eliminan los datos del conector (integraciones con sus credenciales, claves, webhooks y registros de sincronizaciones y eventos). Tu catálogo no se borra: queda en Dotly como carga manual y, si volvés a conectar el mismo sistema, se vincula de nuevo sin duplicarse.

Nunca se elimina nada con una suscripción activa o en pausa, ni si renovás dentro del plazo: renovar mueve el vencimiento al final del período nuevo.

Autenticación y permisos

Cada solicitud lleva Authorization: Bearer dtly_live_…. Las claves pertenecen a una integración, se muestran una sola vez, pueden vencer y se revocan al instante desde Dotly. La API es para servidores: no tiene CORS y la clave nunca debe ir en una página web o una app.

PermisoQué permite
catalog:readLeer productos, servicios, categorías, zonas y promociones
catalog:writeCrear, modificar y eliminar el catálogo (incluye stock y sincronizaciones)
contacts:readVer contactos, etiquetas y el embudo
contacts:writeCrear, modificar y sincronizar contactos (etapa, valor y etiquetas)
contacts:deleteEliminar contactos
conversations:readVer conversaciones y sus mensajes
conversations:writeAsignar, cerrar y reabrir conversaciones
messages:sendEnviar y programar mensajes de WhatsApp
templates:readVer plantillas y respuestas rápidas
team:readVer integrantes, equipos y números de WhatsApp
automations:readVer automatizaciones y sus ejecuciones
analytics:readVer estadísticas
ai:useUsar Dot AI (sugerir, resumir, mejorar y traducir; consume créditos)
account:readVer el plan, su estado y los créditos de Dot AI
embed:sessionsAbrir Dotly dentro de tu sistema (sesiones embebidas cortas para tu equipo)
events:readLeer el feed de eventos
webhooks:manageAdministrar destinos de webhooks

Convenciones

  • Identificadores: usás los de tu sistema (external_id, hasta 120 caracteres; codificalo en la URL). Para algo creado en Dotly, id:<uuid>.
  • Crear o actualizar: PUT crea o reemplaza (upsert); PATCH cambia solo los campos enviados. Repetir el mismo envío devuelve unchanged y no genera eventos.
  • Deduplicación: en la primera sincronización, un producto cargado a mano con el mismo sku se vincula a tu external_id (matched_by: "sku") en vez de duplicarse; las zonas sin identificador se vinculan por nombre.
  • Montos: en unidades de la moneda (189000 o 12500.50). Monedas: PYG, USD, BRL, ARS, EUR, UYU, CLP, BOB, PEN, COP, MXN, GBP, CAD, CHF, JPY, CNY. En guaraníes, sin decimales.
  • Impuestos: iva_10 (IVA 10 %), iva_5 (IVA 5 %), exento (Exento de IVA); los precios llevan el IVA incluido.
  • Fechas: ISO 8601 (2026-10-17T15:30:00Z).
  • Orden de llegada: si enviás source_updated_at (la fecha de modificación en tu sistema), un cambio más viejo que el guardado se ignora (ignored_stale). Así los envíos fuera de orden no pisan datos nuevos.
  • Concurrencia: las respuestas traen ETag: W/"<versión>". Con If-Match la escritura solo se aplica si nadie cambió el recurso (si no, 412).
  • Reintentos seguros: con Idempotency-Key, repetir una escritura (por ejemplo, tras un corte de red) devuelve la misma respuesta durante 24 h, sin aplicarla dos veces.
  • Paginación: los listados devuelven next_cursor; pedí la siguiente página con ?cursor=. Con updated_since traés solo lo modificado.
  • Notas para Dot AI: ai_notes es de Dotly: un PUT que no lo incluye no lo borra, así lo que el negocio le explica a Dot AI no se pierde en cada sincronización.

Probar la conexión

Con la clave de tu integración, GET /api/v1 devuelve la organización, los permisos de la clave y los límites.

import os
import requests

API = "https://www.dotly.com.py/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOTLY_API_KEY']}"}

info = requests.get(API, headers=HEADERS, timeout=30)
info.raise_for_status()
print(info.json()["organization"]["name"], info.json()["key"]["scopes"])

Crear o actualizar un producto con variantes

PUT /items/{external_id} crea o reemplaza. Repetir el mismo envío no cambia nada (unchanged) ni genera eventos. Si un producto cargado a mano en Dotly tiene el mismo SKU, se vincula en vez de duplicarse (matched_by: sku).

import os
import requests
from urllib.parse import quote

API = "https://www.dotly.com.py/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOTLY_API_KEY']}"}

producto = {
    "name": "Zapatilla Runner Pro",
    "category": "Calzado",
    "sku": "RUN-PRO",
    "currency": "PYG",
    "price": 450000,
    "promo_price": 399000,
    "tax": "iva_10",
    "attributes": {"Marca": "Mburucuyá", "Material": "Malla"},
    "variants": [
        {"external_id": "RUN-PRO-40", "sku": "RUN-PRO-40", "options": {"Talle": "40"}, "stock": 5},
        {"external_id": "RUN-PRO-41", "sku": "RUN-PRO-41", "options": {"Talle": "41"}, "stock": 0},
    ],
    "source_updated_at": "2026-10-17T15:30:00Z",
}
# PUT crea o reemplaza; el identificador es el de TU sistema (external_id).
res = requests.put(f"{API}/items/{quote('erp-1001', safe='')}", json=producto, headers=HEADERS, timeout=30)
res.raise_for_status()
print(res.json()["result"], res.json()["data"]["version"])  # created / updated / unchanged

Actualizar stock en lote

Hasta 500 filas por solicitud. Con Idempotency-Key, un reintento por corte de red no suma dos veces.

import os
import requests

API = "https://www.dotly.com.py/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOTLY_API_KEY']}"}

cambios = {"updates": [
    {"item": "erp-1001", "variant": "RUN-PRO-40", "quantity": 3},   # fijar
    {"item": "erp-1001", "variant": "RUN-PRO-41", "delta": 10},     # sumar (o restar con negativo)
]}
res = requests.post(f"{API}/inventory", json=cambios, headers={**HEADERS, "Idempotency-Key": "stock-2026-10-17-1530"}, timeout=30)
res.raise_for_status()
for fila in res.json()["data"]:
    print(fila["external_id"], fila.get("result") or fila["error"]["message"])

Sincronización completa (altas, cambios y bajas)

Para sistemas que no avisan cuando se borra algo: abrí una corrida, mandá todo el catálogo y completala. Lo que tu integración había traído y no apareció se desactiva (o se elimina, o se conserva, según missing).

import os
import requests

API = "https://www.dotly.com.py/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOTLY_API_KEY']}"}

def catalogo_del_erp():
    """Tus productos, con el formato de la API. Acá, un ejemplo fijo."""
    yield {"external_id": "erp-1001", "name": "Zapatilla Runner Pro", "price": 450000, "sku": "RUN-PRO"}
    yield {"external_id": "erp-1002", "name": "Medias deportivas", "price": 35000, "track_stock": True, "stock": 40}

# 1. Abrir la sincronización completa
inicio = requests.post(f"{API}/sync-runs", headers=HEADERS, timeout=30)
inicio.raise_for_status()
run = inicio.json()["data"]

# 2. Enviar todo el catálogo en lotes de hasta 500, marcando la corrida
lote = []
def enviar(lote):
    ops = [{"op": "upsert", "type": "item", "external_id": p.pop("external_id"), "data": p} for p in lote]
    res = requests.post(f"{API}/batch", json={"operations": ops}, timeout=60,
                        headers={**HEADERS, "Dotly-Sync-Run": run["id"]})
    res.raise_for_status()
    for fila in res.json()["data"]:
        if "error" in fila:
            print("Error en", fila["external_id"], fila["error"]["message"])

for producto in catalogo_del_erp():
    lote.append(producto)
    if len(lote) == 500:
        enviar(lote)
        lote = []
if lote:
    enviar(lote)

# 3. Completar: lo que tu sistema ya no tiene se desactiva en Dotly
fin = requests.post(f"{API}/sync-runs/{run['id']}/complete", json={"missing": "deactivate"}, headers=HEADERS, timeout=60)
fin.raise_for_status()
print(fin.json()["data"]["stats"])

Traer los cambios hechos en Dotly (feed de eventos)

Para sincronizar en el otro sentido sin exponer un servidor: consultá el feed con el cursor after.

import os
import time
import requests

API = "https://www.dotly.com.py/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOTLY_API_KEY']}"}
ultimo = 0  # guardalo en tu base: es el cursor

while True:
    res = requests.get(f"{API}/events", params={"after": ultimo, "types": "catalog.item.*"}, headers=HEADERS, timeout=30)
    res.raise_for_status()
    pagina = res.json()
    for evento in pagina["data"]:
        if evento["origin"]["source"] == "dotly":   # cambios hechos en la web de Dotly
            print(evento["type"], evento["resource"]["external_id"], evento["changes"])
    ultimo = pagina["next_after"]
    if not pagina["has_more"]:
        time.sleep(30)

Recibir webhooks y verificar la firma

Dotly firma cada envío con HMAC-SHA256. Respondé 2xx en menos de 10 s; si no, reintenta durante unos 3 días.

# Flask: recibir webhooks de Dotly y verificar la firma
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["DOTLY_WEBHOOK_SECRET"]  # whsec_...
procesados = set()  # usá tu base: los eventos pueden llegar más de una vez

def firma_valida(header: str, cuerpo: bytes) -> bool:
    partes = dict(p.split("=", 1) for p in header.split(","))
    t = int(partes["t"])
    if abs(time.time() - t) > 300:
        return False
    esperado = hmac.new(SECRET.encode(), f"{t}.".encode() + cuerpo, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, partes["v1"])

@app.post("/webhooks/dotly")
def recibir():
    if not firma_valida(request.headers.get("Dotly-Signature", ""), request.get_data()):
        abort(400)
    evento = request.get_json()
    if evento["id"] not in procesados:
        procesados.add(evento["id"])
        print(evento["type"], evento["data"].get("external_id"))
    return "", 204

Endpoints

MétodoRutaDescripciónPermiso
GET/api/v1/Información de la clave, la integración y los límites
GET/api/v1/itemsListar productos y servicioscatalog:read
GET/api/v1/items/{ref}Obtener unocatalog:read
PUT/api/v1/items/{ref}Crear o reemplazar (upsert por external_id)catalog:write
PATCH/api/v1/items/{ref}Cambiar solo los campos enviadoscatalog:write
DELETE/api/v1/items/{ref}Eliminarcatalog:write
GET/api/v1/categoriesListar categoríascatalog:read
GET/api/v1/categories/{ref}Obtener unocatalog:read
PUT/api/v1/categories/{ref}Crear o reemplazar (upsert por external_id)catalog:write
PATCH/api/v1/categories/{ref}Cambiar solo los campos enviadoscatalog:write
DELETE/api/v1/categories/{ref}Eliminarcatalog:write
GET/api/v1/zonesListar zonas de envíocatalog:read
GET/api/v1/zones/{ref}Obtener unocatalog:read
PUT/api/v1/zones/{ref}Crear o reemplazar (upsert por external_id)catalog:write
PATCH/api/v1/zones/{ref}Cambiar solo los campos enviadoscatalog:write
DELETE/api/v1/zones/{ref}Eliminarcatalog:write
GET/api/v1/discountsListar promocionescatalog:read
GET/api/v1/discounts/{ref}Obtener unocatalog:read
PUT/api/v1/discounts/{ref}Crear o reemplazar (upsert por external_id)catalog:write
PATCH/api/v1/discounts/{ref}Cambiar solo los campos enviadoscatalog:write
DELETE/api/v1/discounts/{ref}Eliminarcatalog:write
POST/api/v1/inventoryFijar o ajustar stock en lote (hasta 500)catalog:write
POST/api/v1/batchHasta 500 operaciones (upsert, patch, delete, stock) en una solicitudcatalog:write
POST/api/v1/sync-runsIniciar una sincronización completacatalog:write
GET/api/v1/sync-runs/{run_id}Estado de una corridacatalog:read
POST/api/v1/sync-runs/{run_id}/completeCompletar: lo que no se envió se desactiva o se eliminacatalog:write
POST/api/v1/sync-runs/{run_id}/cancelCancelar (no borra nada)catalog:write
GET/api/v1/eventsFeed de cambios (consulta periódica, alternativa a los webhooks)events:read
GET/api/v1/webhooksListar destinoswebhooks:manage
POST/api/v1/webhooksCrear destino (devuelve el secreto de firma una sola vez)webhooks:manage
GET/api/v1/webhooks/{webhook_id}Obtenerwebhooks:manage
PATCH/api/v1/webhooks/{webhook_id}Cambiar url, events, description o is_activewebhooks:manage
DELETE/api/v1/webhooks/{webhook_id}Eliminarwebhooks:manage
POST/api/v1/webhooks/{webhook_id}/testEnviar un evento ping de pruebawebhooks:manage
POST/api/v1/webhooks/{webhook_id}/rotate-secretGenerar un secreto de firma nuevowebhooks:manage
POST/api/v1/hooks/{token}Aviso de cambios desde tu sistema (Shopify, Odoo, cron): Dotly sincroniza enseguida
GET/api/v1/accountPlan, suscripción, funciones y créditos de Dot AIaccount:read
GET/api/v1/contactsListar contactos (cursor)contacts:read
POST/api/v1/contactsCrear contactocontacts:write
GET/api/v1/contacts/{contact_id}Obtener contactocontacts:read
PATCH/api/v1/contacts/{contact_id}Cambiar solo lo enviadocontacts:write
DELETE/api/v1/contacts/{contact_id}Eliminar contacto (y sus conversaciones)contacts:delete
GET/api/v1/contacts/{contact_id}/conversationsConversaciones del contactoconversations:read
GET/api/v1/tagsEtiquetas (con color)contacts:read
POST/api/v1/tagsCrear etiqueta con color (o la existente con ese nombre)contacts:write
GET/api/v1/contacts/external/{external_id}Contacto vinculado a tu idcontacts:read
PUT/api/v1/contacts/external/{external_id}Crear o actualizar por tu id (vincula por teléfono)contacts:write
PATCH/api/v1/contacts/external/{external_id}Solo actualizar (404 si no hay vínculo)contacts:write
DELETE/api/v1/contacts/external/{external_id}Quitar el vínculo (mode=unlink) o eliminar (mode=delete)contacts:write
POST/api/v1/contacts/batchHasta 100 operaciones con resultado por operacióncontacts:write
GET/api/v1/conversationsListar conversaciones (cursor)conversations:read
GET/api/v1/conversations/{conversation_id}Obtener conversaciónconversations:read
PATCH/api/v1/conversations/{conversation_id}Cambiar estado o asignaciónconversations:write
GET/api/v1/conversations/{conversation_id}/messagesMensajes de la conversación (cursor)conversations:read
POST/api/v1/conversations/{conversation_id}/messagesEnviar texto (asíncrono: vuelve pending)messages:send
POST/api/v1/messagesEscribir a un número (abre el chat si hace falta)messages:send
GET/api/v1/messages/{message_id}Obtener mensajeconversations:read
PATCH/api/v1/messages/{message_id}Editar en WhatsApp un texto enviado por Dotly (hasta edit_until)messages:send
DELETE/api/v1/messages/{message_id}Eliminar para todos en WhatsApp (hasta delete_until)messages:send
POST/api/v1/messages/{message_id}/reactionReaccionar con un emoji (vacío la quita)messages:send
PUT/api/v1/conversations/{conversation_id}/labelsEtiquetas del chat (persona o grupo): reemplaza la lista por nombrecontacts:write
POST/api/v1/groups/join/previewQué grupo es un enlace de invitaciónconversations:write
POST/api/v1/groups/joinUnirse con el enlace de invitaciónconversations:write
POST/api/v1/groups/importTraer todos los grupos del númeroconversations:write
POST/api/v1/conversations/{conversation_id}/group/resetReparar mensajes en "Esperando el mensaje"conversations:write
GET/api/v1/templatesListar plantillastemplates:read
GET/api/v1/templates/{template_id}Obtener plantillatemplates:read
GET/api/v1/templates/{template_id}/flowFlujo de menús desde esta plantilla (cada opción y el submenú que abre)templates:read
POST/api/v1/templates/{template_id}/renderTexto con variables para un contacto (no envía)templates:read
GET/api/v1/membersIntegrantesteam:read
GET/api/v1/teamsEquiposteam:read
GET/api/v1/whatsapp-accountsNúmeros de WhatsAppteam:read
GET/api/v1/scheduled-messagesMensajes programadosconversations:read
POST/api/v1/scheduled-messagesProgramar (texto o mode=ai)messages:send
DELETE/api/v1/scheduled-messages/{scheduled_id}Cancelarmessages:send
GET/api/v1/automationsAutomatizacionesautomations:read
GET/api/v1/automations/runsÚltimas ejecucionesautomations:read
GET/api/v1/statsResumen de atención y ventasanalytics:read
POST/api/v1/ai/suggestRespuesta sugeridaai:use
POST/api/v1/ai/summarizeResumen de la conversaciónai:use
POST/api/v1/ai/rewriteMejorar la redacciónai:use
POST/api/v1/ai/translateTraducirai:use
GET/api/v1/ai/usageDisponibilidad y límites de uso de la personaai:use
GET/api/v1/embed/sessionsSesiones de la aplicaciónembed:sessions
POST/api/v1/embed/sessionsSesión corta para mostrar Dotly en tu sistema (URL de un solo uso)embed:sessions
GET/api/v1/embed/sessions/{session_id}Obtener sesiónembed:sessions
DELETE/api/v1/embed/sessions/{session_id}Cerrar sesión al instanteembed:sessions

Esquemas JSON

Los campos de solo lectura vuelven en las respuestas y se ignoran si los reenviás. Un campo desconocido devuelve un error con su nombre (así un error de tipeo no pasa inadvertido).

Producto o servicio (Item)

CampoTipoDescripción
object solo lectura
id solo lecturastring
external_id solo lecturastring/null
kindstring · product, service
namestring
descriptionstring/null
categorystring/nullNombre de la categoría (se crea si no existe).
category_external_idstring/nullAlternativa: categoría por su external_id (tiene que existir).
skustring/nullÚnico por organización. En la primera sincronización, un producto cargado a mano con el mismo SKU se vincula en vez de duplicarse.
currencystring · PYG, USD, BRL, ARS, EUR, UYU, CLP, BOB, PEN, COP, MXN, GBP, CAD, CHF, JPY, CNY
pricenumber/string/nullMonto en unidades de la moneda (189000, 12500.50 o "12500.50"). null = a consultar.
promo_pricenumber/string/nullPrecio promocional (menor que price).
promo_starts_atstring/nullDesde cuándo vale el precio promocional (vacío = ya).
promo_ends_atstring/nullHasta cuándo vale; después se usa el precio normal.
unitstring/null
taxstring/null · iva_10, iva_5, exentoIVA de Paraguay (precios con IVA incluido).
track_stockboolean
stockinteger/nullCon variantes que controlan stock, es la suma de ellas (solo lectura).
low_stock_atinteger/null
stock_state solo lecturastring · unlimited, in_stock, low, out
duration_minutesinteger/nullServicios: duración del turno.
availabilityobject/nullDías y horarios en que se ofrece un servicio (0 = lunes ... 6 = domingo).
delivery_availableboolean
attributesobjectCaracterísticas que Dot AI puede contar (marca, modelo, año...).
ai_notesstring/nullLo que Dot AI debe saber. Es de Dotly: un PUT que no lo incluye no lo borra.
metadataobjectDatos libres de tu sistema (hasta 8 KB). Dot AI no los lee.
is_activeboolean
product_typestring/null · physical, digital, bundle, subscription, gift_card, rentalProductos: físico, digital o licencia, combo o kit, suscripción, tarjeta de regalo o alquiler.
barcodestring/nullGTIN, EAN, UPC o ISBN.
brandstring/null
conditionstring/null · new, used, refurbished
urlstring/nullPágina del producto en tu tienda.
imagesarrayFotos (https). Una URL o {url, alt}. Dotly no las descarga.
tagsarray
featuredbooleanDestacado: Dot AI lo recomienda primero.
sort_orderinteger
cost_pricenumber/string/nullCosto (nunca lo ve el cliente ni Dot AI).
price_maxnumber/string/nullRango de precios: de price a price_max.
price_frombooleanEl precio es «desde» (varía según lo que se pida).
tax_ratenumber/nullImpuesto en % para otros países (con tax de Paraguay se completa solo).
tax_includedbooleanSi el precio incluye el impuesto.
price_tiersarrayPrecios por cantidad o por lista de precios (mayorista).
other_pricesarrayEl mismo producto en otras monedas.
stock_policystring · deny, backorder, preorder, made_to_orderSin stock: no se vende (deny), se puede encargar (backorder), preventa o se fabrica a pedido.
restock_atstring/nullLlegada de la preventa o reposición.
lead_time_daysinteger/nullDías de entrega o fabricación.
min_order_qtyinteger/null
max_order_qtyinteger/null
order_multipleinteger/nullSe vende de a N (cajas, packs).
locationsarrayStock por sucursal o depósito. Sin `stock` explícito, el total es la suma.
shippingobjectEnvío del producto.
option_groupsarrayAdicionales y opciones con precio (extras, guarniciones, grabado).
bundle_itemsarrayQué incluye un combo o kit.
subscriptionobjectSuscripciones o membresías.
service_detailsobjectServicios: modalidad, cupos, anticipación, sesiones, lugar y profesionales.
warrantystring/null
min_ageinteger/nullEdad mínima (p. ej. 18 para bebidas alcohólicas).
variantsarrayEn PUT reemplaza la lista completa (alta, cambio y baja).
source_updated_atstring/nullFecha de modificación en tu sistema: un cambio más viejo que el guardado se ignora.
source solo lecturaobject/null
version solo lecturainteger
synced_at solo lecturastring/null
created_at solo lecturastring/null
updated_at solo lecturastring/null

Variante

CampoTipoDescripción
id solo lecturastring
external_idstring/null
skustring/null
namestringPor defecto, los valores de options unidos con « / ».
optionsobject
pricenumber/string/nullnull = el precio del producto.
promo_pricenumber/string/nullMonto en unidades de la moneda (189000, 12500.50 o "12500.50"). null = a consultar.
track_stockboolean
stockinteger/null
stock_state solo lecturastring · unlimited, in_stock, out
is_activeboolean
barcodestring/nullGTIN, EAN, UPC o ISBN.
imagestring/nullFoto de la variante (https).
weight_ginteger/nullPeso en gramos.
cost_pricenumber/string/nullCosto (nunca lo ve el cliente ni Dot AI).
locationsarrayStock por sucursal o depósito. Sin `stock` explícito, el total es la suma.

Categoría

CampoTipoDescripción
object solo lectura
id solo lecturastring
external_id solo lecturastring/null
namestring
descriptionstring/null
parent_external_idstring/null
sort_orderinteger
is_activeboolean
source_updated_atstring/null
version solo lecturainteger

Zona de envío

CampoTipoDescripción
object solo lectura
id solo lecturastring
external_id solo lecturastring/null
namestring
areasarrayCiudades o barrios que cubre la zona.
currencystring · PYG, USD, BRL, ARS, EUR, UYU, CLP, BOB, PEN, COP, MXN, GBP, CAD, CHF, JPY, CNY
pricenumber/string/nullCosto de envío. 0 = gratis; null = a coordinar.
free_fromnumber/string/nullMonto en unidades de la moneda (189000, 12500.50 o "12500.50"). null = a consultar.
min_ordernumber/string/nullMonto en unidades de la moneda (189000, 12500.50 o "12500.50"). null = a consultar.
etastring/null
notesstring/null
sort_orderinteger
is_activeboolean
kindstring · delivery, pickupEnvío a domicilio o punto de retiro.
addressstring/nullDirección del punto de retiro.
price_per_kgnumber/string/nullCosto extra por kilo.
max_weight_ginteger/null
carrierstring/null
source_updated_atstring/null
version solo lecturainteger

Promoción

CampoTipoDescripción
object solo lectura
id solo lecturastring
external_id solo lecturastring/null
namestring
descriptionstring/null
typestring · percent, amount, fixed_price, buy_x_get_y, free_shipping
percentnumberPara type=percent; en buy_x_get_y, el descuento de las unidades extra (100 = gratis).
buy_quantityinteger/nullbuy_x_get_y: 2x1 = 1; 3x2 = 2.
get_quantityinteger/nullbuy_x_get_y: unidades con descuento.
customer_groupstring/nullSolo para una lista de clientes (p. ej. mayoristas).
scheduleobject/nullDías y horarios en que vale (happy hour).
amountnumber/string/nullPara type=amount (monto que se descuenta).
pricenumber/string/nullPara type=fixed_price (precio especial).
currencystring · PYG, USD, BRL, ARS, EUR, UYU, CLP, BOB, PEN, COP, MXN, GBP, CAD, CHF, JPY, CNY
applies_tostring · all, category, items
categorystring/null
category_external_idstring/null
itemsarrayexternal_id o id:<uuid> de productos o servicios.
min_quantityinteger/null
min_amountnumber/string/nullMonto en unidades de la moneda (189000, 12500.50 o "12500.50"). null = a consultar.
payment_methodstring/null
codestring/nullCódigo de cupón.
starts_atstring/null
ends_atstring/null
is_activeboolean
source_updated_atstring/null
version solo lecturainteger

Evento

CampoTipoDescripción
idstring
sequenceinteger
typestring · 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, sync.completed, sync.failed, contact.created, contact.updated, contact.stage_changed, contact.deleted, conversation.created, conversation.updated, message.received, message.sent, message.status_updated, message.failed, message.edited, message.deleted, ai.request.completed, ai.request.failed, automation.executed, automation.failed, subscription.updated, subscription.suspended
created_atstring/null
resourceobject
originobject
changesarray
dataobject

Eventos y webhooks

Cada cambio del catálogo genera un evento, venga de la web de Dotly, de la API o de una integración. Los recibís de dos formas:

  • Webhooks (Conector > Webhooks, o POST /webhooks): Dotly envía un POST JSON a tu URL https. Respondé 2xx en menos de 10 s. Si falla, reintenta a los 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, 24 h; después la entrega queda sin entregar (se puede reintentar desde Dotly). Un destino con 50 fallas seguidas se pausa solo.
  • Feed (GET /events?after=): consultá cada tanto con el cursor; no necesitás exponer un servidor.

La entrega es «al menos una vez»: procesá cada evento de forma idempotente con su id. Una integración dueña de un destino no recibe los cambios que ella misma hizo (evita bucles); en el feed, origin.connection_id dice quién originó el cambio.

Encabezados: Dotly-Event-Id, Dotly-Event-Type, Dotly-Delivery-Id, Dotly-Delivery-Attempt y Dotly-Signature: t=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secreto, "<t>.<cuerpo>"). Verificá la firma sobre el cuerpo exacto recibido y rechazá marcas de más de 5 minutos.

Tipos: 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, sync.completed, sync.failed, contact.created, contact.updated, contact.stage_changed, contact.deleted, conversation.created, conversation.updated, message.received, message.sent, message.status_updated, message.failed, message.edited, message.deleted, ai.request.completed, ai.request.failed, automation.executed, automation.failed, subscription.updated, subscription.suspended. Los filtros admiten comodines como catalog.item.*.

{
  "object": "event",
  "id": "5b0c8f0e-6a8e-4b41-9d0a-3f5f2f1d7c11",
  "sequence": 1042,
  "type": "catalog.item.updated",
  "created_at": "2026-10-17T18:30:00Z",
  "resource": {"type": "item", "id": "01a1…", "external_id": "erp-1001"},
  "origin": {"connection_id": null, "source": "dotly"},
  "changes": ["price", "stock"],
  "data": { "object": "item", "external_id": "erp-1001", "name": "Zapatilla Runner Pro", "price": 450000, "…": "…" }
}

Shopify, Odoo, WooCommerce y feeds

Las integraciones listas usan el mismo motor que la API. Dotly consulta el sistema con la frecuencia que elijas (desde cada 15 minutos) y trae solo lo modificado; una vez por día hace una sincronización completa para detectar lo que se eliminó. Con la URL de aviso de la integración, tu sistema puede pedir una sincronización al instante. Las credenciales se guardan cifradas con la clave de tu empresa y nunca se vuelven a mostrar.

  • Shopify: dominio .myshopify.com y una app propia. Desde 2026 se crea en el Dev Dashboard de Shopify (en la misma organización que la tienda) e instalada en ella; en Dotly se cargan su Client ID y Client Secret, y Dotly obtiene y renueva el token solo (dura 24 h). Las apps creadas en el admin antes de 2026 siguen funcionando con su token shpat_. Permisos: read_products y read_inventory; para traer también descuentos, envíos y retiro en tienda, read_discounts, read_shipping y read_locations (sin ellos, esa parte se saltea y el resto se sincroniza igual). Trae productos, todas las variantes, precio de lista y de oferta, costo, código de barras, peso, fotos, etiquetas y marca; descuentos por código y automáticos (porcentaje, monto, compre X lleve Y, envío gratis, con mínimos y fechas); zonas de envío con tarifa, envío gratis desde y peso máximo; y los locales con retiro.
  • Odoo 14 en adelante: URL, base de datos, usuario y clave de API (Preferencias > Seguridad de la cuenta). Usa la API JSON-2 de Odoo 19 y 20 o el JSON-RPC de versiones anteriores (se detecta solo). En Odoo Online la API externa solo está disponible en el plan Custom; con Standard, exportá los productos a Excel e importalos como archivo. Trae categorías con jerarquía, plantillas y variantes con sus atributos, precio de venta, costo, moneda, IVA, código de barras, peso, etiquetas, stock, las listas de precios (mayoristas, por cantidad, en otras monedas y descuentos por categoría), las promociones y cupones (Odoo 16+), los métodos de envío y la foto y página de los productos publicados. Los campos y módulos que una versión no tiene se omiten solos.
  • WooCommerce: URL de la tienda y una clave de la REST API con permiso de lectura. Trae categorías con jerarquía, productos simples y variables con todas sus variaciones, precio regular y de oferta (con sus fechas), stock, encargos sin stock, fotos, etiquetas, marca, código de barras, peso y medidas, zonas de envío y cupones. Si el servidor descarta el encabezado de autenticación, Dotly usa la clave en la URL (sobre https); si un firewall (Cloudflare, Wordfence, Sucuri) bloquea, el error lo indica.
  • Feed CSV o JSON: cada sincronización lee el archivo completo. JSON: una lista de productos con el formato de la API o un objeto con items, categories, zones y discounts. CSV (coma, punto y coma o tabulación): columnas external_id, sku, nombre, descripcion, categoria, precio, precio_promocional, moneda, stock, tipo, unidad, iva, activo, duracion, notas y, para variantes, variante, sku_variante, precio_variante, stock_variante (filas con el mismo external_id son variantes del mismo producto).

Errores

Los errores devuelven {"error": {"code", "message", "details"}}. En los datos no válidos, details.errors lista cada campo con su problema (por ejemplo, variants[2].price). En los lotes, cada operación trae su propio resultado: una que falla no afecta a las demás.

HTTPCódigoQué pasó
400invalid_json, unsupported_media_typeEl cuerpo no es JSON o falta Content-Type: application/json.
401invalid_api_keyFalta la clave, no es válida, venció o fue revocada.
403insufficient_scopeLa clave no tiene el permiso necesario (p. ej. catalog:write).
403integration_pausedLa integración está pausada en Dotly.
403subscription_inactiveLa suscripción no está al día (sin plan, pago pendiente, vencida o en pausa): la API no responde datos. details trae lapsed_at y data_deleted_at (cuándo se eliminan los datos del conector si no se renueva).
403feature_not_available, organization_suspendedEl plan no incluye el conector o la cuenta está suspendida.
413payload_too_largeEl cuerpo supera los 2 MB: dividí el envío en lotes.
404not_foundNo existe un recurso con esa referencia.
409conflictDos escrituras simultáneas con el mismo identificador. Reintentá.
412version_conflictIf-Match no coincide con la versión actual (details.current_version).
422validation_errorDatos no válidos. details.errors lista todos los campos con problema.
422duplicate_sku, duplicate_name, category_not_found, item_not_found, limit_reachedReglas del catálogo.
422idempotency_key_reusedEl mismo Idempotency-Key se usó con otro cuerpo.
422sync_run_closedLa sincronización completa ya terminó o venció (24 h).
429rate_limitedSuperaste el límite de tu clave (o muchos intentos con claves inválidas desde tu IP). Esperá lo que indica Retry-After.

Límites y versionado

  • Solicitudes: 600 por minuto y 20.000 por hora por clave (429 con Retry-After).
  • Lotes e inventario: hasta 500 operaciones por solicitud y 2 MB por cuerpo.
  • Catálogo: hasta 10.000 productos y servicios, 250 variantes por producto y 10 destinos de webhooks.
  • Las sincronizaciones completas vencen a las 24 h si no se completan (no borran nada).
  • Versión v1 en la URL. Los cambios compatibles (campos o eventos nuevos) se agregan sin cambiar de versión: ignorá los campos que no conozcas. Un cambio incompatible saldría como v2, con aviso previo.

Seguridad

  • Solo https. Las claves se guardan en Dotly como hash: si se pierde una, revocala y creá otra.
  • Usá una clave por sistema y solo con los permisos que necesita (por ejemplo, un sistema de stock con catalog:write nada más).
  • Todo lo que lee Dot AI (nombres, descripciones, notas, características) pasa por el mismo filtro que la web: no puede contener instrucciones para la IA. metadata no lo lee Dot AI.
  • Cada organización está aislada: una clave solo ve y cambia los datos de su empresa.
  • Dotly no sigue redirecciones ni se conecta a direcciones privadas cuando consulta tus sistemas o envía webhooks.