Desarrolladores / Python

El SDK de Python

Leer y responder chats, enviar imágenes y vídeo, crear bots y permitir que el agente de IA consulte sus propios sistemas, desde Python. Todo lo que hace la API REST, con las partes delicadas ya resueltas: comprobar que una solicitud procede realmente de nosotros, no responder dos veces al mismo evento, reintentar con seguridad y capturar un mensaje mal formado antes de que salga de su proceso.

¿Aún no usas Conecto? Empiece aquí , cinco minutos de vocabulario y el resto de la página se explica solo.

pip install conectov0.1.0Python 3.9+MIT

One dependency (requests) · Type hints throughout · En PyPI

Empiece aquí

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Si nunca ha usado Conecto, esta sección contiene todo el modelo mental. Cinco minutos aquí hacen evidente el resto de la página.

Qué es Conecto

Un widget de chat en su sitio web. Los visitantes escriben; su equipo, un agente de IA o su propio código responde. Todo lo siguiente sirve para introducir su código su código en ese bucle.

Los cinco sustantivos

Casi todos los métodos de este SDK aceptan o devuelven uno de estos objetos.

Espacio de trabajo

Su cuenta. Su credencial pertenece exactamente a una y nunca puede ver otra.

Widget

One installed chat box. You might have one per website or brand. Has an id.

Visitante

A browser on your site, identified by a session string stored in that browser.

Conversación

One thread between a visitor and you. Has an id, and a status of open or closed.

Mensaje

One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.

Las tres cosas que puede crear

1. Un script

Se ejecuta según su horario. Sincroniza contactos, abre tickets y envía mensajes de campaña. No hay nada que alojar, el script solo llama a la API.

2. Un bot

Un servidor web suyo al que avisamos cuando escribe un visitante. Usted decide la respuesta. Usted controlar la conversación.

3. Un plugin

Un servidor web suyo al que llamamos cuando el IA necesita un dato que solo usted conoce. La IA controla la conversación; usted proporciona las consultas.

¿Bot o plugin? Si quiere escribir las palabras que lee el visitante, cree un bot. Si prefiere dejar hablar a la IA y darle solo acceso a sus datos, como pedidos, suscripciones o existencias, cree un plugin. La mayoría de los equipos termina eligiendo un plugin.

Palabras que utiliza esta página

Jerga definida una sola vez. Solo los dos últimos términos son específicos de Conecto.

WebhookEnviamos una solicitud HTTP a una URL suya cuando ocurre algo, en lugar de hacer que nos consulte repetidamente. Necesita un servidor accesible desde Internet.
FirmaUn hash que añadimos a cada solicitud y calculamos con un secreto que solo usted y nosotros conocemos. Comprobarlo demuestra que la solicitud procede realmente de nosotros y que nadie la modificó. El SDK lo hace por usted.
Al menos una vezSi no podemos determinar si recibió algo, lo enviamos de nuevo. Por eso puede recibir a veces el mismo evento dos veces; su código debe detectarlo en lugar de actuar dos veces.
IdempotenteEjecutarlo dos veces produce el mismo resultado que una sola vez. Envíe dos veces un mensaje con la misma clave y la segunda llamada devolverá el primer mensaje en lugar de publicar un duplicado.
Paginación por cursorLas listas largas llegan por páginas. En lugar de «página 3», pase el ID recibido la última vez. El SDK oculta este detalle; usted solo itera.
BloqueUna pieza de contenido enriquecido dentro de un mensaje, como una imagen, vídeo, tarjeta o fila de botones. Consulte Contenido enriquecido.
Visitante verificadoAlguien cuyo correo hemos demostrado , mediante un código enviado por correo o porque su sitio nos indicó que la persona ha iniciado sesión. Cualquier operación que afecte a los datos privados de una persona espera a esta verificación.

¿Qué sección necesito?

Quiero…Ve a
Leer o responder chats desde un scriptConversaciones
Enviar una imagen, GIF, vídeo o tarjetaContenido enriquecido
Responder automáticamente a visitantes con mi propia lógicaCrear un bot
Dejar que la IA consulte mis sistemasCrear un plugin
Conectar una tienda que la IA pueda buscarCrear un plugin
Omitir el código por correo para usuarios ya conectadosIdentidad
Mantener mi CRM sincronizadoContactos y visitantes
Saber qué capturar cuando algo fallaErrores y reintentos

Instalar y autenticar

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok
pip install conecto

Crear una credencial en Ajustes → Desarrolladores. Obtiene un ID de cliente (ck_…) and a secreto (cs_…). The secret is shown once and stored hashed, so put it somewhere your code can read and your repository cannot.

