Udviklere / Python
Den Python-SDK
Læs og besvar chats, send billeder og video, byg bots, og lad AI-agenten slå oplysninger op i jeres egne systemer med Python. Alt, hvad REST API'et gør, med de vanskelige dele håndteret: kontrol af, at en anmodning virkelig kom fra os, undgåelse af dobbelte svar på samme hændelse, sikre nye forsøg og registrering af ugyldige beskeder, før de forlader processen.
Ny på Conecto? Start her , fem minutters begreber, hvorefter resten af siden giver sig selv.
pip install conectov0.1.0Python 3.9+MIT One dependency (requests) · Type hints throughout · På PyPI
Start her
Hvis I aldrig har brugt Conecto før, indeholder dette afsnit hele tankemodellen. Fem minutter her gør resten af siden selvforklarende.
Hvad Conecto er
En chatwidget på jeres website. Besøgende skriver i den; jeres team, en AI-agent eller jeres egen kode svarer. Alt nedenfor er en måde at føre jeres kode ind i det kredsløb.
De fem navneord
Næsten alle metoder i dette SDK modtager eller returnerer et af disse objekter.
Arbejdsområde
Jeres konto. Legitimationsoplysningen tilhører præcis én konto og kan aldrig se en anden.
Widget
One installed chat box. You might have one per website or brand. Has an id.
Besøgende
A browser on your site, identified by a session string stored in that browser.
Samtale
One thread between a visitor and you. Has an id, and a status of open or closed.
Besked
One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.
De tre ting, I kan bygge
1. Et script
Køres efter jeres tidsplan. Synkroniserer kontakter, åbner sager og sender kampagnebeskeder. Intet skal hostes; scriptet kalder blot API'et.
2. En bot
En af jeres webservere, som vi underretter, når en besøgende skriver. I bestemmer svaret. I styre samtalen.
3. Et plugin
En af jeres webservere, som vi kalder, når AI har brug for en oplysning, kun I har. AI'en styrer samtalen; I leverer opslagene.
Bot eller plugin? Hvis I vil skrive de ord, den besøgende læser, skal I bygge en bot. Hvis I hellere vil lade AI'en tale og blot give den adgang til jeres data, for eksempel ordrer, abonnementer og lagerbeholdning, skal I bygge et plugin. De fleste teams ender med at vælge et plugin.
Ord, der bruges på denne side
Jargon, defineret én gang. Intet her er specifikt for Conecto bortset fra de sidste to begreber.
Hvilket afsnit har jeg brug for?
| Jeg vil… | Gå til |
|---|---|
| Læs eller besvar chats fra et script | Samtaler |
| Send et billede, en GIF, en video eller et kort | Rigt indhold |
| Svare besøgende automatisk med min egen logik | Byg en bot |
| Lad AI'en slå oplysninger op i mine systemer | Byg et plugin |
| Forbind en butik, som AI'en kan søge i | Byg et plugin |
| Spring e-mailkoden over for brugere, der allerede er logget ind | Identitet |
| Hold mit CRM-system synkroniseret | Kontakter og besøgende |
| Vide, hvad jeg skal fange, når noget går galt | Fejl og nye forsøg |
Installer og godkend
pip install conectoOpret en legitimationsoplysning i Indstillinger → Udviklere. I får en klient-id (ck_…) and a hemmelighed (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.
Hurtig start
Tre ting, I kan gøre i løbet af de første fem minutter.
Læs, hvad der sker
for convo in client.conversations.list(status="open"):
who = convo.visitor.email if convo.visitor else "anonymous"
print(f"#{convo.id} {who} {convo.preview}")Svare nogen
convo = client.conversations.get(4821)
print(convo.messages[-1].body) # what they said last
convo.reply("On it — give me one moment.")Send noget andet end tekst
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"),
]),
])Sådan er hele biblioteket opbygget: en klient med ressourcer, objekter, der kan handle på sig selv, og builders til alt struktureret.
Klienten
Opret én, og behold den. Den genbruger forbindelser og kan deles sikkert mellem tråde.
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
)Miljø
| Variabel | Angiver |
|---|---|
CONECTO_CLIENT_ID | Klient-ID'et. |
CONECTO_SECRET | Hemmeligheden. |
CONECTO_BASE_URL | API'ets basis-URL. Behøves sjældent. |
Ressourcer
| Attribut | Dækker |
|---|---|
client.conversations | List, læs, svar, vis skrivning, overdrag, tildel og luk. |
client.contacts | CRM-systemet: opret eller opdater efter e-mailadresse, søg, opdater og slet. |
client.visitors | Sessioner, identitetsbekræftelse og proaktive beskeder. |
client.tickets | Opret, opdater, svar og administrer kategorier. |
client.articles | Søg efter og offentliggør indhold i hjælpecenteret. |
client.integrations | Registrér, installer, kør og deploy plugins. |
client.widgets | Læs og opdater widgetkonfigurationen. |
client.media | Upload filer til brug i blokke. |
client.webhooks | Abonnér endpoints på hændelser. |
client.members | Teammedlemmer, til tildeling. |
client.meta | me(), schema(), stats(). |
Versionsafvigelse
Hvert svar indeholder serverens API-version. Log den ved opstart. Hvis versionen ændres, og et felt, I bruger, forsvinder, er dette det første, I vil vide.
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() returnerer hele API'et som JSON, herunder endpoints, hændelser, bloktyper, reserverede handlinger, fejlkoder og alle grænser. Det leveres af den samme kode, som håndhæver reglerne, og kan derfor ikke afvige fra virkeligheden.
Samtaler
A thread between a visitor and your workspace. Messages have a sender af visitor, agent, bot eller system.
Liste og læsning
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)Svare
convo.reply("I can refund order #1284 — confirm?",
buttons=["Yes, refund it", "Talk to a human"])Et tryk på en hurtigsvarsknap kommer tilbage som en almindelig besked fra den besøgende med den tekst, så jeres handler behøver ikke et særtilfælde.
| Argument | Gør |
|---|---|
body | Teksten, op til 4.000 tegn. |
blocks | Rich content, se Rigt indhold. |
buttons | Op til 6 hurtigsvar. |
products | Op til 4 produktkort, samme antal som den indbyggede AI vedhæfter. |
ask_email | Viser den indbyggede formular til indsamling af e-mailadresser. |
ticket_form | Vedhæfter den indbyggede formular til at åbne en sag. |
internal | En note kun til agenter. Den besøgende ser den aldrig. |
idempotency_key | Send et leverings-ID for webhooken med, så et nyt forsøg ikke kan offentliggøre dobbelt. |
Styring af tråden
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 Fejl.
Rigt indhold
Hver besked kan indeholde op til ti blokke: billeder og GIF'er, video, lyd, filer, indlejringer fra tredjeparter, kortkarruseller, etiket/værdi-lister, knapper og skillelinjer. De vises i widgetten og i agentens indbakke.
Blokke er almindelige dictionaries under overførslen, så I kunne skrive dem manuelt. Builders findes, fordi en forkert stavet nøgle kan få en kunde til at sige, at billedet aldrig blev vist. De anvender de samme regler som serveren, mens traceback stadig peger på jeres kode.
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")]),
]),
])Alle bloktyper
| Builder | Noter |
|---|---|
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 og Spotify. Send det normale delingslink. |
blocks.audio(url, title) | .mp3, .wav, .m4a… |
blocks.file(url, filename, size) | En downloadrække. |
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 og link_button. |
blocks.text(body) | Et ekstra afsnit under andre blokke. |
blocks.divider() | En vandret linje. |
To typer knapper
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 tabGiv en svarknap en udtrykkelig værdi, når etiketten skal være let at læse, men den tekst, som botten matcher, ikke skal ændres, hver gang nogen omskriver teksten.
Send en 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)Regler, som builders håndhæver
- Every URL must be
https. The widget runs on an https page, so anything else is blocked by the browser anyway. - Ti blokke pr. besked, ti kort, tolv listerækker og seks knapper.
- Long strings are truncated, not rejected. Structural mistakes raise
BlockError. - A YouTube link in a
videoblock raises, and tells you to useembed.
Valideringen sker lokalt. 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.
Upload filer
Ingen offentlig URL til GIF'en, kvitteringen eller databladet? Upload filen, op til 10 MB, og brug resultatet.
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 eller blocks.file.
Use media.url, not media.absolute_url. url er en sti, som widgetten opløser i forhold til sin egen oprindelse. Den samme besked fungerer derfor i udvikling, i en forhåndsvisning og i produktion, også den dag jeres domæne ændres.
Kontakter og besøgende
Kontakter
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 er sammenflettet, ikke erstattet. To jobs kan administrere deres egne nøgler uden at overskrive hinanden. Op til 30 nøgler pr. kald.
Besøgende
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.verifiedProaktive beskeder
Send til en session uden at vente på et spørgsmål, for eksempel leveringsopdateringer, påmindelser om prøveperioder eller gendannelse af indkøbskurve.
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"),
])],
)Den genbruger sessionens aktive samtale eller åbner en ny. En åben widget viser den ved næste polling; en lukket widget viser den næste gang, den åbnes, markeret som ulæst.
Sager og artikler
Sager
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 noteEt offentligt svar genåbner en løst sag, fordi et svar på noget, der er markeret som afsluttet, næsten altid betyder en ny runde.
Hjælpecenterartikler
Nyttigt i begge retninger: søg derfra med en bot, før I åbner en sag, og send dokumentation ind fra det værktøj, hvor I skriver den.
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 renses på serversiden mod en tilladelsesliste med sikre tags gennem samme proces, som editoren i dashboardet bruger. Scripts og typografi fjernes, før noget gemmes.
Byg en 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()Udbydning
# 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()Abonnér derefter endpointet:
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
| Dekoratør | Udløses af |
|---|---|
@bot.on_message | En besked fra en besøgende. Beskeder fra bots og agenter når aldrig denne, hvilket forhindrer en bot i at svare sig selv. |
@bot.on_conversation_started | conversation.created , hilse eller angive tilstand. |
@bot.on_rating | conversation.rated. ctx.event.rating has score og 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 | En handler udløste en undtagelse. Uden en særskilt handler logges og ignoreres fejlene. |
Hvad en handler modtager
| På ctx | Er |
|---|---|
ctx.text | Teksten i den besøgendes besked. |
ctx.verified · ctx.verified_email | Om identiteten er verificeret, og hvilken adresse der er verificeret. |
ctx.conversation_id · ctx.widget_id · ctx.session | ID'er, I kan handle på. |
ctx.reply(...) | Answer. Takes everything conversations.reply does. |
ctx.note(...) | En note kun til agenter. |
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close() | Styr tråden. |
ctx.history() | Hent hele beskedhistorikken. Én anmodning. |
ctx.client · ctx.event | Low-level-klienten og den fortolkede hændelse, til alt andet. |
Kører I mere end é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())Byg et plugin
En bot besvarer webhooks. En plugin går i den modsatte retning: I angiver, hvad jeres systemer kan gøre, og AI-agenten bestemmer, hvornår funktionerne skal bruges under en samtale. I skriver aldrig samtalelogikken, kun opslaget.
Sådan bliver en butik, et faktureringssystem eller et CRM-system, vi aldrig har hørt om, en fuldgyldig integration.
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()Udbyd og registrér
# 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() opretter eller opdaterer integrationen, så den svarer til jeres kode, sletter de handlinger, I har fjernet, og installerer den i jeres widgets. Intet eksponeres for AI'en, før integrationen er installeret.
De fem svar
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.
Risikoniveauer
| Risiko | Betyder |
|---|---|
public_read | Katalog, tilgængelighed og dokumentation. Ingen identitet kræves. |
verified_read | Én persons data. Afvises, indtil e-mailadressen er verificeret. |
public_write | En ændring, der ikke kræver identitet, for eksempel tilmelding til et nyhedsbrev eller indsamling af leads. |
write | En ændring af en persons konto. Verificeret, revideret og aldrig forsøgt igen i det skjulte. |
req.verified_email er den eneste identitet, I kan stole på. An email in req.arguments is whatever the model parsed out of a chat. For verified_read og 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.
Reserverede handlinger
Nogle navne har en særlig betydning på platformen. Deklarer et af dem, så kobles jeres tjeneste til den funktion, der allerede findes for navnet.
| Navn | Hvad I får |
|---|---|
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 | Ordrestatus, altid efter verificering. |
orders.get_tracking | Sporing af forsendelser, altid verificeret. |
orders.list_recent | Den besøgendes seneste ordrer, altid verificerede. |
account.lookup | En vilkårlig enkel post om den verificerede besøgende, for eksempel abonnement, kreditter eller fornyelsesdato. |
Test det, før en besøgende gør det
client.integrations.run("acme-store", "catalog.search_products",
{"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}Dette kører den faktiske 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.
Skriv en god beskrivelse
Den 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.
Svag: »Slå ordre op.«
Stærk: »Slå status og leveringsdato op for en af den besøgendes egne ordrer ved hjælp af ordrenummeret. Brug dette, når personen spørger, hvor noget er, eller hvornår det kommer.«
Hvad platformen garanterer
- 8 sekunder timeout, 128 kB svargrænse.
- Calls are signed with your
signing_secret— same scheme as webhooks. - Jeres URL opløses én gang og bindes til en offentlig IP-adresse; omdirigeringer afvises.
- Alt, I returnerer, når modellen som data, aldrig som instruktioner, og nøgler, der ligner hemmeligheder, fjernes.
- Tilpassede integrationer kræver et betalt abonnement med AI-agent. Op til 20 pr. arbejdsområde og 40 handlinger i hver integration.
Identitet
Alt, der berører én persons data, venter, indtil vi ved, hvem personen er. Det sker på en af to måder: med en kode via e-mail eller gennem jeres websites bekræftelse for en bruger, der allerede er logget ind.
# 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)Så længe bekræftelsen gælder, behandler Conecto e-mailadressen som verificeret: widgetten kender navnet, chats knyttes til den rigtige kontakt, og flows, der ellers ville kræve en kode via e-mail, for eksempel ordreopslag, refunderinger, kontodata eller verificerede pluginhandlinger, fortsætter uden kode.
Stå kun inde for sessioner, som jeres backend faktisk har godkendt. Jeres bekræftelse behandles som lige så stærk som vores egen kode via e-mail, fordi I faktisk har godkendt personen. Den er tidsbegrænset og knyttet til sessionen, så en ny browser kræver en ny bekræftelse. Browseren kan aldrig selv markere sig som verificeret; kaldet er godkendt og hører hjemme på jeres server.
Webhooks og hændelser
Bot gør alt dette for jer. Brug low-level-hjælperne, når I vil verificere i kanten og behandle via en kø.
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.verifiedVerificér mod det ubehandlede indhold. 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.
Hændelser
conversation.createdEn besøgende startede en samtale.message.createdEn besøgende sendte en besked. Bot-triggeren.conversation.closedEn samtale blev lukket.conversation.assignedTildelt til eller fjernet fra et teammedlem.conversation.handoffMarkeret som kræver hjælp fra et menneske.conversation.ratedEn CSAT-vurdering kom ind (1 til 5 samt en kommentar).ticket.createdEn sag blev åbnet, uanset kilde.ticket.updatedEn sags status, prioritet eller tildelte person blev ændret.contact.createdEn ny person blev føjet til CRM-systemet.visitor.identifiedEn session blev identificeret via API'et.Leveringssemantik
Leveringer sker mindst én gang og uden garanteret rækkefølge: 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-Timestamplader jer afvise gamle leveringer; SDK'et kontrollerer dem som standard med fem minutters tolerance.
signature = webhooks.sign(SECRET, raw_body) # what we send
webhooks.verify(raw_body, header_value, SECRET) # True / False, never raisesIntegrationskald, vi foretager til your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.
Fejl og nye forsøg
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")| Undtagelse | Status | Betyder |
|---|---|---|
AuthenticationError | 401 | Manglende, ukendt eller tilbagekaldt legitimationsoplysning. |
InvalidRequestError | 400 | Indholdet bestod ikke valideringen; beskeden forklarer hvorfor. |
NotFoundError | 404 | Objektet findes ikke i dette arbejdsområde. |
ConflictError | 409 | Denne identifikator er allerede i brug. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | En grænse for abonnementet eller arbejdsområdet. |
PlanRequired | 403 | Abonnementet omfatter ikke denne funktion. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | En afhængighed er ikke konfigureret. |
ServerError | 5xx | Vores fejl. Sikkert at prøve igen. |
BlockError | — | Udløst lokalt af en blok-builder, før nogen anmodning. |
SignatureError | 401 | Verificeringen af et webhook- eller integrationskald mislykkedes. |
APIConnectionError · APITimeoutError | — | Anmodningen fik aldrig noget svar. |
Every exception carries code, message, status and the raw response.
Nye forsøg
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.
Skriveoperationer forsøges også igen, hvilket normalt er farligt. Her er det sikkert, fordi hver skriveoperation har en idempotensnøgle, og et nyt forsøg genbruger samme nøgle. Serveren genkender det andet forsøg som det samme kald og gentager det ikke.
client = Conecto(max_retries=0) # opt out entirelyGrænser, der er værd at kende
| Grænse | Værdi |
|---|---|
| Anmodninger pr. minut og legitimationsoplysning | 300 |
| Beskedindhold | 4.000 tegn |
| Blokke pr. besked | 10 |
| Hurtigsvarsknapper | 6 |
| Upload medier | 10 MB |
| Integrationer pr. arbejdsområde | 20 |
| Handlinger pr. integration | 40 |
| Webhooks pr. arbejdsområde | 10 |
client.meta.schema()["limits"] indeholder altid de aktuelle tal.
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.
Tester
The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete og 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/"For bots og plugins bør I springe signaturverificeringen over i tests i stedet for at signere fixtures:
bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)Metodereference
Samtaler
client.conversations.list(status, widget_id, session, limit, before_id)Samtaler, nyeste aktivitet først. Returnerer en Page.
client.conversations.get(id, since_id=None)En samtale med sine beskeder. since_id returnerer kun det, der er nyere.
client.conversations.messages(id, since_id=None)Kun beskederne.
client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)Send en botbesked. Findes også som send().
client.conversations.typing(id, on=True, name="Bot")Vis eller ryd skriveindikatoren.
client.conversations.handoff(id)Markér som kræver hjælp fra et menneske.
client.conversations.assign(id, user_id)None fjerner tildelingen.
client.conversations.close(id) · reopen(id) · set_status(id, status)Skift status.
Kontakter
client.contacts.list(email, query, limit, before_id) · get(id) · find(email)find() returnerer én kontakt eller None.
client.contacts.upsert(email, name, title, company, location, phone, notes, custom_fields)Opret eller opdater efter e-mailadresse. Findes også som create().
client.contacts.update(id, **fields) · delete(id)Besøgende
client.visitors.get(widget_id, session)Identitet og verificeringsstatus.
client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)Tilknyt jeres bruger; verified=True står inde for identiteten.
client.visitors.unverify(widget_id, session)Tilbagekald bekræftelsen. Kald ved logout.
client.visitors.message(widget_id, session, body, blocks, buttons, products)En proaktiv besked.
Integrationer
client.integrations.list() · get(slug) · actions(slug)actions() returnerer også kataloget over reserverede handlinger.
client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)Registrér én. Svaret indeholder signing_secret.
client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)replace_actions gør jeres liste autoritativ.
client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)Opret eller opdater, og installer derefter. Idempotent, så læg det i jeres deployscript.
client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)actions er en tilladelsesliste; [] eksponerer intet.
client.integrations.run(slug, action, arguments, conversation_id)Kald gennem den faktiske kaldsti.
client.integrations.delete(slug) · remove_action(slug, name)Sager, artikler, widgets, medier, webhooks og metadata
client.tickets.list · get · create · update · reply · categoriesclient.articles.list · get · create · update · deleteclient.widgets.get(id) · update(id, **fields)Den fulde konfiguration ligger i widget.raw.
client.media.upload(file, filename, content_type)Returnerer et Media-objekt, der kan sendes til en blok-builder.
client.webhooks.list() · create(url, events, widget_id) · delete(id)Ukendte hændelsesnavne afvises lokalt.
client.members.list() · client.meta.me() · schema() · stats()Modeller
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 sentOpskrifter
Undgå en sag ved hjælp af jeres hjælpecenter
@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)Kvalificér et lead, og route det
@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()Red en dårlig vurdering
@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",
)Natlig kontaktsynkronisering
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)},
)En butik, vi aldrig har hørt om
@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()Det er hele integrationen. Produktkort, Home-visningen og reglen, der forhindrer modellen i at indsætte rå URL'er, følger med det reserverede navn.
Bygger I noget? Tal med os , vi ønsker, at folk bygger med Conecto, og prioriterer det, udviklere efterspørger. SDK'et har MIT-licens og er open source; issues og pull requests er velkomne på GitHub.