Developers / Python
De Python-SDK
Lees en beantwoord chats, stuur afbeeldingen en video, bouw bots en laat de AI-agent vanuit Python gegevens in uw eigen systemen opzoeken. Alles wat de REST API doet, met de lastige onderdelen al geregeld: controleren of een verzoek echt van ons kwam, niet tweemaal op dezelfde gebeurtenis antwoorden, veilig opnieuw proberen en een ongeldig bericht afvangen voordat het uw proces verlaat.
Nieuw bij Conecto? Begin hier , vijf minuten terminologie, waarna de rest van deze pagina voor zich spreekt.
pip install conectov0.1.0Python 3.9+MIT One dependency (requests) · Type hints throughout · Op PyPI
Begin hier
Als u Conecto nog nooit hebt gebruikt, bevat dit gedeelte het volledige denkmodel. Vijf minuten hier maakt de rest van de pagina duidelijk.
Wat Conecto is
Een chatwidget op uw website. Bezoekers typen erin; uw team, een AI-agent of uw eigen code antwoordt. Alles hieronder is een manier om uw code in die cyclus.
De vijf zelfstandige naamwoorden
Bijna elke methode in deze SDK accepteert of retourneert een van deze objecten.
Workspace
Uw account. Uw referentie behoort tot precies één account en kan nooit een ander account zien.
Widget
One installed chat box. You might have one per website or brand. Has an id.
Bezoeker
A browser on your site, identified by a session string stored in that browser.
Gesprek
One thread between a visitor and you. Has an id, and a status of open or closed.
Bericht
One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.
De drie dingen die u kunt bouwen
1. Een script
Wordt volgens uw planning uitgevoerd. Synchroniseert contacten, opent tickets en verstuurt campagneberichten. U hoeft niets te hosten; het script roept alleen de API aan.
2. Een bot
Een webserver van u die we informeren wanneer een bezoeker schrijft. U bepaalt het antwoord. U het gesprek te sturen.
3. Een plugin
Een webserver van u die we aanroepen wanneer de AI heeft een gegeven nodig dat alleen u bezit. De AI stuurt het gesprek; u levert de zoekopdrachten.
Bot of plugin? Wilt u de woorden schrijven die de bezoeker leest, bouw dan een bot. Wilt u liever dat de AI praat en alleen toegang krijgt tot uw gegevens, zoals bestellingen, abonnementen en voorraadniveaus, bouw dan een plugin. De meeste teams kiezen uiteindelijk voor een plugin.
Woorden die deze pagina gebruikt
Jargon, eenmaal gedefinieerd. Niets hier is specifiek voor Conecto, behalve de laatste twee termen.
Welke sectie heb ik nodig?
| Ik wil… | Ga naar |
|---|---|
| Chats lezen of beantwoorden vanuit een script | Gesprekken |
| Een afbeelding, GIF, video of kaart verzenden | Rich content |
| Bezoekers automatisch antwoorden met mijn eigen logica | Een bot bouwen |
| De AI gegevens in mijn systemen laten opzoeken | Een plugin bouwen |
| Een winkel koppelen waarin de AI kan zoeken | Een plugin bouwen |
| De e-mailcode overslaan voor reeds ingelogde gebruikers | Identiteit |
| Mijn CRM gesynchroniseerd houden | Contacten en bezoekers |
| Weten wat ik moet afvangen als iets mislukt | Fouten en nieuwe pogingen |
Installeren en verifiëren
pip install conectoMaak een referentie in Instellingen → Ontwikkelaars. U ontvangt een client-ID (ck_…) and a geheim (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.
Snelstart
Drie dingen die u in de eerste vijf minuten kunt doen.
Lezen wat er gebeurt
for convo in client.conversations.list(status="open"):
who = convo.visitor.email if convo.visitor else "anonymous"
print(f"#{convo.id} {who} {convo.preview}")Iemand antwoorden
convo = client.conversations.get(4821)
print(convo.messages[-1].body) # what they said last
convo.reply("On it — give me one moment.")Iets anders dan tekst verzenden
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"),
]),
])Dit is de opbouw van de hele bibliotheek: een client met resources, objecten die zelf acties kunnen uitvoeren en builders voor alles met structuur.
De client
Maak er één en bewaar deze. De client hergebruikt verbindingen en kan veilig tussen threads worden gedeeld.
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
)Omgeving
| Variabele | Stelt in |
|---|---|
CONECTO_CLIENT_ID | De client-ID. |
CONECTO_SECRET | Het geheim. |
CONECTO_BASE_URL | Basis-URL van de API. Zelden nodig. |
Bronnen
| Attribuut | Omvat |
|---|---|
client.conversations | Weergeven, lezen, antwoorden, typen, overdragen, toewijzen en sluiten. |
client.contacts | Het CRM: maken of bijwerken op e-mailadres, zoeken, bijwerken en verwijderen. |
client.visitors | Sessies, identiteitsbevestiging en proactieve berichten. |
client.tickets | Maken, bijwerken, antwoorden en categorieën beheren. |
client.articles | Helpcenterinhoud zoeken en publiceren. |
client.integrations | Plugins registreren, installeren, uitvoeren en deployen. |
client.widgets | Widgetconfiguratie lezen en bijwerken. |
client.media | Bestanden uploaden voor gebruik in blokken. |
client.webhooks | Endpoints op gebeurtenissen abonneren. |
client.members | Teamleden, voor toewijzing. |
client.meta | me(), schema(), stats(). |
Versieverschil
Elke reactie bevat de API-versie van de server. Registreer deze bij het opstarten. Als de versie verandert en een gebruikt veld verdwijnt, is dit het eerste wat u wilt weten.
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() retourneert de volledige API als JSON, met endpoints, gebeurtenissen, bloktypen, gereserveerde acties, foutcodes en alle limieten. Deze wordt geleverd door dezelfde code die de regels afdwingt en kan daardoor niet van de werkelijkheid afwijken.
Gesprekken
A thread between a visitor and your workspace. Messages have a sender van visitor, agent, bot van system.
Weergeven en lezen
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)Antwoorden
convo.reply("I can refund order #1284 — confirm?",
buttons=["Yes, refund it", "Talk to a human"])Een tik op een snelantwoordknop komt terug als een gewoon bezoekersbericht met die tekst. Uw handler heeft er dus geen speciaal geval voor nodig.
| Argument | Doet |
|---|---|
body | De tekst, maximaal 4000 tekens. |
blocks | Rijke inhoud, zie Rich content. |
buttons | Maximaal 6 snelle antwoorden. |
products | Maximaal 4 productkaarten, gelijk aan wat de ingebouwde AI toevoegt. |
ask_email | Toont het ingebouwde formulier voor het verzamelen van e-mailadressen. |
ticket_form | Voegt het ingebouwde formulier voor het openen van een ticket toe. |
internal | Een notitie die alleen medewerkers zien. De bezoeker ziet deze nooit. |
idempotency_key | Geef de ID van een webhookbezorging door, zodat een nieuwe poging niet tweemaal kan plaatsen. |
De thread sturen
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 Fouten.
Rich content
Elk bericht kan maximaal tien bevatten blokken: afbeeldingen en GIF’s, video, audio, bestanden, ingesloten inhoud van derden, kaartcarrousels, label/waardelijsten, knoppen en scheidingslijnen. Ze worden weergegeven in de widget en in de inbox van de medewerker.
Blokken zijn tijdens de overdracht gewone dictionaries, dus u zou ze handmatig kunnen schrijven. De builders bestaan omdat een verkeerd gespelde sleutel ertoe leidt dat een klant meldt dat de afbeelding nooit verscheen. Ze passen dezelfde regels als de server toe op het punt waar de traceback nog naar uw code wijst.
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")]),
]),
])Elk bloktype
| Builder | Notities |
|---|---|
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 en Spotify. Geef de gewone deellink door. |
blocks.audio(url, title) | .mp3, .wav, .m4a… |
blocks.file(url, filename, size) | Een downloadrij. |
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 en link_button. |
blocks.text(body) | Een extra alinea onder andere blokken. |
blocks.divider() | Een horizontale scheidingslijn. |
Twee soorten knoppen
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 tabGeef een antwoordknop een expliciete waarde wanneer het label goed leesbaar moet zijn, maar de tekst waarop uw bot reageert niet telkens mag veranderen wanneer iemand de tekst herschrijft.
Een GIF verzenden
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)Regels die de builders afdwingen
- Every URL must be
https. The widget runs on an https page, so anything else is blocked by the browser anyway. - Tien blokken per bericht, tien kaarten, twaalf lijstrijen en zes knoppen.
- Long strings are truncated, not rejected. Structural mistakes raise
BlockError. - A YouTube link in a
videoblock raises, and tells you to useembed.
Validatie vindt lokaal plaats. 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.
Bestanden uploaden
Geen openbare URL voor die GIF, bon of datasheet? Upload het bestand, tot 10 MB, en gebruik het resultaat.
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 van blocks.file.
Use media.url, not media.absolute_url. url is een pad dat de widget ten opzichte van zijn eigen oorsprong oplost. Hetzelfde bericht werkt daardoor tijdens ontwikkeling, in een previewdeployment en in productie, ook wanneer uw domein verandert.
Contacten en bezoekers
Contacten
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 is samengevoegd, niet vervangen. Twee taken kunnen elk hun eigen sleutels beheren zonder elkaar te overschrijven. Maximaal 30 sleutels per aanroep.
Bezoekers
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.verifiedProactieve berichten
Stuur iets naar een sessie zonder op een vraag te wachten, zoals verzendupdates, proefperiodeherinneringen of winkelwagenherstel.
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"),
])],
)De actieve conversatie van de sessie wordt hergebruikt of er wordt een nieuwe geopend. Een geopende widget toont deze bij de volgende poll; een gesloten widget toont deze bij het opnieuw openen als ongelezen.
Tickets en artikelen
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 noteEen openbaar antwoord heropent een opgelost ticket, omdat een antwoord op iets dat als afgerond is gemarkeerd vrijwel altijd een nieuwe ronde betekent.
Helpcenterartikelen
Nuttig in beide richtingen: zoek erin vanuit een bot voordat u een ticket opent en stuur documentatie vanuit de omgeving waarin u die opstelt.
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)HTML wordt aan de serverzijde opgeschoond aan de hand van een toelatingslijst met veilige tags, via hetzelfde proces als in de dashboardeditor. Scripts en stijlen worden verwijderd voordat iets wordt opgeslagen.
Een bot bouwen
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()Beschikbaar stellen
# 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()Abonneer vervolgens het 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 deliveriesHandlers
| Decorator | Wordt geactiveerd bij |
|---|---|
@bot.on_message | Een bericht van een bezoeker. Berichten van bots en medewerkers bereiken deze nooit, waardoor een bot niet op zichzelf kan antwoorden. |
@bot.on_conversation_started | conversation.created , begroeten of een status instellen. |
@bot.on_rating | conversation.rated. ctx.event.rating has score en 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 | Een handler heeft een fout gegenereerd. Zonder eigen handler worden fouten vastgelegd en genegeerd. |
Wat een handler ontvangt
| Op ctx | Is |
|---|---|
ctx.text | De tekst van het bezoekersbericht. |
ctx.verified · ctx.verified_email | Of de identiteit is geverifieerd en welk adres is geverifieerd. |
ctx.conversation_id · ctx.widget_id · ctx.session | ID’s waarop u kunt handelen. |
ctx.reply(...) | Answer. Takes everything conversations.reply does. |
ctx.note(...) | Een notitie die alleen medewerkers zien. |
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close() | De thread sturen. |
ctx.history() | De volledige berichtgeschiedenis ophalen. Eén verzoek. |
ctx.client · ctx.event | De low-level client en de verwerkte gebeurtenis, voor al het overige. |
Gebruikt u meer dan één 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())Een plugin bouwen
Een bot beantwoordt webhooks. Een plugin werkt in de andere richting: u geeft aan wat uw systemen kunnen en de AI-agent bepaalt wanneer die mogelijkheden nodig zijn tijdens een gesprek. U schrijft nooit de gesprekslogica, alleen de zoekopdracht.
Zo wordt een winkel, facturatiesysteem of CRM dat we niet kennen een volwaardige integratie.
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()Beschikbaar stellen en registreren
# 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() maakt of werkt de integratie bij zodat deze met uw code overeenkomt, verwijdert acties die u hebt geschrapt en installeert de integratie op uw widgets. Niets wordt aan de AI beschikbaar gesteld voordat de integratie is geïnstalleerd.
De vijf antwoorden
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.
Risiconiveaus
| Risico | Betekent |
|---|---|
public_read | Catalogus, beschikbaarheid en documentatie. Geen identiteit nodig. |
verified_read | Gegevens van één persoon. Geweigerd totdat het e-mailadres is geverifieerd. |
public_write | Een wijziging waarvoor geen identiteit nodig is, zoals een nieuwsbriefinschrijving of leadregistratie. |
write | Een wijziging in het account van één persoon. Geverifieerd, gecontroleerd en nooit ongemerkt opnieuw geprobeerd. |
req.verified_email is de enige identiteit die u kunt vertrouwen. An email in req.arguments is whatever the model parsed out of a chat. For verified_read en 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.
Gereserveerde acties
Enkele namen hebben binnen het platform een eigen betekenis. Declareer er een en uw service wordt gekoppeld aan de bestaande functie voor die naam.
| Naam | Wat u ontvangt |
|---|---|
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 | Bestelstatus, altijd achter verificatie. |
orders.get_tracking | Zending volgen, altijd geverifieerd. |
orders.list_recent | De recente bestellingen van de bezoeker, altijd geverifieerd. |
account.lookup | Elke platte record over de geverifieerde bezoeker, zoals abonnement, tegoed of verlengdatum. |
Test het voordat een bezoeker dat doet
client.integrations.run("acme-store", "catalog.search_products",
{"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}Dit voert het werkelijke 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.
Een goede beschrijving schrijven
De 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.
Zwak: «Bestelling opzoeken.»
Sterk: «Zoek de status en bezorgdatum van een eigen bestelling van de bezoeker op aan de hand van het bestelnummer. Gebruik dit telkens wanneer de bezoeker vraagt waar iets is of wanneer het aankomt.»
Wat het platform garandeert
- 8 seconden time-out, 128 KB reactielimiet.
- Calls are signed with your
signing_secret— same scheme as webhooks. - Uw URL wordt eenmaal omgezet en aan een openbaar IP-adres gekoppeld; omleidingen worden geweigerd.
- Alles wat u retourneert bereikt het model als gegevens, nooit als instructies, en sleutels die op geheimen lijken worden verwijderd.
- Aangepaste integraties vereisen een betaald abonnement met AI-agent. Per workspace zijn 20 integraties toegestaan, elk met 40 acties.
Identiteit
Alles wat de gegevens van één persoon raakt, wacht totdat we weten wie die persoon is. Dat gebeurt op een van twee manieren: met een code per e-mail of via de bevestiging door uw site voor een gebruiker die al is ingelogd.
# 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)Zolang de bevestiging geldig is, behandelt Conecto het e-mailadres als geverifieerd: de widget kent de naam, chats worden aan het juiste contact gekoppeld en processen die anders een e-mailcode vereisen, zoals bestellingen opzoeken, terugbetalingen, accountgegevens of geverifieerde pluginacties, gaan zonder code verder.
Sta alleen in voor sessies die uw backend daadwerkelijk heeft geverifieerd. Uw bevestiging geldt als even sterk als onze eigen e-mailcode, omdat u de persoon werkelijk hebt geverifieerd. Deze is tijdgebonden en sessiegebonden, dus een nieuwe browser heeft een nieuwe bevestiging nodig. De browser kan zichzelf nooit als geverifieerd markeren; die aanroep is beveiligd en hoort op uw server.
Webhooks en gebeurtenissen
Bot doet dit allemaal voor u. Gebruik de low-level helpers wanneer u aan de rand wilt verifiëren en via een wachtrij wilt verwerken.
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.verifiedVerifiëren aan de hand van de onbewerkte inhoud. 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.
Gebeurtenissen
conversation.createdEen bezoeker is een gesprek gestart.message.createdEen bezoeker heeft een bericht gestuurd. De trigger voor de bot.conversation.closedEen gesprek is gesloten.conversation.assignedToegewezen aan of losgekoppeld van een teamlid.conversation.handoffGemarkeerd als hulp van een medewerker vereist.conversation.ratedEr is een CSAT-beoordeling ontvangen (1 tot 5 plus een opmerking).ticket.createdEr is vanuit een willekeurige bron een ticket geopend.ticket.updatedDe status, prioriteit of toegewezen medewerker van een ticket is gewijzigd.contact.createdEen nieuwe persoon is aan het CRM toegevoegd.visitor.identifiedEen sessie is via de API geïdentificeerd.Betekenis van bezorgingen
Bezorgingen vinden plaats ten minste eenmaal en zonder gegarandeerde volgorde: 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-Timestamplaat u verouderde bezorgingen weigeren; de SDK controleert ze standaard met een tolerantie van vijf minuten.
signature = webhooks.sign(SECRET, raw_body) # what we send
webhooks.verify(raw_body, header_value, SECRET) # True / False, never raisesIntegratieaanroepen die wij uitvoeren tot your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.
Fouten en nieuwe pogingen
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")| Uitzondering | Status | Betekent |
|---|---|---|
AuthenticationError | 401 | Ontbrekende, onbekende of ingetrokken referentie. |
InvalidRequestError | 400 | De inhoud is niet door de validatie gekomen; het bericht vermeldt waarom. |
NotFoundError | 404 | Het object bestaat niet in deze workspace. |
ConflictError | 409 | Die identificatie is al in gebruik. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | Een limiet van het abonnement of de workspace. |
PlanRequired | 403 | Het abonnement bevat deze functie niet. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | Een afhankelijkheid is niet geconfigureerd. |
ServerError | 5xx | Onze fout. U kunt veilig opnieuw proberen. |
BlockError | — | Lokaal gegenereerd door een blokbuilder, vóór enig verzoek. |
SignatureError | 401 | De verificatie van een webhook- of integratieaanroep is mislukt. |
APIConnectionError · APITimeoutError | — | Het verzoek heeft nooit antwoord gekregen. |
Every exception carries code, message, status and the raw response.
Nieuwe pogingen
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.
Schrijfbewerkingen worden ook opnieuw geprobeerd, wat normaal gevaarlijk is. Hier is het veilig omdat elke schrijfbewerking een idempotentiesleutel bevat en een nieuwe poging dezelfde sleutel hergebruikt. De server herkent de tweede poging als dezelfde aanroep en voert deze niet opnieuw uit.
client = Conecto(max_retries=0) # opt out entirelyBelangrijke limieten
| Limiet | Waarde |
|---|---|
| Verzoeken per minuut, per referentie | 300 |
| Berichtinhoud | 4000 tekens |
| Blokken per bericht | 10 |
| Snelantwoordknoppen | 6 |
| Media uploaden | 10 MB |
| Integraties per workspace | 20 |
| Acties per integratie | 40 |
| Webhooks per workspace | 10 |
client.meta.schema()["limits"] bevat altijd de actuele cijfers.
Paginering
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.
Wordt getest
The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete en 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/"Sla bij bots en plugins de handtekeningverificatie in tests over in plaats van fixtures te ondertekenen:
bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)Methodereferentie
Gesprekken
client.conversations.list(status, widget_id, session, limit, before_id)Gesprekken, nieuwste activiteit eerst. Retourneert een Page.
client.conversations.get(id, since_id=None)Eén gesprek met de bijbehorende berichten. since_id retourneert alleen wat nieuwer is.
client.conversations.messages(id, since_id=None)Alleen de berichten.
client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)Een botbericht verzenden. Ook beschikbaar als send().
client.conversations.typing(id, on=True, name="Bot")De typindicator tonen of wissen.
client.conversations.handoff(id)Markeren als hulp van een medewerker vereist.
client.conversations.assign(id, user_id)None verwijdert de toewijzing.
client.conversations.close(id) · reopen(id) · set_status(id, status)Status wijzigen.
Contacten
client.contacts.list(email, query, limit, before_id) · get(id) · find(email)find() retourneert één contact of None.
client.contacts.upsert(email, name, title, company, location, phone, notes, custom_fields)Maken of bijwerken op e-mailadres. Ook beschikbaar als create().
client.contacts.update(id, **fields) · delete(id)Bezoekers
client.visitors.get(widget_id, session)Identiteit en verificatiestatus.
client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)Koppel uw gebruiker; verified=True staat voor diens identiteit in.
client.visitors.unverify(widget_id, session)Trek de bevestiging in. Roep aan bij het uitloggen.
client.visitors.message(widget_id, session, body, blocks, buttons, products)Een proactief bericht.
Integraties
client.integrations.list() · get(slug) · actions(slug)actions() retourneert ook de catalogus met gereserveerde acties.
client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)Registreer er één. De reactie bevat signing_secret.
client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)replace_actions maakt uw lijst bepalend.
client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)Maken of bijwerken en vervolgens installeren. Idempotent, dus geschikt voor uw deployscript.
client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)actions is een toelatingslijst; [] stelt niets beschikbaar.
client.integrations.run(slug, action, arguments, conversation_id)Aanroepen via het werkelijke aanroeppad.
client.integrations.delete(slug) · remove_action(slug, name)Tickets, artikelen, widgets, media, webhooks en metadata
client.tickets.list · get · create · update · reply · categoriesclient.articles.list · get · create · update · deleteclient.widgets.get(id) · update(id, **fields)De volledige configuratie staat in widget.raw.
client.media.upload(file, filename, content_type)Retourneert een Media-object dat u aan een blokbuilder kunt doorgeven.
client.webhooks.list() · create(url, events, widget_id) · delete(id)Onbekende gebeurtenisnamen worden lokaal geweigerd.
client.members.list() · client.meta.me() · schema() · stats()Modellen
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 sentRecepten
Een ticket voorkomen met uw helpcenter
@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)Een lead kwalificeren en doorsturen
@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()Een slechte beoordeling herstellen
@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",
)Nachtelijke contactsynchronisatie
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)},
)Een winkel die we niet kennen
@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()Dit is de volledige integratie. Productkaarten, de Home-showcase en de discipline die voorkomt dat het model onbewerkte URL’s plakt, worden allemaal geleverd door de gereserveerde naam.
Bouwt u iets? Praat met ons , we willen dat mensen met Conecto bouwen en geven prioriteit aan wat ontwikkelaars vragen. De SDK heeft een MIT-licentie en is open source; issues en pull requests zijn welkom op GitHub.