export CONECTO_CLIENT_ID=ck_your_client_id
export CONECTO_SECRET=cs_your_secret
from conecto import Conecto

client = Conecto()                    # reads the environment
client = Conecto("ck_...", "cs_...")  # or pass them directly

print(client.ping()["workspace"]["name"])

Call ping() once at startup. It raises AuthenticationError immediately if the credential is wrong or revoked — much better than finding out mid-conversation. A credential can cover the workspace or be scoped to one widget; a scoped one simply cannot see other widgets' conversations.

Inicio rápido

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Tres cosas que puede hacer en los primeros cinco minutos.

Leer qué está ocurriendo

for convo in client.conversations.list(status="open"):
    who = convo.visitor.email if convo.visitor else "anonymous"
    print(f"#{convo.id}  {who}  {convo.preview}")

Responder a alguien

convo = client.conversations.get(4821)
print(convo.messages[-1].body)          # what they said last
convo.reply("On it — give me one moment.")

Enviar algo que no sea texto

from conecto import blocks

convo.reply("Here's how to reset it:", blocks=[
    blocks.image("https://cdn.you.com/reset.gif", alt="Reset flow"),
    blocks.buttons([
        blocks.reply_button("That worked"),
        blocks.link_button("Full guide", "https://docs.you.com/reset"),
    ]),
])

Así se estructura toda la biblioteca: un cliente con recursos, objetos capaces de actuar sobre sí mismos y builders para cualquier contenido estructurado.

El cliente

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Cree un cliente y consérvelo. Agrupa conexiones y puede compartirse de forma segura entre hilos.

client = Conecto(
    client_id="ck_...",
    secret="cs_...",
    base_url="https://conecto.chat/api/v1",  # override for staging
    timeout=30.0,        # seconds per request
    max_retries=2,       # timeouts, 429s and 5xx
    app="acme-billing",  # added to the User-Agent; shows up in our logs
)

Entorno

VariableEstablece
CONECTO_CLIENT_IDEl ID de cliente.
CONECTO_SECRETEl secreto.
CONECTO_BASE_URLURL base de la API. Rara vez necesaria.

Recursos

AtributoAbarca
client.conversationsListar, leer, responder, indicador de escritura, transferir, asignar, cerrar.
client.contactsEl CRM: upsert por correo, buscar, actualizar, eliminar.
client.visitorsSesiones, validación de identidad y mensajes proactivos.
client.ticketsCrear, actualizar, responder, categorías.
client.articlesBuscar y publicar contenido del centro de ayuda.
client.integrationsRegistrar, instalar, ejecutar y desplegar plugins.
client.widgetsLeer y actualizar la configuración del widget.
client.mediaSubir archivos para utilizarlos en bloques.
client.webhooksSuscribir endpoints a eventos.
client.membersCompañeros para asignaciones.
client.metame(), schema(), stats().

Desviación de versiones

Cada respuesta incluye la versión de la API del servidor. Regístrela al iniciar. Si cambia y desaparece un campo que utiliza, será lo primero que querrá conocer.

print(client.api_version)   # what the server last reported
print(client.sdk_version)   # what this SDK was built against

limits = client.meta.schema()["limits"]
assert limits["blocks_per_message"] >= 4

client.meta.schema() devuelve toda la API como JSON, incluidos endpoints, eventos, tipos de bloque, acciones reservadas, códigos de error y todos los límites. El mismo código aplica los límites y sirve la respuesta, por lo que no puede apartarse de la realidad.

Conversaciones

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

A thread between a visitor and your workspace. Messages have a sender de visitor, agent, bot o system.

Listar y leer

page = client.conversations.list(status="open", limit=25)

page.items                     # this page only
for convo in page:             # every page, fetched lazily
    ...

convo = client.conversations.get(4821)
convo.status, convo.visitor.email, convo.messages[-1].body

# Poll cheaply: only what is newer than what you already have.
fresh = client.conversations.messages(4821, since_id=last_seen_id)

Responder

convo.reply("I can refund order #1284 — confirm?",
            buttons=["Yes, refund it", "Talk to a human"])

Al pulsar un botón de respuesta rápida, el texto vuelve como un mensaje normal del visitante; por tanto, el controlador no necesita ningún caso especial.

ArgumentoFunción
bodyEl texto, hasta 4000 caracteres.
blocksContenido enriquecido, consulte Contenido enriquecido.
buttonsHasta 6 respuestas rápidas.
productsHasta 4 tarjetas de producto, iguales a las que adjunta la IA integrada.
ask_emailMuestra el formulario nativo de captura de correo.
ticket_formAdjunta el formulario nativo para abrir tickets.
internalUna nota solo para agentes. El visitante nunca la ve.
idempotency_keyPase el ID de entrega del webhook para que un reintento no publique dos veces.

