Développeurs / Python

Le SDK Python

Lire et répondre aux chats, envoyer des images et vidéos, créer des bots et permettre à l’agent IA de consulter vos propres systèmes, en Python. Tout ce que fait l’API REST, avec les parties délicates déjà gérées: vérifier qu’une requête vient réellement de nous, ne pas répondre deux fois au même événement, réessayer en toute sécurité et intercepter un message mal formé avant qu’il ne quitte votre processus.

Vous découvrez Conecto ? Commencer ici , cinq minutes de vocabulaire, puis le reste de cette page devient évident.

pip install conectov0.1.0Python 3.9+MIT

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

Commencer ici

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Si vous n’avez jamais utilisé Conecto, cette section présente tout le modèle mental. Cinq minutes ici rendent le reste de la page évident.

Ce qu’est Conecto

Un widget de chat présent sur votre site web. Les visiteurs y écrivent; votre équipe, un agent IA ou votre propre code répond. Tout ce qui suit permet d’insérer votre code votre code dans cette boucle.

Les cinq noms

Presque toutes les méthodes de ce SDK acceptent ou renvoient l’un de ces objets.

Espace de travail

Votre compte. Votre identifiant appartient exactement à un compte et ne peut jamais en voir un autre.

Widget

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

Visiteur

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

Conversation

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

Message

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

Les trois éléments que vous pouvez créer

1. Un script

S’exécute selon votre calendrier. Synchronise les contacts, ouvre des tickets et envoie des messages de campagne. Aucun hébergement requis, le script appelle simplement l’API.

2. Un bot

Un de vos serveurs web que nous prévenons lorsqu’un visiteur écrit. Vous choisissez la réponse. Vous contrôler la conversation.

3. Un plugin

Un de vos serveurs web que nous appelons lorsque le IA a besoin d’une information que vous seul détenez. L’IA contrôle la conversation; vous fournissez les recherches.

Bot ou plugin? Si vous voulez écrire les mots lus par le visiteur, créez un bot. Si vous préférez laisser l’IA parler et lui donner seulement accès à vos données, comme les commandes, abonnements ou stocks, créez un plugin. La plupart des équipes finissent par choisir un plugin.

Termes utilisés sur cette page

Jargon défini une seule fois. Seuls les deux derniers termes sont propres à Conecto.

WebhookNous envoyons une requête HTTP à votre URL lorsqu’un événement se produit, au lieu de vous laisser nous interroger sans cesse. Vous avez besoin d’un serveur accessible depuis Internet.
SignatureUn hash que nous joignons à chaque requête, calculé avec un secret connu seulement de vous et de nous. Sa vérification prouve que la requête vient réellement de nous et n’a pas été modifiée. Le SDK s’en charge pour vous.
Au moins une foisSi nous ne pouvons pas déterminer si vous avez reçu un élément, nous le renvoyons. Vous pouvez donc parfois recevoir le même événement deux fois; votre code doit le remarquer plutôt que d’agir deux fois.
IdempotentUne double exécution produit le même résultat qu’une exécution unique. Envoyez deux fois un message avec la même clé et le second appel renvoie le premier message au lieu de publier un doublon.
Pagination par curseurLes longues listes arrivent par pages. Au lieu de «page 3», transmettez l’identifiant reçu en dernier. Le SDK masque ce détail; il vous suffit d’itérer.
BlocUn contenu enrichi dans un message, comme une image, une vidéo, une carte ou une rangée de boutons. Voir Contenu enrichi.
Visiteur vérifiéUne personne dont nous avons prouvée , soit en lui envoyant un code par e-mail, soit parce que votre site nous a indiqué qu’elle était connectée. Toute opération touchant aux données privées d’une personne attend cette vérification.

De quelle section ai-je besoin?

Je veux…Allez dans
Lire ou répondre aux chats depuis un scriptConversations
Envoyer une image, un GIF, une vidéo ou une carteContenu enrichi
Répondre automatiquement aux visiteurs avec ma propre logiqueCréer un bot
Permettre à l’IA de consulter mes systèmesCréer un plugin
Connecter une boutique que l’IA peut rechercherCréer un plugin
Éviter le code par e-mail pour les utilisateurs déjà connectésIdentité
Maintenir mon CRM synchroniséContacts et visiteurs
Savoir quoi intercepter en cas d’échecErreurs et nouvelles tentatives

