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
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.
De quelle section ai-je besoin?
| Je veux… | Allez dans |
|---|---|
| Lire ou répondre aux chats depuis un script | Conversations |
| Envoyer une image, un GIF, une vidéo ou une carte | Contenu enrichi |
| Répondre automatiquement aux visiteurs avec ma propre logique | Créer un bot |
| Permettre à l’IA de consulter mes systèmes | Créer un plugin |
| Connecter une boutique que l’IA peut rechercher | Créer un plugin |
| Éviter le code par e-mail pour les utilisateurs déjà connectés | Identité |
| Maintenir mon CRM synchronisé | Contacts et visiteurs |
| Savoir quoi intercepter en cas d’échec | Erreurs et nouvelles tentatives |
Installer et authentifier
pip install conectoCré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_secretfrom 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
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
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
| Variable | Définit |
|---|---|
CONECTO_CLIENT_ID | L’identifiant client. |
CONECTO_SECRET | Le secret. |
CONECTO_BASE_URL | URL de base de l’API. Rarement nécessaire. |
Ressources
| Attribut | Couvre |
|---|---|
client.conversations | Lister, lire, répondre, indicateur de saisie, transférer, attribuer, fermer. |
client.contacts | Le CRM: upsert par e-mail, recherche, mise à jour, suppression. |
client.visitors | Sessions, attestation d’identité et messages proactifs. |
client.tickets | Créer, mettre à jour, répondre, catégories. |
client.articles | Rechercher et publier le contenu du centre d’aide. |
client.integrations | Enregistrer, installer, exécuter et déployer des plugins. |
client.widgets | Lire et mettre à jour la configuration du widget. |
client.media | Importer des fichiers à utiliser dans les blocs. |
client.webhooks | Abonner des points de terminaison aux événements. |
client.members | Membres de l’équipe pour l’attribution. |
client.meta | me(), 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"] >= 4client.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
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.
| Argument | Fonction |
|---|---|
body | Le texte, jusqu’à 4000 caractères. |
blocks | Contenu enrichi, voir Contenu enrichi. |
buttons | Jusqu’à 6 réponses rapides. |
products | Jusqu’à 4 cartes produit, identiques à celles jointes par l’IA intégrée. |
ask_email | Affiche le formulaire natif de collecte d’e-mail. |
ticket_form | Joint le formulaire natif d’ouverture de ticket. |
internal | Une note réservée aux agents. Le visiteur ne la voit jamais. |
idempotency_key | Transmettez 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
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
| Builder | Notes |
|---|---|
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 tabDonnez 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
videoblock raises, and tells you to useembed.
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
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
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.verifiedMessages 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
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 noteUne 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
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 deliveriesGestionnaires
| Décorateur | Se déclenche sur |
|---|---|
@bot.on_message | Un 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_started | conversation.created , accueillir ou initialiser un état. |
@bot.on_rating | conversation.rated. ctx.event.rating has score et comment. |
@bot.on_ticket · @bot.on_contact | ticket.created · contact.created. |
@bot.on("event.name") | Any event by name. @bot.on("*") catches everything. |
@bot.on_error | Un gestionnaire a levé une erreur. Sans gestionnaire, les erreurs sont consignées puis ignorées. |
Ce que reçoit un gestionnaire
| Dans le contexte | Est |
|---|---|
ctx.text | Le texte du message du visiteur. |
ctx.verified · ctx.verified_email | Si l’identité est prouvée et quelle adresse a été prouvée. |
ctx.conversation_id · ctx.widget_id · ctx.session | Identifiants 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.event | Le 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
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 environmentdeploy() 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
| Risque | Signification |
|---|---|
public_read | Catalogue, disponibilité, documentation. Aucune identité requise. |
verified_read | Les données d’une personne. Refusées jusqu’à ce que son adresse e-mail soit prouvée. |
public_write | Une modification ne nécessitant aucune identité, comme l’inscription à une newsletter ou la collecte d’un prospect. |
write | Une 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.
| Nom | Ce que vous obtenez |
|---|---|
catalog.search_products | Results 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_status | Statut de commande, toujours soumis à vérification. |
orders.get_tracking | Suivi d’expédition, toujours vérifié. |
orders.list_recent | Les commandes récentes du visiteur, toujours vérifiées. |
account.lookup | Tout 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é
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
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.verifiedVé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 inX-Conecto-Delivery). Store it and skip ids you have already handled. - Pass that same id as
idempotency_keywhen you reply, and a redelivery can never make your bot speak twice. X-Conecto-Timestamppermet 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 raisesAppels 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
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")| Exception | Statut | Signification |
|---|---|---|
AuthenticationError | 401 | Identifiant absent, inconnu ou révoqué. |
InvalidRequestError | 400 | Le corps n’a pas passé la validation; le message en précise la raison. |
NotFoundError | 404 | Objet introuvable dans cet espace de travail. |
ConflictError | 409 | Cet identifiant est déjà utilisé. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | Une limite d’offre ou d’espace de travail. |
PlanRequired | 403 | L’offre n’inclut pas cette fonctionnalité. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | Une dépendance n’est pas configurée. |
ServerError | 5xx | Notre erreur. Une nouvelle tentative est sûre. |
BlockError | — | Levée localement par un builder de blocs, avant toute requête. |
SignatureError | 401 | La vérification d’un webhook ou d’un appel d’intégration a échoué. |
APIConnectionError · APITimeoutError | — | La 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 entirelyLimites à connaître
| Limite | Valeur |
|---|---|
| Requêtes par minute et par identifiant | 300 |
| Corps du message | 4000 caractères |
| Blocs par message | 10 |
| Boutons de réponse rapide | 6 |
| Importation de médias | 10 Mo |
| Intégrations par espace de travail | 20 |
| Actions par intégration | 40 |
| Webhooks par espace de travail | 10 |
client.meta.schema()["limits"] contient toujours les chiffres en direct.
Pagination
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 listIterating 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
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
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 · categoriesclient.articles.list · get · create · update · deleteclient.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 sentRecettes
É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.