Dirección del hilo

convo.typing()                  # "…is typing", expires after ~8s
convo.reply("Working on it…")

convo.reply("Escalating: refund over policy limit.", internal=True)
convo.handoff()                 # flag for a human; routing rules apply
convo.assign(user_id=12)        # ids come from client.members.list()

convo.close()
convo.reopen()

Writing to a closed thread raises ConversationClosed. Reopen it first, or catch it — see Errores.

Contenido enriquecido

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Cualquier mensaje puede incluir hasta diez bloques: imágenes y GIF, vídeo, audio, archivos, contenido incrustado de terceros, carruseles de tarjetas, listas de etiqueta/valor, botones y separadores. Se muestran en el widget y en la bandeja del agente.

Los bloques son diccionarios sencillos durante la transmisión y podrían escribirse a mano. Los builders existen porque una clave mal escrita se convierte en un cliente que dice que la imagen nunca apareció. Aplican las mismas reglas que el servidor cuando el traceback todavía apunta a su código.

from conecto import blocks

convo.reply("Here's your order:", blocks=[
    blocks.list_block([
        blocks.row("Placed", "12 July 2026"),
        blocks.row("Status", "Shipped"),
        blocks.row("Carrier", "DHL", subtitle="Tracking 4Z8871",
                   url="https://track.dhl.com/4Z8871"),
    ], title="Order #1042"),
    blocks.divider(),
    blocks.cards([
        blocks.card("Trail Runner 2",
                    subtitle="Road · Neutral",
                    price="89.00 USD",
                    badge="Back in stock",
                    image="https://cdn.you.com/tr2.jpg",
                    url="https://shop.you.com/p/tr2",
                    buttons=[blocks.link_button("Buy", "https://shop.you.com/cart")]),
    ]),
])

Cada tipo de bloque

BuilderNotas
blocks.image(url, alt, caption, link)Animated GIFs are images — point at the .gif. blocks.gif is an alias.
blocks.video(url, poster, autoplay, loop, muted)Direct files only (.mp4, .webm, .mov…).
blocks.embed(url)YouTube, Vimeo, Loom, Wistia y Spotify. Pase el enlace normal para compartir.
blocks.audio(url, title).mp3, .wav, .m4a
blocks.file(url, filename, size)Una fila de descarga.
blocks.cards([...], layout)carousel scrolls sideways, list stacks full width.
blocks.list_block([...], title)Label/value rows. blocks.rows is an alias.
blocks.buttons([...])Mix reply_button y link_button.
blocks.text(body)Un párrafo adicional debajo de otros bloques.
blocks.divider()Una línea horizontal.

Dos tipos de botones

blocks.reply_button("Yes, refund it")                    # sends that text back
blocks.reply_button("Yes, refund it", "confirm_refund")  # nice label, stable value
blocks.link_button("Read the policy", "https://you.com/refunds")  # opens a tab

Dé un valor explícito a un botón de respuesta cuando la etiqueta deba leerse bien, pero el texto que interpreta su bot no deba cambiar cada vez que alguien reescribe el contenido.

Enviar un GIF

A .gif works. A muted, looping, autoplaying video is usually the better trade — a fraction of the bytes, and nobody can tell.

blocks.video("https://cdn.you.com/reset.mp4",
             autoplay=True, loop=True, muted=True)

Reglas que aplican los builders

  • Every URL must be https. The widget runs on an https page, so anything else is blocked by the browser anyway.
  • Diez bloques por mensaje, diez tarjetas, doce filas de lista, seis botones.
  • Long strings are truncated, not rejected. Structural mistakes raise BlockError.
  • A YouTube link in a video block raises, and tells you to use embed.

La validación se realiza localmente. reply(blocks=…) checks the list before sending, so a typo raises BlockError from your line rather than coming back as an HTTP 400 from inside the client.

Subida de archivos

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

¿No hay una URL pública para ese GIF, recibo o ficha técnica? Suba el archivo, hasta 10 MB, y use el resultado.

media = client.media.upload("celebrate.gif")     # a path
media = client.media.upload(open("a.pdf", "rb")) # a file object
media = client.media.upload(raw_bytes, filename="receipt.png")

convo.reply("All set!", blocks=[blocks.image(media)])

A Media can be passed straight to blocks.image, blocks.video o blocks.file.

Use media.url, not media.absolute_url. url es una ruta que el widget resuelve respecto a su propio origen. Así, el mismo mensaje funciona en desarrollo, en una vista previa y en producción, incluso después de cambiar de dominio.

Contactos y visitantes

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Contactos

The CRM. upsert is keyed on email, so it is safe to point a nightly sync at.