Installer et authentifier

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok
pip install conecto

Créer un identifiant dans Paramètres → Développeurs. Vous obtenez un identifiant client (ck_…) and a secret (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.

Démarrage rapide

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Trois actions possibles dans les cinq premières minutes.

Lire ce qui se passe

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

Répondre à quelqu’un

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

Envoyer autre chose que du texte

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"),
    ]),
])

Voilà la structure de toute la bibliothèque: un client avec des ressources, des objets capables d’agir sur eux-mêmes et des builders pour tout contenu structuré.

Le client

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Créez un client et conservez-le. Il regroupe les connexions et peut être partagé entre les threads en toute sécurité.

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
)

Environnement

VariableDéfinit
CONECTO_CLIENT_IDL’identifiant client.
CONECTO_SECRETLe secret.
CONECTO_BASE_URLURL de base de l’API. Rarement nécessaire.

Ressources

AttributCouvre
client.conversationsLister, lire, répondre, indicateur de saisie, transférer, attribuer, fermer.
client.contactsLe CRM: upsert par e-mail, recherche, mise à jour, suppression.
client.visitorsSessions, attestation d’identité et messages proactifs.
client.ticketsCréer, mettre à jour, répondre, catégories.
client.articlesRechercher et publier le contenu du centre d’aide.
client.integrationsEnregistrer, installer, exécuter et déployer des plugins.
client.widgetsLire et mettre à jour la configuration du widget.
client.mediaImporter des fichiers à utiliser dans les blocs.
client.webhooksAbonner des points de terminaison aux événements.
client.membersMembres de l’équipe pour l’attribution.
client.metame(), schema(), stats().

Dérive de version

Chaque réponse contient la version de l’API du serveur. Consignez-la au démarrage. Si elle évolue et qu’un champ utilisé disparaît, c’est la première information dont vous aurez besoin.

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() renvoie toute l’API sous forme de JSON, avec points de terminaison, événements, types de blocs, actions réservées, codes d’erreur et toutes les limites. Le même code applique les limites et fournit la réponse; celle-ci ne peut donc pas s’écarter de la réalité.

Conversations

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

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

Lister et lire

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)

Répondre

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

Un clic sur un bouton de réponse rapide revient comme un message ordinaire du visiteur contenant ce texte; votre gestionnaire n’a donc besoin d’aucun cas particulier.

ArgumentFonction
bodyLe texte, jusqu’à 4000 caractères.
blocksContenu enrichi, voir Contenu enrichi.
buttonsJusqu’à 6 réponses rapides.
productsJusqu’à 4 cartes produit, identiques à celles jointes par l’IA intégrée.
ask_emailAffiche le formulaire natif de collecte d’e-mail.
ticket_formJoint le formulaire natif d’ouverture de ticket.
internalUne note réservée aux agents. Le visiteur ne la voit jamais.
idempotency_keyTransmettez l’identifiant de livraison du webhook afin qu’une nouvelle tentative ne publie pas deux fois.

Pilotage du fil

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 Erreurs.

Contenu enrichi

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Tout message peut contenir jusqu’à dix blocs: images et GIF, vidéo, audio, fichiers, intégrations de tiers, carrousels de cartes, listes libellé/valeur, boutons et séparateurs. Ils s’affichent dans le widget et dans la boîte de réception de l’agent.

Les blocs sont de simples dicts lors du transport et pourraient être écrits à la main. Les builders existent parce qu’une clé mal orthographiée devient un client vous signalant que l’image n’est jamais apparue. Ils appliquent les mêmes règles que le serveur tant que la trace pointe encore vers votre code.

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")]),
    ]),
])

Chaque type de bloc

BuilderNotes
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, Spotify. Transmettez le lien de partage habituel.
blocks.audio(url, title).mp3, .wav, .m4a
blocks.file(url, filename, size)Une ligne de téléchargement.
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 et link_button.
blocks.text(body)Un paragraphe supplémentaire sous les autres blocs.
blocks.divider()Une ligne horizontale.

Deux types de boutons

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

