Sviluppatori / Python
Il SDK Python
Leggi e rispondi alle chat, invia immagini e video, crea bot e consenti all’agente IA di consultare i tuoi sistemi tramite Python. Tutto ciò che fa l’API REST, con gli aspetti più delicati già gestiti: verificare che una richiesta provenga davvero da noi, non rispondere due volte allo stesso evento, riprovare in sicurezza e intercettare un messaggio non valido prima che lasci il processo.
Non usi ancora Conecto? Inizia qui , cinque minuti di terminologia e il resto della pagina diventa intuitivo.
pip install conectov0.1.0Python 3.9+MIT One dependency (requests) · Type hints throughout · Su PyPI
Inizia qui
Se non hai mai usato Conecto, questa sezione contiene l’intero modello mentale. Cinque minuti qui rendono evidente il resto della pagina.
Cos’è Conecto
Un widget di chat sul tuo sito. I visitatori scrivono al suo interno; risponde il tuo team, un agente IA o il tuo codice. Tutto ciò che segue è un modo per inserire il tuo codice in quel ciclo.
I cinque sostantivi
Quasi tutti i metodi di questo SDK accettano o restituiscono uno di questi elementi.
Spazio di lavoro
Il tuo account. La credenziale appartiene esattamente a un account e non può mai vederne un altro.
Widget
One installed chat box. You might have one per website or brand. Has an id.
Visitatore
A browser on your site, identified by a session string stored in that browser.
Conversazione
One thread between a visitor and you. Has an id, and a status of open or closed.
Messaggio
One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.
Le tre cose che puoi creare
1. Uno script
Viene eseguito secondo la tua pianificazione. Sincronizza i contatti, apre ticket e invia messaggi delle campagne. Non devi ospitare nulla; chiama semplicemente l’API.
2. Un bot
Un tuo server Web che avvisiamo quando un visitatore scrive. Sei tu a decidere la risposta. Tu controllare la conversazione.
3. Un plugin
Un tuo server Web che chiamiamo quando il IA richiede un dato che solo tu possiedi. L’IA controlla la conversazione; tu fornisci le ricerche.
Bot o plugin? Se vuoi scrivere le parole che il visitatore legge, crea un bot. Se preferisci lasciare parlare l’IA e darle soltanto accesso ai tuoi dati, come ordini, abbonamenti e disponibilità, crea un plugin. La maggior parte dei team finisce per scegliere un plugin.
Termini usati in questa pagina
Terminologia definita una sola volta. Nulla qui è specifico di Conecto, tranne gli ultimi due termini.
Quale sezione mi serve?
| Voglio… | Vai su |
|---|---|
| Leggere o rispondere alle chat da uno script | Conversazioni |
| Inviare un’immagine, una GIF, un video o una scheda | Contenuti avanzati |
| Rispondere automaticamente ai visitatori con la mia logica | Creare un bot |
| Consentire all’IA di consultare i miei sistemi | Creare un plugin |
| Collegare un negozio in cui l’IA possa cercare | Creare un plugin |
| Saltare il codice e-mail per gli utenti che hanno già effettuato l’accesso | Identità |
| Mantenere sincronizzato il mio CRM | Contatti e visitatori |
| Sapere cosa intercettare quando qualcosa non funziona | Errori e nuovi tentativi |
Installazione e autenticazione
pip install conectoCrea una credenziale in Impostazioni → Sviluppatori. Ottieni un ID client (ck_…) and a segreto (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.
Avvio rapido
Tre cose che puoi fare nei primi cinque minuti.
Leggere ciò che sta accadendo
for convo in client.conversations.list(status="open"):
who = convo.visitor.email if convo.visitor else "anonymous"
print(f"#{convo.id} {who} {convo.preview}")Rispondere a qualcuno
convo = client.conversations.get(4821)
print(convo.messages[-1].body) # what they said last
convo.reply("On it — give me one moment.")Inviare contenuti diversi dal testo
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"),
]),
])Questa è la struttura dell’intera libreria: un client con risorse, oggetti capaci di agire su se stessi e builder per qualsiasi contenuto strutturato.
Il client
Creane uno e conservalo. Riutilizza le connessioni e può essere condiviso in sicurezza tra thread.
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
)Ambiente
| Variabile | Imposta |
|---|---|
CONECTO_CLIENT_ID | L’ID client. |
CONECTO_SECRET | Il secret. |
CONECTO_BASE_URL | URL di base dell’API. Serve raramente. |
Risorse
| Attributo | Comprende |
|---|---|
client.conversations | Elencare, leggere, rispondere, indicare la digitazione, trasferire, assegnare e chiudere. |
client.contacts | Il CRM: crea o aggiorna per e-mail, cerca, aggiorna ed elimina. |
client.visitors | Sessioni, garanzia dell’identità e messaggi proattivi. |
client.tickets | Creare, aggiornare, rispondere e gestire le categorie. |
client.articles | Cercare e pubblicare contenuti del centro assistenza. |
client.integrations | Registrare, installare, eseguire e distribuire plugin. |
client.widgets | Leggere e aggiornare la configurazione del widget. |
client.media | Caricare file da usare nei blocchi. |
client.webhooks | Iscrivere gli endpoint agli eventi. |
client.members | Membri del team, per l’assegnazione. |
client.meta | me(), schema(), stats(). |
Disallineamento delle versioni
Ogni risposta contiene la versione dell’API del server. Registrala all’avvio. Se cambia e scompare un campo che usi, sarà la prima informazione che vorrai conoscere.
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() restituisce l’intera API in formato JSON, inclusi endpoint, eventi, tipi di blocco, azioni riservate, codici di errore e ogni limite. È fornita dallo stesso codice che applica queste regole, quindi non può discostarsi dalla realtà.
Conversazioni
A thread between a visitor and your workspace. Messages have a sender di visitor, agent, bot oppure system.
Elenco e lettura
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)Risposte
convo.reply("I can refund order #1284 — confirm?",
buttons=["Yes, refund it", "Talk to a human"])Il tocco su un pulsante di risposta rapida torna come un normale messaggio del visitatore contenente quel testo, quindi l’handler non richiede un caso speciale.
| Argomento | Funzione |
|---|---|
body | Il testo, fino a 4000 caratteri. |
blocks | Rich content, vedi Contenuti avanzati. |
buttons | Fino a 6 risposte rapide. |
products | Fino a 4 schede prodotto, come quelle allegate dall’IA integrata. |
ask_email | Visualizza il modulo nativo per la raccolta dell’e-mail. |
ticket_form | Allega il modulo nativo per aprire un ticket. |
internal | Una nota visibile solo agli operatori. Il visitatore non la vede mai. |
idempotency_key | Passa l’ID di una consegna webhook per evitare che un nuovo tentativo pubblichi due volte. |
Gestione del thread
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 Errori.
Contenuti avanzati
Ogni messaggio può contenere fino a dieci blocchi: immagini e GIF, video, audio, file, contenuti incorporati di terze parti, caroselli di schede, elenchi etichetta/valore, pulsanti e divisori. Vengono visualizzati nel widget e nella posta in arrivo dell’operatore.
Durante la trasmissione, i blocchi sono semplici dizionari e potresti scriverli a mano. I builder esistono perché una chiave scritta male porta un cliente a segnalare che l’immagine non è mai comparsa. Applicano le stesse regole del server nel punto in cui il traceback indica ancora il tuo codice.
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")]),
]),
])Tutti i tipi di blocco
| Builder | Nota |
|---|---|
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 e Spotify. Passa il normale link di condivisione. |
blocks.audio(url, title) | .mp3, .wav, .m4a… |
blocks.file(url, filename, size) | Una riga di download. |
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 e link_button. |
blocks.text(body) | Un paragrafo aggiuntivo sotto gli altri blocchi. |
blocks.divider() | Una linea orizzontale. |
Due tipi di pulsante
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 tabAssegna un valore esplicito a un pulsante di risposta quando l’etichetta deve essere chiara, ma il testo riconosciuto dal bot non deve cambiare ogni volta che qualcuno modifica il copy.
Inviare una 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)Regole applicate dai builder
- Every URL must be
https. The widget runs on an https page, so anything else is blocked by the browser anyway. - Dieci blocchi per messaggio, dieci schede, dodici righe di elenco e sei pulsanti.
- Long strings are truncated, not rejected. Structural mistakes raise
BlockError. - A YouTube link in a
videoblock raises, and tells you to useembed.
La convalida avviene 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.
Caricamento dei file
Nessun URL pubblico per quella GIF, ricevuta o scheda tecnica? Caricala, fino a 10 MB, e usa il risultato.
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 oppure blocks.file.
Use media.url, not media.absolute_url. url è un percorso risolto dal widget rispetto alla propria origine, quindi lo stesso messaggio funziona in sviluppo, in un deployment di anteprima e in produzione, anche quando il dominio cambia.
Contatti e visitatori
Contatti
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 è unite, non sostituite. Due processi possono gestire le proprie chiavi senza sovrascriversi. Fino a 30 chiavi per chiamata.
Visitatori
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.verifiedMessaggi proattivi
Invia a una sessione senza aspettare una richiesta, ad esempio aggiornamenti di spedizione, promemoria sul periodo di prova o recupero del carrello.
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"),
])],
)Riutilizza la conversazione attiva della sessione o ne apre una. Un widget aperto la mostra al polling successivo; un widget chiuso la mostra alla riapertura, contrassegnata come non letta.
Ticket e articoli
Ticket
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 noteUna risposta pubblica riapre un ticket risolto, perché rispondere a qualcosa segnato come concluso avvia quasi sempre un nuovo scambio.
Articoli del centro assistenza
Utile in entrambe le direzioni: cercalo da un bot prima di aprire un ticket e invia la documentazione dallo strumento in cui la crei.
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)L’HTML viene ripulito lato server in base a una lista di tag sicuri, tramite lo stesso processo usato dall’editor della dashboard. Script e stili vengono rimossi prima di archiviare qualsiasi contenuto.
Creare 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()Pubblicazione
# 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()Quindi iscrivi l’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 deliveriesHandler
| Decorator | Si attiva con |
|---|---|
@bot.on_message | Un messaggio da un visitatore. I messaggi di bot e operatori non lo raggiungono mai, impedendo così a un bot di rispondere a se stesso. |
@bot.on_conversation_started | conversation.created , salutare o impostare lo stato. |
@bot.on_rating | conversation.rated. ctx.event.rating has score e 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 handler ha generato un’eccezione. Se non ne definisci uno, gli errori vengono registrati e ignorati. |
Cosa riceve un handler
| In ctx | È |
|---|---|
ctx.text | Il testo del messaggio del visitatore. |
ctx.verified · ctx.verified_email | Se l’identità è verificata e qual è l’indirizzo verificato. |
ctx.conversation_id · ctx.widget_id · ctx.session | ID su cui puoi intervenire. |
ctx.reply(...) | Answer. Takes everything conversations.reply does. |
ctx.note(...) | Una nota visibile solo agli operatori. |
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close() | Guidare il thread. |
ctx.history() | Recupera l’intera cronologia dei messaggi. Una sola richiesta. |
ctx.client · ctx.event | Il client di basso livello e l’evento elaborato, per tutto il resto. |
Usi più di 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())Creare un plugin
Un bot risponde ai webhook. Un plugin funziona nella direzione opposta: dichiari cosa possono fare i tuoi sistemi e l’agente IA decide quando usarli mentre parla con qualcuno. Non scrivi mai la logica della conversazione, ma soltanto la ricerca.
È così che un negozio, un sistema di fatturazione o un CRM che non conosciamo diventa un’integrazione di prima classe.
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()Pubblicare e registrare
# 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() crea o aggiorna l’integrazione in modo che corrisponda al codice, elimina le azioni rimosse e la installa sui widget. Nulla viene esposto all’IA finché non è installata.
Le cinque risposte
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.
Livelli di rischio
| Rischio | Significa |
|---|---|
public_read | Catalogo, disponibilità e documentazione. Non serve alcuna identità. |
verified_read | I dati di una persona. Rifiutati finché il suo indirizzo e-mail non viene verificato. |
public_write | Una modifica che non richiede identità, come l’iscrizione alla newsletter o l’acquisizione di un lead. |
write | Una modifica all’account di una persona. Verificata, registrata e mai ripetuta in modo invisibile. |
req.verified_email è l’unica identità attendibile. An email in req.arguments is whatever the model parsed out of a chat. For verified_read e 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.
Azioni riservate
Alcuni nomi hanno un significato specifico per la piattaforma. Dichiarandone uno, il servizio si collega alla funzionalità già esistente per quel nome.
| Nome | Cosa ottieni |
|---|---|
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 | Stato dell’ordine, sempre soggetto a verifica. |
orders.get_tracking | Tracciamento della spedizione, sempre verificato. |
orders.list_recent | Gli ordini recenti del visitatore, sempre verificati. |
account.lookup | Qualsiasi record semplice sul visitatore verificato, come piano, crediti o data di rinnovo. |
Provalo prima che lo faccia un visitatore
client.integrations.run("acme-store", "catalog.search_products",
{"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}Questo esegue il reale 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.
Scrivere una buona descrizione
Il 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.
Debole: «Ricerca ordine.»
Forte: «Cerca lo stato e la data di consegna di uno degli ordini del visitatore tramite il numero d’ordine. Usa questa azione ogni volta che chiede dove si trova qualcosa o quando arriverà.»
Cosa garantisce la piattaforma
- 8 secondi timeout, 128 KB limite della risposta.
- Calls are signed with your
signing_secret— same scheme as webhooks. - Il tuo URL viene risolto una volta e associato a un IP pubblico; i reindirizzamenti vengono rifiutati.
- Tutto ciò che restituisci arriva al modello come dati, mai come istruzioni, e le chiavi che sembrano contenere secret vengono rimosse.
- Le integrazioni personalizzate richiedono un piano a pagamento con Agente IA. Sono consentite 20 integrazioni per spazio di lavoro e 40 azioni ciascuna.
Identità
Qualsiasi operazione sui dati di una persona attende finché non sappiamo chi è. Ciò avviene in uno di due modi: tramite un codice inviato via e-mail oppure mediante la garanzia del tuo sito per un utente che ha già effettuato l’accesso.
# 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)Finché la garanzia è valida, Conecto considera verificato l’indirizzo e-mail: il widget conosce il nome della persona, le chat vengono collegate al contatto corretto e i flussi che altrimenti richiederebbero un codice via e-mail, come ricerche di ordini, rimborsi, dati dell’account o azioni verificate del plugin, proseguono senza codice.
Garantisci solo le sessioni realmente autenticate dal tuo backend. La tua attestazione è considerata solida quanto il nostro codice via e-mail, perché hai davvero autenticato la persona. Ha durata limitata ed è legata alla sessione, quindi un nuovo browser richiede una nuova garanzia. Il browser non può mai contrassegnarsi come verificato; quella chiamata è autenticata e deve avvenire sul tuo server.
Webhook ed eventi
Bot fa tutto questo per te. Usa gli helper di basso livello quando vuoi verificare al perimetro ed elaborare in una coda.
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.verifiedVerificare rispetto al corpo originale. 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.
Eventi
conversation.createdUn visitatore ha iniziato una conversazione.message.createdUn visitatore ha inviato un messaggio. Il trigger del bot.conversation.closedUna conversazione è stata chiusa.conversation.assignedAssegnato a un membro del team o rimosso da quest’ultimo.conversation.handoffContrassegnato come richiesta di intervento umano.conversation.ratedÈ arrivata una valutazione CSAT (da 1 a 5 con un commento).ticket.createdÈ stato aperto un ticket, da qualsiasi origine.ticket.updatedUn ticket ha cambiato stato, priorità o assegnatario.contact.createdUna nuova persona è entrata nel CRM.visitor.identifiedUna sessione è stata identificata tramite l’API.Semantica delle consegne
Le consegne avvengono almeno una volta e senza ordine garantito: 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-Timestampconsente di rifiutare le consegne obsolete; per impostazione predefinita, l’SDK le controlla con una tolleranza di cinque minuti.
signature = webhooks.sign(SECRET, raw_body) # what we send
webhooks.verify(raw_body, header_value, SECRET) # True / False, never raisesChiamate di integrazione che eseguiamo alle your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.
Errori e nuovi tentativi
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")| Eccezione | Stato | Significa |
|---|---|---|
AuthenticationError | 401 | Credenziale mancante, sconosciuta o revocata. |
InvalidRequestError | 400 | Il corpo non ha superato la convalida; il messaggio ne indica il motivo. |
NotFoundError | 404 | L’oggetto non esiste in questo spazio di lavoro. |
ConflictError | 409 | Questo identificatore è già in uso. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | Un limite del piano o dello spazio di lavoro. |
PlanRequired | 403 | Il piano non include questa funzionalità. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | Una dipendenza non è configurata. |
ServerError | 5xx | Errore nostro. Puoi riprovare in sicurezza. |
BlockError | — | Generata localmente da un builder di blocchi, prima di qualsiasi richiesta. |
SignatureError | 401 | La verifica di una chiamata webhook o di integrazione non è riuscita. |
APIConnectionError · APITimeoutError | — | La richiesta non ha mai ricevuto risposta. |
Every exception carries code, message, status and the raw response.
Nuovi tentativi
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.
Anche le scritture vengono ritentate, cosa normalmente pericolosa. Qui è sicuro perché ogni scrittura contiene una chiave di idempotenza e un nuovo tentativo riutilizza la stessa chiave. Il server riconosce il secondo tentativo come la stessa chiamata e non la ripete.
client = Conecto(max_retries=0) # opt out entirelyLimiti da conoscere
| Limite | Valore |
|---|---|
| Richieste al minuto per credenziale | 300 |
| Corpo del messaggio | 4000 caratteri |
| Blocchi per messaggio | 10 |
| Pulsanti di risposta rapida | 6 |
| Upload di contenuti multimediali | 10 MB |
| Integrazioni per spazio di lavoro | 20 |
| Azioni per integrazione | 40 |
| Webhook per spazio di lavoro | 10 |
client.meta.schema()["limits"] contiene sempre i valori aggiornati.
Paginazione
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 in corso
The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete e 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/"Per bot e plugin, disattiva la verifica della firma nei test invece di firmare le fixture:
bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)Riferimento dei metodi
Conversazioni
client.conversations.list(status, widget_id, session, limit, before_id)Conversazioni, con l’attività più recente per prima. Restituisce una Page.
client.conversations.get(id, since_id=None)Una conversazione con i relativi messaggi. since_id restituisce solo quelli più recenti.
client.conversations.messages(id, since_id=None)Solo i messaggi.
client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)Invia un messaggio del bot. Disponibile anche come send().
client.conversations.typing(id, on=True, name="Bot")Mostrare o cancellare l’indicatore di digitazione.
client.conversations.handoff(id)Contrassegnare come richiesta di intervento umano.
client.conversations.assign(id, user_id)None rimuove l’assegnazione.
client.conversations.close(id) · reopen(id) · set_status(id, status)Cambiare lo stato.
Contatti
client.contacts.list(email, query, limit, before_id) · get(id) · find(email)find() restituisce un contatto o None.
client.contacts.upsert(email, name, title, company, location, phone, notes, custom_fields)Crea o aggiorna per e-mail. Disponibile anche come create().
client.contacts.update(id, **fields) · delete(id)Visitatori
client.visitors.get(widget_id, session)Identità e stato di verifica.
client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)Collega il tuo utente; verified=True ne garantisce l’identità.
client.visitors.unverify(widget_id, session)Revoca la garanzia. Chiamalo al logout.
client.visitors.message(widget_id, session, body, blocks, buttons, products)Un messaggio proattivo.
Integrazioni
client.integrations.list() · get(slug) · actions(slug)actions() restituisce anche il catalogo delle azioni riservate.
client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)Registrane uno. La risposta contiene signing_secret.
client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)replace_actions rende definitivo il tuo elenco.
client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)Crea o aggiorna, quindi installa. È idempotente, quindi inseriscilo nello script di deployment.
client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)actions è una lista consentita; [] non espone nulla.
client.integrations.run(slug, action, arguments, conversation_id)Richiamare tramite il percorso reale della chiamata.
client.integrations.delete(slug) · remove_action(slug, name)Ticket, articoli, widget, contenuti multimediali, webhook e metadati
client.tickets.list · get · create · update · reply · categoriesclient.articles.list · get · create · update · deleteclient.widgets.get(id) · update(id, **fields)La configurazione completa si trova in widget.raw.
client.media.upload(file, filename, content_type)Restituisce un oggetto Media che puoi passare a un builder di blocchi.
client.webhooks.list() · create(url, events, widget_id) · delete(id)I nomi di eventi sconosciuti vengono rifiutati localmente.
client.members.list() · client.meta.me() · schema() · stats()Modelli
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 sentRicette
Evitare un ticket con il centro assistenza
@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)Qualificare un lead e instradarlo
@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()Recuperare una valutazione negativa
@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",
)Sincronizzazione notturna dei contatti
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)},
)Un negozio che non conosciamo
@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()Questa è l’intera integrazione. Le schede prodotto, la vetrina Home e la regola che impedisce al modello di incollare URL grezzi sono incluse con il nome riservato.
Stai creando qualcosa? Parla con noi , vogliamo che le persone creino con Conecto e diamo priorità alle richieste degli sviluppatori. L’SDK ha licenza MIT ed è open source; issue e pull request sono benvenute su GitHub.