client.contacts.upsert(
    "maya@acme.io",
    name="Maya Silva",
    company="Acme",
    custom_fields={"plan": "scale", "mrr": "480"},
)

contact = client.contacts.find("maya@acme.io")     # or None
for c in client.contacts.list(query="acme"):
    print(c.email, c.custom_fields.get("plan"))

custom_fields es combinado, no se sustituyen. Dos tareas pueden administrar sus propias claves sin sobrescribirse. Hasta 30 claves por llamada.

Visitantes

A browser session on a widget, keyed by (widget_id, session). The session id lives in the visitor's browser; read it client-side and send it to your backend.

visitor = client.visitors.get(widget_id=7, session=session_id)
visitor.email, visitor.verified

Mensajes proactivos

Enviar a una sesión sin esperar una petición, por ejemplo novedades de envío, avisos de prueba o recuperación del carrito.

client.visitors.message(
    widget_id=7, session=session_id,
    body="Still thinking it over? Your cart is saved:",
    blocks=[blocks.cards([
        blocks.card("Trail Runner 2", price="89.00 USD",
                    image="https://cdn.you.com/tr2.jpg",
                    url="https://shop.you.com/cart"),
    ])],
)

Reutiliza la conversación activa de la sesión o abre una. Un widget abierto la muestra en la siguiente consulta; uno cerrado la muestra al volver a abrirse, con el tratamiento de no leído.

Tickets y artículos

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Tickets

ticket = client.tickets.create(
    email="maya@acme.io",
    message="Card was declined on renewal.",
    priority="high",          # low · normal · high · urgent
)

client.tickets.update(ticket.id, status="pending", assignee_user_id=12)
ticket.reply("We've fixed the card on file — try again?")   # emails the requester
ticket.reply("Billing confirmed the retry.", internal=True)  # private note

Una respuesta pública reabre un ticket resuelto, porque responder a algo marcado como terminado casi siempre significa que empieza otra ronda.

Artículos del centro de ayuda

Útil en ambas direcciones: un bot puede buscar ahí antes de abrir un ticket y usted puede enviar documentación desde cualquier herramienta de autoría.

hits = client.articles.list(query="refund", published=True)
if hits:
    convo.reply(f"This might help: “{hits[0].title}”")

client.articles.create("Shipping times", "<p>2-4 business days.</p>",
                       published=True)

El HTML se limpia en el servidor con una lista de etiquetas seguras, mediante la misma canalización que usa el editor del panel. Los scripts y estilos se eliminan antes de guardar nada.

Crear un bot

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

A bot is a webhook and a reply. Bot handles the five things that are easy to get subtly wrong — signature verification, delivery deduplication, event routing, filtering out your own messages, and answering fast — so your code is the part that is yours.

from conecto import Conecto, Bot, blocks

client = Conecto()
bot = Bot(client, secret=WEBHOOK_SECRET)

@bot.on_message
def handle(ctx):
    text = ctx.text.lower()

    if "human" in text:
        ctx.reply("Of course — connecting you now.")
        ctx.handoff()
        return

    if "order" in text:
        if not ctx.verified:
            ctx.reply("What's the email on the order?", ask_email=True)
            return
        order = lookup_order(ctx.verified_email)     # your system
        ctx.reply("Here it is:", blocks=[
            blocks.list_block([
                blocks.row("Status", order["status"]),
                blocks.row("Carrier", order["carrier"]),
            ], title=order["number"]),
        ])
        return

    ctx.reply("I'm not sure about that one — let me get someone who is.")
    ctx.note(f"Bot could not classify: {ctx.text!r}")
    ctx.handoff()

Publicación

# Flask
app.add_url_rule("/conecto", view_func=bot.flask_view(), methods=["POST"])

# FastAPI (handlers run in a worker thread, so they never stall the loop)
app.post("/conecto")(bot.fastapi_route())

# Django (urls.py)
path("conecto/", csrf_exempt(bot.django_view()))

# No framework at all
from wsgiref.simple_server import make_server
make_server("", 8000, bot.wsgi_app()).serve_forever()

Después, suscriba el endpoint:

hook = client.webhooks.create(
    "https://bots.you.com/conecto",
    ["message.created", "conversation.created", "conversation.rated"],
)
print(hook.secret)   # store this — it is how you verify deliveries

Controladores

DecoradorSe activa con
@bot.on_messageUn mensaje de un visitante. Los mensajes de bots y agentes nunca lo alcanzan, lo que impide que un bot se responda a sí mismo.
@bot.on_conversation_startedconversation.created , saludar o preparar el estado.
@bot.on_ratingconversation.rated. ctx.event.rating has score y comment.
@bot.on_ticket · @bot.on_contactticket.created · contact.created.
@bot.on("event.name")Any event by name. @bot.on("*") catches everything.
@bot.on_errorUn controlador produjo un error. Sin un controlador, los errores se registran y se descartan.