Donnez une valeur explicite à un bouton de réponse lorsque le libellé doit rester lisible, mais que le texte traité par votre bot ne doit pas changer à chaque réécriture du contenu.

Envoyer 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)

Règles appliquées par les builders

  • Every URL must be https. The widget runs on an https page, so anything else is blocked by the browser anyway.
  • Dix blocs par message, dix cartes, douze lignes de liste, six boutons.
  • 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 validation s’effectue localement. 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.

Importation de fichiers

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Aucune URL publique pour ce GIF, ce reçu ou cette fiche technique? Importez le fichier, jusqu’à 10 Mo, puis utilisez le résultat.

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 ou blocks.file.

Use media.url, not media.absolute_url. url est un chemin que le widget résout par rapport à sa propre origine. Le même message fonctionne ainsi en développement, en préproduction et en production, même après un changement de domaine.

Contacts et visiteurs

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Contacts

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 est fusionné, et non remplacées. Deux tâches peuvent chacune posséder leurs propres clés sans écraser celles de l’autre. Jusqu’à 30 clés par appel.

Visiteurs

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

Messages proactifs

Envoyer à une session sans attendre une demande, par exemple mises à jour d’expédition, rappels d’essai ou récupération de panier.

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"),
    ])],
)

Réutilise la conversation active de la session ou en ouvre une. Un widget ouvert l’affiche à la prochaine interrogation; un widget fermé l’affiche à sa prochaine ouverture, comme élément non lu.

Tickets et articles

Besoin d’aide? Interrogez une IA sur cette section: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

Une réponse publique rouvre un ticket résolu, car une réponse à un élément marqué comme terminé signifie presque toujours qu’un nouvel échange commence.

Articles du centre d’aide

Utile dans les deux sens: un bot peut y chercher avant d’ouvrir un ticket, et vous pouvez y pousser des documents depuis votre outil de rédaction.

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)

Le HTML est nettoyé côté serveur selon une liste d’autorisation de balises sûres, au moyen du même pipeline que l’éditeur du tableau de bord. Les scripts et styles sont retirés avant tout enregistrement.

Créer un bot

Besoin d’aide? Interrogez une IA sur cette section: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()

Mise à disposition

# 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()

Abonnez ensuite le point de terminaison:

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

Gestionnaires

DécorateurSe déclenche sur
@bot.on_messageUn message d’un visiteur. Les messages des bots et des agents ne lui parviennent jamais, ce qui empêche un bot de se répondre à lui-même.
@bot.on_conversation_startedconversation.created , accueillir ou initialiser un état.
@bot.on_ratingconversation.rated. ctx.event.rating has score et 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 gestionnaire a levé une erreur. Sans gestionnaire, les erreurs sont consignées puis ignorées.

Ce que reçoit un gestionnaire

Dans le contexteEst
ctx.textLe texte du message du visiteur.
ctx.verified · ctx.verified_emailSi l’identité est prouvée et quelle adresse a été prouvée.
ctx.conversation_id · ctx.widget_id · ctx.sessionIdentifiants sur lesquels vous pouvez agir.
ctx.reply(...)Answer. Takes everything conversations.reply does.
ctx.note(...)Une note réservée aux agents.
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close()Piloter le fil.
ctx.history()Récupérer tout l’historique des messages. Une requête.
ctx.client · ctx.eventLe client bas niveau et l’événement analysé pour tout le reste.

Vous exécutez plusieurs workers? 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())

Créer un plugin

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Un bot répond aux webhooks. Un plugin est l’autre sens: vous déclarez ce que vos systèmes savent faire et l’agent IA décide quand les utiliser pendant une conversation. Vous n’écrivez jamais la logique de conversation, seulement la recherche.

Voilà comment une boutique, un système de facturation ou un CRM que nous ne connaissons pas devient une intégration de premier ordre.

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 et enregistrer

# 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() crée ou met à jour l’intégration pour qu’elle corresponde à votre code, supprime les actions retirées et l’installe sur vos widgets. Rien n’est exposé à l’IA avant l’installation.

Les cinq types de réponses

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.

Niveaux de risque

