Utviklere / Python
Den Python-SDK
Les og svar på chatter, send bilder og video, bygg boter og la KI-agenten slå opp informasjon i deres egne systemer med Python. Alt REST API-et gjør, med de vanskelige delene ferdig håndtert: kontroll av at en forespørsel virkelig kom fra oss, unngåelse av doble svar på samme hendelse, trygge nye forsøk og oppdagelse av ugyldige meldinger før de forlater prosessen.
Ny på Conecto? Start her , fem minutter med begreper, så blir resten av siden selvforklarende.
pip install conectov0.1.0Python 3.9+MIT One dependency (requests) · Type hints throughout · På PyPI
Start her
Hvis dere aldri har brukt Conecto før, inneholder denne delen hele tankemodellen. Fem minutter her gjør resten av siden selvforklarende.
Hva Conecto er
En chatwidget på nettstedet deres. Besøkende skriver i den; teamet deres, en KI-agent eller deres egen kode svarer. Alt nedenfor er en måte å føre koden deres inn i den løkken.
De fem substantivene
Nesten alle metodene i denne SDK-en tar imot eller returnerer ett av disse objektene.
Arbeidsområde
Kontoen deres. Autentiseringsopplysningen tilhører nøyaktig én konto og kan aldri se en annen.
Widget
One installed chat box. You might have one per website or brand. Has an id.
Besøkende
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.
Melding
One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.
De tre tingene dere kan bygge
1. Et skript
Kjøres etter deres tidsplan. Synkroniserer kontakter, åpner saker og sender kampanjemeldinger. Ingenting må driftes; skriptet kaller bare API-et.
2. En bot
En av nettserverne deres som vi varsler når en besøkende skriver. Dere bestemmer svaret. Dere styre samtalen.
3. En plugin
En av nettserverne deres som vi kaller når AI trenger en opplysning bare dere har. KI-en styrer samtalen; dere leverer oppslagene.
Bot eller plugin? Hvis dere vil skrive ordene den besøkende leser, bygger dere en bot. Hvis dere heller vil la KI-en snakke og bare gi den tilgang til dataene deres, for eksempel bestillinger, abonnementer og lagernivåer, bygger dere en plugin. De fleste team ender opp med å velge en plugin.
Ord som brukes på denne siden
Sjargong, definert én gang. Ingenting her er spesifikt for Conecto bortsett fra de to siste begrepene.
Hvilken seksjon trenger jeg?
| Jeg vil… | Gå til |
|---|---|
| Les eller svar på chatter fra et skript | Samtaler |
| Send et bilde, en GIF, en video eller et kort | Rikt innhold |
| Svare besøkende automatisk med min egen logikk | Bygg en bot |
| La KI-en slå opp informasjon i systemene mine | Bygg en plugin |
| Koble til en butikk KI-en kan søke i | Bygg en plugin |
| Hopp over e-postkoden for brukere som allerede er logget inn | Identitet |
| Hold CRM-systemet mitt synkronisert | Kontakter og besøkende |
| Vite hva jeg skal fange opp når noe går galt | Feil og nye forsøk |
Installer og autentiser
pip install conectoOpprett en autentiseringsopplysning i Innstillinger → Utviklere. Dere får en klient-ID (ck_…) and a hemmelighet (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.
Hurtigstart
Tre ting dere kan gjøre i løpet av de første fem minuttene.
Les hva som skjer
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 noen
convo = client.conversations.get(4821)
print(convo.messages[-1].body) # what they said last
convo.reply("On it — give me one moment.")Send noe annet enn 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"),
]),
])Slik er hele biblioteket bygget opp: en klient med ressurser, objekter som kan handle på seg selv, og builders for alt som er strukturert.
Klienten
Lag én og behold den. Den gjenbruker tilkoblinger og kan trygt deles mellom tråder.
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 | Angir |
|---|---|
CONECTO_CLIENT_ID | Klient-ID-en. |
CONECTO_SECRET | Hemmeligheten. |
CONECTO_BASE_URL | Basis-URL for API-et. Trengs sjelden. |
Ressurser
| Attributt | Dekker |
|---|---|
client.conversations | List opp, les, svar, vis skriving, overfør, tildel og lukk. |
client.contacts | CRM-systemet: opprett eller oppdater etter e-postadresse, søk, oppdater og slett. |
client.visitors | Økter, identitetsbekreftelse og proaktive meldinger. |
client.tickets | Opprett, oppdater, svar og administrer kategorier. |
client.articles | Søk etter og publiser innhold i hjelpesenteret. |
client.integrations | Registrer, installer, kjør og deploy plugins. |
client.widgets | Les og oppdater widgetkonfigurasjonen. |
client.media | Last opp filer som skal brukes i blokker. |
client.webhooks | Abonner endepunkter på hendelser. |
client.members | Teammedlemmer, for tildeling. |
client.meta | me(), schema(), stats(). |
Versjonsavvik
Hvert svar inneholder serverens API-versjon. Loggfør den ved oppstart. Hvis versjonen endres og et felt dere bruker forsvinner, er dette det første dere vil vite.
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, inkludert endepunkter, hendelser, blokktyper, reserverte handlinger, feilkoder og alle grenser. Det leveres av den samme koden som håndhever reglene, og kan derfor ikke avvike fra virkeligheten.
Samtaler
A thread between a visitor and your workspace. Messages have a sender av visitor, agent, bot eller system.
Liste og lesing
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 trykk på en hurtigsvarsknapp kommer tilbake som en vanlig melding fra den besøkende med den teksten, så handleren trenger ikke noe spesialtilfelle.
| Argument | Gjør |
|---|---|
body | Teksten, opptil 4000 tegn. |
blocks | Rich content, se Rikt innhold. |
buttons | Opptil 6 hurtigsvar. |
products | Opptil 4 produktkort, samme antall som den innebygde KI-en legger ved. |
ask_email | Viser det innebygde skjemaet for innsamling av e-postadresser. |
ticket_form | Legger ved det innebygde skjemaet for å åpne en sak. |
internal | Et notat bare for agenter. Den besøkende ser det aldri. |
idempotency_key | Send med en leverings-ID for webhooken, slik at et nytt forsøk ikke kan publisere dobbelt. |
Styring av 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 Feil.
Rikt innhold
Hver melding kan inneholde opptil ti blokker: bilder og GIF-er, video, lyd, filer, innebygd innhold fra tredjeparter, kortkaruseller, etikett/verdi-lister, knapper og skillelinjer. De vises i widgeten og i innboksen til agenten.
Blokker er vanlige dictionaries under overføringen, så dere kunne skrevet dem for hånd. Builders finnes fordi en feilstavet nøkkel kan føre til at en kunde sier at bildet aldri dukket opp. De bruker de samme reglene som serveren mens traceback fortsatt peker på koden deres.
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 blokktyper
| Builder | Notater |
|---|---|
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 den vanlige delingslenken. |
blocks.audio(url, title) | .mp3, .wav, .m4a… |
blocks.file(url, filename, size) | En nedlastingsrad. |
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 avsnitt under andre blokker. |
blocks.divider() | En vannrett 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 tabGi en svarknapp en uttrykkelig verdi når etiketten skal være lett å lese, men teksten boten matcher, ikke skal endres hver gang noen skriver om 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åndhever
- Every URL must be
https. The widget runs on an https page, so anything else is blocked by the browser anyway. - Ti blokker per melding, ti kort, tolv listerader 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 skjer 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.
Last opp filer
Ingen offentlig URL for GIF-en, kvitteringen eller databladet? Last opp filen, opptil 10 MB, og bruk 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 bane som widgeten løser relativt til sitt eget opphav. Dermed fungerer samme melding i utvikling, i en forhåndsvisning og i produksjon, også den dagen domenet deres endres.
Kontakter og besøkende
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 slått sammen, ikke erstattet. To jobber kan forvalte hver sine nøkler uten å overskrive hverandre. Opptil 30 nøkler per kall.
Besøkende
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 meldinger
Send til en økt uten å vente på et spørsmål, for eksempel fraktoppdateringer, påminnelser om prøveperioder eller gjenoppretting av handlekurver.
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 gjenbruker den aktive samtalen i økten eller åpner en ny. En åpen widget viser den ved neste polling; en lukket widget viser den neste gang den åpnes, merket som ulest.
Saker og artikler
Saker
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 offentlig svar åpner en løst sak på nytt, fordi et svar på noe som er merket som ferdig, nesten alltid betyr en ny runde.
Hjelpesenterartikler
Nyttig i begge retninger: søk derfra med en bot før dere åpner en sak, og send inn dokumentasjon fra verktøyet der dere 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 mot en tillatelsesliste med sikre tagger, gjennom samme prosess som redigeringsverktøyet i dashboardet bruker. Skript og stiler fjernes før noe lagres.
Bygg 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()Tilgjengeliggjøring
# 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()Abonner deretter endepunktet:
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 | Utløses av |
|---|---|
@bot.on_message | En melding fra en besøkende. Meldinger fra boter og agenter når aldri denne, noe som hindrer en bot i å svare seg selv. |
@bot.on_conversation_started | conversation.created , hilse eller angi 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 utløste et unntak. Uten en egen handler blir feil loggført og ignorert. |
Hva en handler får
| På ctx | Er |
|---|---|
ctx.text | Teksten i meldingen fra den besøkende. |
ctx.verified · ctx.verified_email | Om identiteten er verifisert, og hvilken adresse som er verifisert. |
ctx.conversation_id · ctx.widget_id · ctx.session | ID-er dere kan handle på. |
ctx.reply(...) | Answer. Takes everything conversations.reply does. |
ctx.note(...) | Et notat bare for agenter. |
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close() | Styr tråden. |
ctx.history() | Hent hele meldingshistorikken. Én forespørsel. |
ctx.client · ctx.event | Low-level-klienten og den tolkede hendelsen, for alt annet. |
Kjører dere mer enn é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())Bygg en plugin
En bot besvarer webhooks. En plugin går i motsatt retning: dere angir hva systemene kan gjøre, og KI-agenten bestemmer når funksjonene skal brukes mens den snakker med noen. Dere skriver aldri samtalelogikken, bare oppslaget.
Slik blir en butikk, et faktureringssystem eller et CRM-system vi aldri har hørt om, en fullverdig integrasjon.
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()Tilgjengeliggjør og registrer
# 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() oppretter eller oppdaterer integrasjonen slik at den samsvarer med koden, sletter handlingene dere har fjernet, og installerer den i widgetene. Ingenting eksponeres for KI-en før integrasjonen er installert.
De fem svarene
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.
Risikonivåer
| Risiko | Betyr |
|---|---|
public_read | Katalog, tilgjengelighet og dokumentasjon. Ingen identitet kreves. |
verified_read | Dataene til én person. Avvises til e-postadressen er verifisert. |
public_write | En endring som ikke krever identitet, for eksempel påmelding til nyhetsbrev eller registrering av leads. |
write | En endring i kontoen til én person. Verifisert, revidert og aldri forsøkt på nytt i det skjulte. |
req.verified_email er den eneste identiteten dere 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.
Reserverte handlinger
Noen navn har en egen betydning på plattformen. Deklarer ett av dem, så kobles tjenesten deres til funksjonen som allerede finnes for navnet.
| Navn | Hva dere 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 | Bestillingsstatus, alltid etter verifisering. |
orders.get_tracking | Sporing av forsendelser, alltid verifisert. |
orders.list_recent | De siste bestillingene til den besøkende, alltid verifisert. |
account.lookup | En vilkårlig enkel post om den verifiserte besøkende, for eksempel abonnement, kreditter eller fornyelsesdato. |
Test det før en besøkende gjør det
client.integrations.run("acme-store", "catalog.search_products",
{"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}Dette kjø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.
Svak: «Slå opp bestilling.»
Sterk: «Slå opp status og leveringsdato for en av den besøkendes egne bestillinger ved hjelp av ordrenummeret. Bruk dette når personen spør hvor noe er, eller når det kommer.»
Hva plattformen garanterer
- 8 sekunder timeout, 128 kB svargrense.
- Calls are signed with your
signing_secret— same scheme as webhooks. - URL-en deres løses én gang og bindes til en offentlig IP-adresse; omdirigeringer avvises.
- Alt dere returnerer, når modellen som data, aldri som instruksjoner, og nøkler som ser ut som hemmeligheter, blir fjernet.
- Egendefinerte integrasjoner krever et betalt abonnement med KI-agent. Opptil 20 per arbeidsområde og 40 handlinger i hver integrasjon.
Identitet
Alt som berører dataene til én person, venter til vi vet hvem personen er. Det skjer på én av to måter: med en kode på e-post eller gjennom nettstedets bekreftelse for en bruker som allerede er logget inn.
# 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å lenge bekreftelsen gjelder, behandler Conecto e-postadressen som verifisert: widgeten kjenner navnet, chatter knyttes til riktig kontakt, og flyter som ellers ville krevd en kode på e-post, for eksempel oppslag av bestillinger, refusjoner, kontodata eller verifiserte pluginhandlinger, fortsetter uten kode.
Gå bare god for økter som backend faktisk har autentisert. Bekreftelsen deres behandles som like sterk som vår egen kode på e-post, fordi dere faktisk har autentisert personen. Den er tidsbegrenset og knyttet til økten, så en ny nettleser trenger en ny bekreftelse. Nettleseren kan aldri merke seg selv som verifisert; kallet er autentisert og hører hjemme på serveren deres.
Webhooks og hendelser
Bot gjør alt dette for dere. Bruk low-level-hjelperne når dere vil verifisere i ytterkanten 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.verifiedVerifiser mot det ubehandlede innholdet. 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.
Hendelser
conversation.createdEn besøkende startet en samtale.message.createdEn besøkende sendte en melding. Utløseren for boten.conversation.closedEn samtale ble lukket.conversation.assignedTildelt til eller fjernet fra et teammedlem.conversation.handoffMerket som at hjelp fra et menneske er nødvendig.conversation.ratedEn CSAT-vurdering kom inn (1 til 5 samt en kommentar).ticket.createdEn sak ble åpnet, uansett kilde.ticket.updatedStatus, prioritet eller tildelt person for en sak ble endret.contact.createdEn ny person ble lagt til i CRM-systemet.visitor.identifiedEn økt ble identifisert via API-et.Leveringssemantikk
Leveringer skjer minst én gang og uten garantert rekkefø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-Timestamplar dere avvise gamle leveringer; SDK-en kontrollerer dem som standard med fem minutters toleranse.
signature = webhooks.sign(SECRET, raw_body) # what we send
webhooks.verify(raw_body, header_value, SECRET) # True / False, never raisesIntegrasjonskall vi utfører til your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.
Feil og nye forsøk
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")| Unntak | Status | Betyr |
|---|---|---|
AuthenticationError | 401 | Manglende, ukjent eller tilbakekalt autentiseringsopplysning. |
InvalidRequestError | 400 | Innholdet besto ikke valideringen; meldingen forklarer hvorfor. |
NotFoundError | 404 | Objektet finnes ikke i dette arbeidsområdet. |
ConflictError | 409 | Den identifikatoren er allerede i bruk. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | En grense for abonnementet eller arbeidsområdet. |
PlanRequired | 403 | Abonnementet omfatter ikke denne funksjonen. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | En avhengighet er ikke konfigurert. |
ServerError | 5xx | Vår feil. Trygt å prøve på nytt. |
BlockError | — | Utløst lokalt av en blokk-builder, før noen forespørsel. |
SignatureError | 401 | Verifiseringen av et webhook- eller integrasjonskall mislyktes. |
APIConnectionError · APITimeoutError | — | Forespørselen fikk aldri noe svar. |
Every exception carries code, message, status and the raw response.
Nye forsøk
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.
Skriveoperasjoner forsøkes også på nytt, noe som vanligvis er farlig. Her er det trygt fordi hver skriveoperasjon har en idempotensnøkkel, og et nytt forsøk gjenbruker den samme nøkkelen. Serveren kjenner igjen det andre forsøket som det samme kallet og gjentar det ikke.
client = Conecto(max_retries=0) # opt out entirelyGrenser det er verdt å kjenne til
| Grense | Verdi |
|---|---|
| Forespørsler per minutt og autentiseringsopplysning | 300 |
| Meldingsinnhold | 4000 tegn |
| Blokker per melding | 10 |
| Hurtigsvarsknapper | 6 |
| Last opp media | 10 MB |
| Integrasjoner per arbeidsområde | 20 |
| Handlinger per integrasjon | 40 |
| Webhooks per arbeidsområde | 10 |
client.meta.schema()["limits"] inneholder alltid de gjeldende tallene.
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 boter og plugins bør dere hoppe over signaturverifisering i tester i stedet for å signere fixtures:
bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)Metodereferanse
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 meldingene sine. since_id returnerer bare det som er nyere.
client.conversations.messages(id, since_id=None)Bare meldingene.
client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)Send en botmelding. Finnes også som send().
client.conversations.typing(id, on=True, name="Bot")Vis eller fjern skriveindikatoren.
client.conversations.handoff(id)Merk som at hjelp fra et menneske er nødvendig.
client.conversations.assign(id, user_id)None fjerner tildelingen.
client.conversations.close(id) · reopen(id) · set_status(id, status)Endre 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)Opprett eller oppdater etter e-postadresse. Finnes også som create().
client.contacts.update(id, **fields) · delete(id)Besøkende
client.visitors.get(widget_id, session)Identitet og verifiseringsstatus.
client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)Koble til brukeren deres; verified=True går god for identiteten.
client.visitors.unverify(widget_id, session)Trekk tilbake bekreftelsen. Kall ved utlogging.
client.visitors.message(widget_id, session, body, blocks, buttons, products)En proaktiv melding.
Integrasjoner
client.integrations.list() · get(slug) · actions(slug)actions() returnerer også katalogen over reserverte handlinger.
client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)Registrer én. Svaret inneholder signing_secret.
client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)replace_actions gjør listen deres autoritativ.
client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)Opprett eller oppdater, og installer deretter. Idempotent, så legg det i deployskriptet.
client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)actions er en tillatelsesliste; [] eksponerer ingenting.
client.integrations.run(slug, action, arguments, conversation_id)Kall gjennom den faktiske kallbanen.
client.integrations.delete(slug) · remove_action(slug, name)Saker, artikler, widgeter, 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 fullstendige konfigurasjonen ligger i widget.raw.
client.media.upload(file, filename, content_type)Returnerer et Media-objekt som kan sendes til en blokk-builder.
client.webhooks.list() · create(url, events, widget_id) · delete(id)Ukjente hendelsesnavn avvises 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 sentOppskrifter
Unngå en sak ved hjelp av hjelpesenteret
@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)Kvalifiser et lead og rut 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()Redd 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",
)Nattlig 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 butikk vi aldri 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()Dette er hele integrasjonen. Produktkort, Home-visningen og regelen som hindrer modellen i å lime inn rå URL-er, følger med det reserverte navnet.
Bygger dere noe? Snakk med oss , vi vil at folk skal bygge med Conecto og prioriterer det utviklere ber om. SDK-en har MIT-lisens og er open source; issues og pull requests er velkomne på GitHub.