Qué recibe un controlador

En el contextoEs
ctx.textEl texto del mensaje del visitante.
ctx.verified · ctx.verified_emailSi la identidad está demostrada y qué dirección se ha demostrado.
ctx.conversation_id · ctx.widget_id · ctx.sessionID sobre los que puede actuar.
ctx.reply(...)Answer. Takes everything conversations.reply does.
ctx.note(...)Una nota solo para agentes.
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close()Dirigir el hilo.
ctx.history()Obtener todo el historial de mensajes. Una solicitud.
ctx.client · ctx.eventEl cliente de bajo nivel y el evento analizado, para todo lo demás.

¿Ejecuta más de un worker? Deduplication is in-process by default, so two workers can each handle the same delivery once. Pass your own store — anything with add(id) -> bool, backed by Redis or your database.

class RedisSeen:
    def add(self, delivery_id):
        # True if new, False if we have handled it before
        return bool(redis.set(f"conecto:{delivery_id}", 1, nx=True, ex=86400))

bot = Bot(client, secret=WEBHOOK_SECRET, seen=RedisSeen())

Crear un plugin

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Un bot responde a webhooks. Un plugin es la otra dirección: usted declara lo que pueden hacer sus sistemas y el agente de IA decide cuándo usarlo mientras habla con alguien. Nunca escribe la lógica de la conversación, solo la consulta.

Así es como una tienda, sistema de facturación o CRM que no conocemos se convierte en una integración de primera clase.

from conecto import Conecto, Plugin, parameter

plugin = Plugin("acme-store", base_url="https://api.acme.com/conecto")

@plugin.action(
    "catalog.search_products",
    description="Search the Acme catalog by keyword.",
    parameters=[parameter("query", required=True,
                          description="Search words from the visitor.")],
)
def search(req):
    hits = catalog.search(req.get("query"), limit=req.get("limit", 4))
    if not hits:
        return req.not_found()
    return req.ok(products=[
        {"title": p.name, "url": p.url, "image": p.image,
         "price_from": f"{p.price:.2f}", "currency": "USD",
         "available": p.stock > 0}
        for p in hits
    ])

@plugin.action(
    "orders.get_status",
    risk="verified_read",
    description="Status of one of the visitor's own orders.",
    parameters=[parameter("order_number", required=True)],
)
def order_status(req):
    order = orders.find(req.get("order_number"), email=req.verified_email)
    return req.ok(**order) if order else req.not_found()

Servir y registrar

# One route serves every action.
app.add_url_rule("/conecto/<path:action>", view_func=plugin.flask_view(),
                 methods=["POST"])

# Tell Conecto it exists. Idempotent — run it on every release.
integration = plugin.deploy(Conecto())
print(integration.signing_secret)   # set this in your environment

deploy() crea o actualiza la integración para que coincida con su código, elimina las acciones retiradas y la instala en sus widgets. No se expone nada a la IA hasta instalarla.

Los cinco tipos de respuesta

return req.ok(status="Shipped", carrier="DHL")   # success
return req.ok(result, blocks=[...])              # …plus rich content in the reply
return req.not_found()                           # nothing matched — a normal outcome
return req.verify_required()                     # "I need a proven identity"
return req.error("That subscription was already cancelled.")

Write error messages for the visitor, not for your logs: the AI may relay them. Returning a plain dict is also fine — it is treated as the result.

Niveles de riesgo

RiesgoSignifica
public_readCatálogo, disponibilidad y documentación. No requiere identidad.
verified_readLos datos de una persona. Se rechazan hasta demostrar su correo electrónico.
public_writeUna modificación que no requiere identidad, como suscripción al boletín o captura de leads.
writeUna modificación en la cuenta de una persona. Verificada, auditada y nunca reintentada en silencio.

req.verified_email es la única identidad de confianza. An email in req.arguments is whatever the model parsed out of a chat. For verified_read y write the platform will not call you at all until we have proven who the visitor is, and the proven address is the one we hand you.

Acciones reservadas

Algunos nombres tienen un significado especial para la plataforma. Declare uno y su servicio se conectará a la función que ya existe para él.

NombreQué obtiene
catalog.search_productsResults become product cards under the AI's replies, and can fill the widget's Home showcase. Return {"products": [{title, url, image, price_from, currency, available}]}.
orders.get_statusEstado de pedido, siempre sujeto a verificación.
orders.get_trackingSeguimiento de envíos, siempre verificado.
orders.list_recentLos pedidos recientes del visitante, siempre verificados.
account.lookupCualquier registro plano sobre el visitante verificado, como plan, créditos o fecha de renovación.