RisqueSignification
public_readCatalogue, disponibilité, documentation. Aucune identité requise.
verified_readLes données d’une personne. Refusées jusqu’à ce que son adresse e-mail soit prouvée.
public_writeUne modification ne nécessitant aucune identité, comme l’inscription à une newsletter ou la collecte d’un prospect.
writeUne modification du compte d’une personne. Vérifiée, auditée et jamais retentée silencieusement.

req.verified_email est la seule identité digne de confiance. An email in req.arguments is whatever the model parsed out of a chat. For verified_read et 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.

Actions réservées

Quelques noms ont une signification particulière pour la plateforme. Déclarez-en un et votre service se connecte à la fonctionnalité qui existe déjà pour lui.

NomCe que vous obtenez
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_statusStatut de commande, toujours soumis à vérification.
orders.get_trackingSuivi d’expédition, toujours vérifié.
orders.list_recentLes commandes récentes du visiteur, toujours vérifiées.
account.lookupTout enregistrement plat concernant le visiteur vérifié, comme l’offre, les crédits ou la date de renouvellement.

Tester avant qu’un visiteur ne le fasse

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

Cette opération exécute le réel 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.

Rédiger une bonne description

Le 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.

Faible: «Recherche de commande.»
Fort: «Rechercher le statut et la date de livraison de l’une des commandes du visiteur à partir de son numéro. À utiliser chaque fois qu’il demande où se trouve un article ou quand il arrivera.»

Ce que garantit la plateforme

  • 8 secondes délai d’expiration, 128 Ko limite de réponse.
  • Calls are signed with your signing_secret — same scheme as webhooks.
  • Votre URL est résolue une seule fois et épinglée à une adresse IP publique; les redirections sont refusées.
  • Tout ce que vous renvoyez arrive au modèle comme données, jamais comme instructions. Les clés qui ressemblent à des secrets sont supprimées.
  • Les intégrations personnalisées nécessitent une offre payante Agent IA. 20 par espace de travail, 40 actions chacune.

Identité

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Toute opération concernant les données d’une personne attend que nous sachions qui elle est. Cela se produit de deux façons: par un code envoyé par e-mail ou grâce à l’attestation par votre site pour un utilisateur déjà connecté.

# 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)

Pendant la durée de l’attestation, Conecto considère l’e-mail comme prouvé: le widget connaît le nom, les chats sont associés au bon contact et les parcours qui exigeraient autrement un code par e-mail, comme les recherches de commande, remboursements, données de compte ou actions vérifiées de votre plugin, se poursuivent sans code.

N’attestez que les sessions réellement authentifiées par votre backend. Votre attestation est considérée comme aussi forte que notre code envoyé par e-mail, car vous avez réellement authentifié la personne. Elle est limitée dans le temps et à la session; un nouveau navigateur nécessite donc une nouvelle attestation. Le navigateur ne peut jamais se marquer lui-même comme vérifié, car cet appel est authentifié et doit se faire sur votre serveur.

Webhooks et événements

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Bot prend tout cela en charge. Utilisez les helpers bas niveau lorsque vous souhaitez vérifier en périphérie et traiter dans une file d’attente.

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

Vérifier à partir du corps brut. 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.

Événements

conversation.createdUn visiteur a commencé une conversation.
message.createdUn visiteur a envoyé un message. Le déclencheur du bot.
conversation.closedUne conversation a été fermée.
conversation.assignedAttribué à un membre de l’équipe ou retiré de son attribution.
conversation.handoffSignalé comme nécessitant une intervention humaine.
conversation.ratedUne note CSAT est arrivée (1–5 plus un commentaire).
ticket.createdUn ticket a été ouvert depuis n’importe quelle source.
ticket.updatedLe statut, la priorité ou l’agent attribué à un ticket a changé.
contact.createdUne nouvelle personne est entrée dans le CRM.
visitor.identifiedUne session a été identifiée par l’API.

Sémantique des livraisons

Les livraisons sont au moins une fois et sans ordre garanti: 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 permet de refuser les livraisons obsolètes; le SDK le vérifie avec une tolérance de cinq minutes par défaut.
signature = webhooks.sign(SECRET, raw_body)      # what we send
webhooks.verify(raw_body, header_value, SECRET)  # True / False, never raises

Appels d’intégration que nous effectuons à your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.

Erreurs et nouvelles tentatives

Besoin d’aide? Interrogez une IA sur cette section: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")
ExceptionStatutSignification
AuthenticationError401Identifiant absent, inconnu ou révoqué.
InvalidRequestError400Le corps n’a pas passé la validation; le message en précise la raison.
NotFoundError404Objet introuvable dans cet espace de travail.
ConflictError409Cet identifiant est déjà utilisé.
ConversationClosed409Reopen the thread first. A subclass of ConflictError.
LimitReached403Une limite d’offre ou d’espace de travail.
PlanRequired403L’offre n’inclut pas cette fonctionnalité.
RateLimited429Over 300 requests/minute. Carries retry_after.
ServiceUnavailable503Une dépendance n’est pas configurée.
ServerError5xxNotre erreur. Une nouvelle tentative est sûre.
BlockErrorLevée localement par un builder de blocs, avant toute requête.
SignatureError401La vérification d’un webhook ou d’un appel d’intégration a échoué.
APIConnectionError · APITimeoutErrorLa requête n’a jamais reçu de réponse.

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

Nouvelles tentatives

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.

Les écritures font également l’objet de nouvelles tentatives, ce qui serait normalement dangereux. Ici, c’est sûr, car chaque écriture porte une clé d’idempotence et chaque tentative réutilise la même. Le serveur reconnaît le second essai comme le même appel et ne le répète pas.

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

Limites à connaître

LimiteValeur
Requêtes par minute et par identifiant300
Corps du message4000 caractères
Blocs par message10
Boutons de réponse rapide6
Importation de médias10 Mo
Intégrations par espace de travail20
Actions par intégration40
Webhooks par espace de travail10

client.meta.schema()["limits"] contient toujours les chiffres en direct.

Pagination

Besoin d’aide? Interrogez une IA sur cette section: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.

Test en cours

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete et 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/"

Pour les bots et plugins, ignorez la vérification de signature dans les tests plutôt que de signer les fixtures:

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

Référence des méthodes

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Conversations

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

Conversations, activité la plus récente en premier. Renvoie une Page.

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

Une conversation avec ses messages. since_id ne renvoie que les messages plus récents.

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

Uniquement les messages.

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

Envoyer un message de bot. Alias: send().

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

Afficher ou masquer l’indicateur de saisie.

client.conversations.handoff(id)

Signaler qu’une intervention humaine est nécessaire.

client.conversations.assign(id, user_id)

None retire l’attribution.

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

Modifier le statut.

Contacts

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

find() renvoie un contact ou None.

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

Créer ou mettre à jour par e-mail. Alias: create().

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

Visiteurs

client.visitors.get(widget_id, session)

Identité et état de vérification.

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

Joindre votre utilisateur; verified=True atteste son identité.

client.visitors.unverify(widget_id, session)

Révoquer l’attestation. Appeler lors de la déconnexion.

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

Un message proactif.

Intégrations

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

actions() renvoie aussi le catalogue des actions réservées.

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

En enregistrer un. La réponse contient signing_secret.

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

replace_actions rend votre liste définitive.

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

Créer ou mettre à jour, puis installer. Idempotent, donc adapté à votre script de déploiement.

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

actions est une liste d’autorisation; [] n’expose rien.

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

Appeler par le véritable chemin d’appel.

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

Tickets, articles, widgets, médias, webhooks, métadonnées

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

La configuration complète se trouve dans widget.raw.

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

Renvoie un objet Media transmissible à un builder de blocs.

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

Les noms d’événements inconnus sont refusés localement.

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

Modèles

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

Recettes

Besoin d’aide? Interrogez une IA sur cette section:ClaudeChatGPTGrok

Éviter un ticket grâce à votre centre d’aide

@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)

Qualifier un prospect et le router

@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()

Récupérer une mauvaise note

@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",
        )

Synchronisation nocturne des contacts

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)},
    )

Une boutique que nous ne connaissons pas

@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()

Voilà toute l’intégration. Les cartes produit, la sélection Accueil et la discipline qui empêche le modèle de coller des URL brutes accompagnent automatiquement le nom réservé.

Vous construisez quelque chose? Parlez-nous , nous voulons que les développeurs construisent sur Conecto et nous donnons la priorité à leurs demandes. Le SDK est open source sous licence MIT; les issues et pull requests sont les bienvenues sur GitHub.