Probarlo antes que un visitante

client.integrations.run("acme-store", "catalog.search_products",
                        {"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}

Esto ejecuta la real call path — the signature, your credential, the checks that stop your URL pointing anywhere private, the timeout, the parsing — so what you see is exactly what the AI will get. Pass conversation_id= to run in the context of a live chat, which is the only way a verified action can succeed.

Escribir una buena descripción

El description is the highest-leverage string in your integration: it is what the AI reads to decide whether this action answers the question in front of it. Write it as an instruction to a capable colleague who cannot see your code.

Débil: «Consulta de pedidos.»
Fuerte: «Consulta el estado y la fecha de entrega de uno de los pedidos del visitante mediante su número. Úsalo siempre que pregunte dónde está algo o cuándo llegará.»

Qué garantiza la plataforma

  • 8 segundos tiempo de espera, 128 KB límite de respuesta.
  • Calls are signed with your signing_secret — same scheme as webhooks.
  • Su URL se resuelve una vez y se fija a una IP pública; las redirecciones se rechazan.
  • Todo lo que devuelve llega al modelo como datos, nunca como instrucciones. Las claves que parecen secretos se eliminan.
  • Las integraciones personalizadas requieren un plan de pago Agente de IA. 20 por espacio de trabajo, 40 acciones cada una.

Identidad

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Cualquier operación que afecte a los datos de una persona espera hasta que sepamos quién es. Esto sucede de dos formas: mediante un código enviado por correo o gracias a la validación de su sitio para un usuario que ya ha iniciado sesión.

# your backend, right after your own auth check
client.visitors.identify(
    widget_id=7,
    session=conecto_session,          # read from the visitor's browser
    email=user.email,
    name=user.name,
    verified=True,                    # the vouch
    verify_hours=24,                  # max 72, default 12
    data={"plan": user.plan, "customer_since": "2024"},
)

# when they log out
client.visitors.unverify(widget_id=7, session=conecto_session)

Mientras dura la validación, Conecto considera demostrado el correo: el widget conoce el nombre, los chats se vinculan al contacto correcto y los flujos que de otro modo pedirían un código por correo, como consultas de pedidos, reembolsos, datos de cuenta o acciones verificadas de su plugin, continúan sin él.

Valide solo las sesiones que su backend haya autenticado realmente. Su validación se considera tan sólida como nuestro propio código por correo porque realmente autenticó a la persona. Tiene duración limitada y ámbito de sesión, por lo que un navegador nuevo necesita una nueva validación. El navegador nunca puede marcarse a sí mismo como verificado, ya que esa llamada está autenticada y debe hacerse en su servidor.

Webhooks y eventos

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Bot se ocupa de todo esto. Use los helpers de bajo nivel cuando quiera verificar en el borde y procesar en una cola.

from conecto import webhooks

event = webhooks.parse(request.get_data(), request.headers, secret=SECRET)

event.id                  # the delivery id — dedupe on this
event.event               # "message.created"
event.conversation_id
event.text
event.is_visitor_message  # excludes your own bot's messages
event.visitor.verified

Verificar con el cuerpo sin procesar. Re-serializing JSON changes the bytes and the signature stops matching — this is the most common first-integration bug there is. Use request.get_data() in Flask, request.body in Django, await request.body() in FastAPI. Passing a parsed dict raises with this exact advice.

Eventos

conversation.createdUn visitante inició una conversación.
message.createdUn visitante envió un mensaje. El activador del bot.
conversation.closedSe cerró una conversación.
conversation.assignedAsignado a un compañero o sin asignar.
conversation.handoffMarcado como que necesita una persona.
conversation.ratedLlegó una valoración CSAT (1–5 más un comentario).
ticket.createdSe abrió un ticket desde cualquier origen.
ticket.updatedCambió el estado, la prioridad o la persona asignada a un ticket.
contact.createdUna nueva persona entró en el CRM.
visitor.identifiedUna sesión se identificó mediante la API.

Semántica de entrega

Las entregas son al menos una vez y sin orden garantizado: if we cannot confirm you got one we send it again, and they will not always arrive in the order things happened. They also time out after 2.5 seconds — so answer 200 immediately and do the work afterwards.

  • Every payload carries an id (also in X-Conecto-Delivery). Store it and skip ids you have already handled.
  • Pass that same id as idempotency_key when you reply, and a redelivery can never make your bot speak twice.
  • X-Conecto-Timestamp permite rechazar entregas antiguas; el SDK lo comprueba con una tolerancia predeterminada de cinco minutos.
signature = webhooks.sign(SECRET, raw_body)      # what we send
webhooks.verify(raw_body, header_value, SECRET)  # True / False, never raises

Llamadas de integración que realizamos a your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.

Errores y reintentos

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Every failure raises a subclass of ConectoError, so you can catch exactly what you expect.

from conecto import ConversationClosed, RateLimited, NotFoundError

try:
    convo.reply("Refunded — you'll see it in 3-5 days.")
except ConversationClosed:
    convo.reopen()
    convo.reply("Refunded — you'll see it in 3-5 days.")
except NotFoundError:
    log.warning("conversation vanished")
ExcepciónEstadoSignifica
AuthenticationError401Credencial ausente, desconocida o revocada.
InvalidRequestError400El cuerpo no superó la validación; el mensaje indica el motivo.
NotFoundError404Objeto inexistente en este espacio de trabajo.
ConflictError409Ese identificador ya está ocupado.
ConversationClosed409Reopen the thread first. A subclass of ConflictError.
LimitReached403Un límite del plan o espacio de trabajo.
PlanRequired403El plan no incluye esta función.
RateLimited429Over 300 requests/minute. Carries retry_after.
ServiceUnavailable503No hay una dependencia configurada.
ServerError5xxFallo nuestro. Se puede reintentar con seguridad.
BlockErrorProducido localmente por un builder de bloques antes de cualquier solicitud.
SignatureError401Falló la verificación de un webhook o una llamada de integración.
APIConnectionError · APITimeoutErrorLa solicitud nunca recibió respuesta.

Every exception carries code, message, status and the raw response.

Reintentos

Timeouts, rate limits (429) and server errors (5xx) are retried for you. Each retry waits a little longer than the last, with a random amount added so that a thousand clients recovering at once do not all come back in the same instant — and if we send a Retry-After, that wins.

Las escrituras también se reintentan, lo que normalmente sería peligroso. Aquí es seguro porque cada escritura incluye una clave de idempotencia y el reintento reutiliza la misma. El servidor reconoce el segundo intento como la misma llamada y no lo repite.

client = Conecto(max_retries=0)   # opt out entirely

Límites importantes

LímiteValor
Solicitudes por minuto y credencial300
Cuerpo del mensaje4000 caracteres
Bloques por mensaje10
Botones de respuesta rápida6
Subida de contenido multimedia10 MB
Integraciones por espacio de trabajo20
Acciones por integración40
Webhooks por espacio de trabajo10

client.meta.schema()["limits"] siempre contiene los números en directo.

Paginación

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

List endpoints return a Page you can use three ways.

page = client.contacts.list(query="acme")

page.items        # just this page — a list, so len() and [0] work
page.has_more     # is there another?

for contact in page:   # every page, fetched lazily as you consume
    ...

page.all()        # everything, collected into one list

Iterating is lazy: next(iter(page)) costs exactly one request no matter how many contacts exist. all() is fine for contacts and tickets; think twice on a busy workspace's conversations, where iterating and stopping early is usually what you meant.

Probando

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete y request works — no network, no credentials, no mocking library.

class FakeTransport:
    base_url = "https://conecto.test/api/v1"

    def __init__(self):
        self.calls = []
        self.responses = {}

    def request(self, method, path, **kw):
        self.calls.append((method, path, kw.get("json_body")))
        return self.responses.get(f"{method} {path}", {})

    def get(self, p, **kw): return self.request("GET", p, **kw)
    def post(self, p, json_body=None, **kw): return self.request("POST", p, json_body=json_body, **kw)
    def patch(self, p, json_body=None, **kw): return self.request("PATCH", p, json_body=json_body, **kw)
    def delete(self, p, **kw): return self.request("DELETE", p, **kw)


def test_bot_answers_refunds():
    transport = FakeTransport()
    client = Conecto(transport=transport)
    client.conversations.reply(1, "Refunded.")
    assert transport.calls[0][1] == "/conversations/1/messages/"

Para bots y plugins, omita la verificación de firma en las pruebas en lugar de firmar fixtures:

bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)

Referencia de métodos

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Conversaciones

client.conversations.list(status, widget_id, session, limit, before_id)

Conversaciones, actividad más reciente primero. Devuelve una Page.

client.conversations.get(id, since_id=None)

Una conversación con sus mensajes. since_id devuelve solo los mensajes más recientes.

client.conversations.messages(id, since_id=None)

Solo los mensajes.

client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)

Enviar un mensaje del bot. Alias: send().

client.conversations.typing(id, on=True, name="Bot")

Mostrar u ocultar el indicador de escritura.

client.conversations.handoff(id)

Marcar que se necesita una persona.

client.conversations.assign(id, user_id)

None elimina la asignación.

client.conversations.close(id) · reopen(id) · set_status(id, status)

Cambiar el estado.

Contactos

client.contacts.list(email, query, limit, before_id) · get(id) · find(email)

find() devuelve un contacto o None.

client.contacts.upsert(email, name, title, company, location, phone, notes, custom_fields)

Crear o actualizar por correo. Alias: create().

client.contacts.update(id, **fields) · delete(id)

Visitantes

client.visitors.get(widget_id, session)

Identidad y estado de verificación.

client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)

Adjunte su usuario; verified=True valida su identidad.

client.visitors.unverify(widget_id, session)

Revocar la validación. Llamar a al cerrar sesión.

client.visitors.message(widget_id, session, body, blocks, buttons, products)

Un mensaje proactivo.

Integraciones

client.integrations.list() · get(slug) · actions(slug)

actions() también devuelve el catálogo de acciones reservadas.

client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)

Registrar uno. La respuesta incluye signing_secret.

client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)

replace_actions convierte su lista en la fuente autorizada.

client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)

Crear o actualizar y después instalar. Idempotente, por lo que puede incluirlo en su script de despliegue.

client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)

actions es una lista de permitidos; [] no expone nada.

client.integrations.run(slug, action, arguments, conversation_id)

Invocar mediante la ruta real de llamada.

client.integrations.delete(slug) · remove_action(slug, name)

Tickets, artículos, widgets, contenido multimedia, webhooks y metadatos

client.tickets.list · get · create · update · reply · categories
client.articles.list · get · create · update · delete
client.widgets.get(id) · update(id, **fields)

La configuración completa está en widget.raw.

client.media.upload(file, filename, content_type)

Devuelve un objeto Media que puede pasar a un builder de bloques.

client.webhooks.list() · create(url, events, widget_id) · delete(id)

Los nombres de eventos desconocidos se rechazan localmente.

client.members.list() · client.meta.me() · schema() · stats()

Modelos

Every returned object keeps the payload it was built from. Attribute access is the nice path; obj.raw is always there for a field this SDK version does not know about yet, so a release is never what stands between you and a new API field.

convo.status          # typed
convo.raw["status"]   # the same thing
convo["status"]       # shorthand for the raw payload
convo.to_dict()       # exactly what the API sent

Recetas

¿Necesita ayuda? Pregunte a una IA sobre esta sección:ClaudeChatGPTGrok

Desviar un ticket mediante su centro de ayuda

@bot.on_message
def deflect(ctx):
    hits = ctx.client.articles.list(query=ctx.text, published=True)
    if hits:
        ctx.reply(f"This might help: “{hits[0].title}”. Did that solve it?",
                  buttons=["Solved it", "Open a ticket"])
    else:
        ctx.reply("Let's get this to the team.", ticket_form=True)

Calificar un lead y dirigirlo

@bot.on_message
def qualify(ctx):
    if not ctx.event.visitor or not ctx.event.visitor.email:
        ctx.reply("Happy to help — what's your work email?", ask_email=True)
        return

    ctx.client.contacts.upsert(
        ctx.event.visitor.email,
        custom_fields={"lead_source": "chat", "intent": classify(ctx.text)},
    )
    ctx.note("Qualified lead — routing to sales.")
    ctx.assign(SALES_USER_ID)
    ctx.handoff()

Recuperar una mala valoración

@bot.on_rating
def follow_up(ctx):
    rating = ctx.event.rating or {}
    if rating.get("score", 5) <= 2:
        ctx.client.tickets.create(
            email=ctx.event.visitor.email,
            message=f"Low CSAT ({rating['score']}/5): {rating.get('comment', '')}",
            priority="high",
        )

Sincronización nocturna de contactos

for user in your_database.active_users():
    client.contacts.upsert(
        user.email, name=user.name, company=user.company,
        custom_fields={"plan": user.plan, "mrr": str(user.mrr)},
    )

Una tienda que no conocemos

@plugin.action("catalog.search_products",
               description="Search the catalog by keyword.",
               parameters=[parameter("query", required=True)])
def search(req):
    hits = your_catalog.search(req.get("query"))
    return req.ok(products=[to_conecto(p) for p in hits]) if hits else req.not_found()

Esa es toda la integración. Las tarjetas de producto, la selección Inicio y la disciplina que impide al modelo pegar URL sin procesar vienen incluidas con el nombre reservado.

¿Está creando algo? Habla con nosotros , queremos que los desarrolladores creen sobre Conecto y damos prioridad a sus peticiones. El SDK es de código abierto con licencia MIT; se aceptan issues y pull requests en GitHub.