Utvecklare / Python
Den Python-SDK
Läs och besvara chattar, skicka bilder och video, bygg botar och låt AI-agenten slå upp information i era egna system med Python. Allt som REST API:t gör, med de besvärliga delarna redan hanterade: kontroll av att en begäran verkligen kom från oss, undvikande av dubbla svar på samma händelse, säkra nya försök och upptäckt av felaktiga meddelanden innan de lämnar processen.
Ny på Conecto? Börja här , fem minuters begrepp, sedan blir resten av sidan självklar.
pip install conectov0.1.0Python 3.9+MIT One dependency (requests) · Type hints throughout · På PyPI
Börja här
Om ni aldrig har använt Conecto tidigare innehåller detta avsnitt hela tankemodellen. Fem minuter här gör resten av sidan självklar.
Vad Conecto är
En chattwidget på er webbplats. Besökare skriver i den; ert team, en AI-agent eller er egen kod svarar. Allt nedan är ett sätt att föra in er kod i den loopen.
De fem substantiven
Nästan alla metoder i denna SDK tar emot eller returnerar ett av dessa objekt.
Arbetsyta
Ert konto. Er autentiseringsuppgift tillhör exakt ett konto och kan aldrig se ett annat.
Widget
One installed chat box. You might have one per website or brand. Has an id.
Besökare
A browser on your site, identified by a session string stored in that browser.
Samtal
One thread between a visitor and you. Has an id, and a status of open or closed.
Meddelande
One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.
De tre saker ni kan bygga
1. Ett skript
Körs enligt ert schema. Synkroniserar kontakter, öppnar ärenden och skickar kampanjmeddelanden. Inget behöver driftas; skriptet anropar bara API:t.
2. En bot
En av era webbservrar som vi meddelar när en besökare skriver. Ni bestämmer svaret. Ni styra konversationen.
3. Ett plugin
En av era webbservrar som vi anropar när AI behöver en uppgift som bara ni har. AI:n styr konversationen; ni tillhandahåller uppslag.
Bot eller plugin? Om ni vill skriva orden som besökaren läser ska ni bygga en bot. Om ni hellre låter AI:n prata och bara ger den åtkomst till era data, till exempel beställningar, abonnemang och lagersaldon, ska ni bygga ett plugin. De flesta team väljer till slut ett plugin.
Ord som används på sidan
Jargong, definierad en gång. Ingenting här är specifikt för Conecto utom de två sista begreppen.
Vilket avsnitt behöver jag?
| Jag vill… | Gå till |
|---|---|
| Läs eller besvara chattar från ett skript | Samtal |
| Skicka en bild, GIF-fil, video eller ett kort | Rikt innehåll |
| Svara besökare automatiskt med min egen logik | Bygg en bot |
| Låt AI:n slå upp information i mina system | Bygg ett plugin |
| Anslut en butik som AI:n kan söka i | Bygg ett plugin |
| Hoppa över e-postkoden för användare som redan är inloggade | Identitet |
| Håll mitt CRM-system synkroniserat | Kontakter och besökare |
| Veta vad jag ska fånga när något går fel | Fel och nya försök |
Installera och autentisera
pip install conectoSkapa en autentiseringsuppgift i Inställningar → Utvecklare. Ni får en klient-ID (ck_…) and a hemlighet (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.
Snabbstart
Tre saker ni kan göra under de första fem minuterna.
Läs vad som händer
for convo in client.conversations.list(status="open"):
who = convo.visitor.email if convo.visitor else "anonymous"
print(f"#{convo.id} {who} {convo.preview}")Svara någon
convo = client.conversations.get(4821)
print(convo.messages[-1].body) # what they said last
convo.reply("On it — give me one moment.")Skicka något som inte är text
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å är hela biblioteket uppbyggt: en klient med resurser, objekt som kan agera på sig själva och builders för allt strukturerat.
Klienten
Skapa en och behåll den. Den återanvänder anslutningar och kan delas säkert mellan trådar.
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 | Anger |
|---|---|
CONECTO_CLIENT_ID | Klient-ID:t. |
CONECTO_SECRET | Hemligheten. |
CONECTO_BASE_URL | API:ts bas-URL. Behövs sällan. |
Resurser
| Attribut | Omfattar |
|---|---|
client.conversations | Lista, läsa, svara, visa skrivstatus, lämna över, tilldela och stänga. |
client.contacts | CRM-systemet: skapa eller uppdatera via e-postadress, sök, uppdatera och ta bort. |
client.visitors | Sessioner, identitetsintygande och proaktiva meddelanden. |
client.tickets | Skapa, uppdatera, svara och hantera kategorier. |
client.articles | Sök och publicera innehåll i hjälpcentret. |
client.integrations | Registrera, installera, kör och driftsätt pluginer. |
client.widgets | Läs och uppdatera widgetkonfigurationen. |
client.media | Ladda upp filer för användning i block. |
client.webhooks | Prenumerera endpoints på händelser. |
client.members | Teammedlemmar, för tilldelning. |
client.meta | me(), schema(), stats(). |
Versionsavvikelse
Varje svar innehåller serverns API-version. Logga den vid start. Om versionen ändras och ett fält ni använder försvinner är detta det första ni vill känna till.
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() returnerar hela API:t som JSON, inklusive endpoints, händelser, blocktyper, reserverade åtgärder, felkoder och alla gränser. Det levereras av samma kod som tillämpar reglerna och kan därför inte glida från verkligheten.
Samtal
A thread between a visitor and your workspace. Messages have a sender av visitor, agent, bot eller system.
Lista och läsa
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)Svara
convo.reply("I can refund order #1284 — confirm?",
buttons=["Yes, refund it", "Talk to a human"])Ett tryck på en snabbsvarsknapp återkommer som ett vanligt besökarmeddelande med den texten, så er handler behöver inget specialfall.
| Argument | Gör |
|---|---|
body | Texten, upp till 4 000 tecken. |
blocks | Rich content, se Rikt innehåll. |
buttons | Upp till 6 snabbsvar. |
products | Upp till 4 produktkort, samma antal som den inbyggda AI:n bifogar. |
ask_email | Visar det inbyggda formuläret för insamling av e-postadresser. |
ticket_form | Bifogar det inbyggda formuläret för att öppna ett ärende. |
internal | En anteckning enbart för handläggare. Besökaren ser den aldrig. |
idempotency_key | Skicka med ett leverans-ID för webhooken så att ett nytt försök inte kan publicera dubbelt. |
Styrning 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 Fel.
Rikt innehåll
Varje meddelande kan innehålla upp till tio block: bilder och GIF-filer, video, ljud, filer, inbäddningar från tredje part, kortkaruseller, etikett/värde-listor, knappar och avdelare. De visas i widgeten och i handläggarens inkorg.
Block är vanliga dictionaries vid överföringen, så ni skulle kunna skriva dem för hand. Builders finns eftersom en felstavad nyckel kan leda till att en kund säger att bilden aldrig visades. De tillämpar samma regler som servern medan traceback fortfarande pekar på er kod.
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")]),
]),
])Alla blocktyper
| Builder | Anteckningar |
|---|---|
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 och Spotify. Skicka den vanliga delningslänken. |
blocks.audio(url, title) | .mp3, .wav, .m4a… |
blocks.file(url, filename, size) | En nedladdningsrad. |
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 och link_button. |
blocks.text(body) | Ett extra stycke under andra block. |
blocks.divider() | En horisontell linje. |
Två sorters knappar
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 tabGe en svarsknapp ett uttryckligt värde när etiketten ska vara lättläst men texten som boten matchar inte ska ändras varje gång någon skriver om texten.
Skicka en GIF-fil
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 tillämpar
- Every URL must be
https. The widget runs on an https page, so anything else is blocked by the browser anyway. - Tio block per meddelande, tio kort, tolv listrader och sex knappar.
- 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.
Ladda upp filer
Saknas en publik URL för GIF-filen, kvittot eller databladet? Ladda upp filen, upp till 10 MB, och använd 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 är en sökväg som widgeten löser relativt sitt eget ursprung. Samma meddelande fungerar därför under utveckling, i en förhandsgranskning och i produktion, även den dag er domän ändras.
Kontakter och besökare
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 är sammanfogade, inte ersatta. Två jobb kan hantera sina egna nycklar utan att skriva över varandra. Upp till 30 nycklar per anrop.
Besökare
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.verifiedProaktiva meddelanden
Skicka till en session utan att vänta på en fråga, till exempel leveransuppdateringar, påminnelser om provperioder eller återställning av varukorgar.
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 återanvänder sessionens aktiva konversation eller öppnar en ny. En öppen widget visar den vid nästa pollning; en stängd widget visar den nästa gång den öppnas, markerad som oläst.
Ärenden och artiklar
Ärenden
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 noteEtt offentligt svar öppnar ett löst ärende igen, eftersom ett svar på något som markerats som klart nästan alltid innebär en ny omgång.
Hjälpcenterartiklar
Användbart i båda riktningarna: sök därifrån med en bot innan ni öppnar ett ärende och skicka in dokumentation från det verktyg där ni 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 rensas på serversidan mot en tillåtelselista med säkra taggar, genom samma process som redigeraren i dashboarden använder. Skript och stilar tas bort innan något lagras.
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()Tillhandahålla
# 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()Prenumerera sedan endpointen:
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
| Dekorator | Utlöses av |
|---|---|
@bot.on_message | Ett meddelande från en besökare. Meddelanden från botar och handläggare når aldrig denna, vilket hindrar en bot från att svara sig själv. |
@bot.on_conversation_started | conversation.created , hälsa eller ange tillstånd. |
@bot.on_rating | conversation.rated. ctx.event.rating has score och 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 ett undantag. Utan en egen handler loggas och ignoreras felen. |
Vad en handler får
| På ctx | Är |
|---|---|
ctx.text | Texten i besökarens meddelande. |
ctx.verified · ctx.verified_email | Om identiteten har verifierats och vilken adress som är verifierad. |
ctx.conversation_id · ctx.widget_id · ctx.session | ID:n som ni kan agera på. |
ctx.reply(...) | Answer. Takes everything conversations.reply does. |
ctx.note(...) | En anteckning enbart för handläggare. |
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close() | Styr tråden. |
ctx.history() | Hämta hela meddelandehistoriken. En begäran. |
ctx.client · ctx.event | Low-level-klienten och den tolkade händelsen, för allt annat. |
Kör ni fler än en 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 ett plugin
En bot besvarar webhooks. En plugin går åt andra hållet: ni anger vad era system kan göra och AI-agenten bestämmer när funktionerna ska användas under ett samtal. Ni skriver aldrig konversationslogiken, bara uppslaget.
Så blir en butik, ett faktureringssystem eller ett CRM-system som vi aldrig har hört talas om en förstklassig 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()Tillhandahåll och registrera
# 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() skapar eller uppdaterar integrationen så att den motsvarar er kod, tar bort åtgärder som ni har tagit bort och installerar den i era widgetar. Ingenting exponeras för AI:n innan integrationen har installerats.
De fem svaren
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.
Risknivåer
| Risk | Betyder |
|---|---|
public_read | Katalog, tillgänglighet och dokumentation. Ingen identitet krävs. |
verified_read | En persons uppgifter. Avvisas tills e-postadressen har verifierats. |
public_write | En ändring som inte kräver identitet, till exempel registrering för nyhetsbrev eller insamling av leads. |
write | En ändring av en persons konto. Verifierad, granskad och aldrig tyst omprövad. |
req.verified_email är den enda identitet ni kan lita på. An email in req.arguments is whatever the model parsed out of a chat. For verified_read och 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.
Reserverade åtgärder
Några namn har en särskild betydelse för plattformen. Deklarera ett av dem så kopplas er tjänst till den funktion som redan finns för namnet.
| Namn | Vad ni 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 | Beställningsstatus, alltid efter verifiering. |
orders.get_tracking | Leveransspårning, alltid verifierad. |
orders.list_recent | Besökarens senaste beställningar, alltid verifierade. |
account.lookup | Valfri enkel post om den verifierade besökaren, till exempel abonnemang, saldo eller förnyelsedatum. |
Testa innan en besökare gör det
client.integrations.run("acme-store", "catalog.search_products",
{"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}Detta kör den verkliga 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 bra beskrivning
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å upp beställning.”
Stark: ”Slå upp status och leveransdatum för en av besökarens egna beställningar med hjälp av ordernumret. Använd detta när personen frågar var något är eller när det kommer.”
Vad plattformen garanterar
- 8 sekunder timeout, 128 kB svarsgräns.
- Calls are signed with your
signing_secret— same scheme as webhooks. - Er URL löses en gång och binds till en publik IP-adress; omdirigeringar avvisas.
- Allt ni returnerar når modellen som data, aldrig som instruktioner, och nycklar som ser ut att innehålla hemligheter tas bort.
- Anpassade integrationer kräver ett betalt abonnemang med AI-agent. Upp till 20 per arbetsyta och 40 åtgärder i varje integration.
Identitet
Allt som berör en persons uppgifter väntar tills vi vet vem personen är. Det sker på ett av två sätt: med en kod via e-post eller genom er webbplats intygande för en användare som redan är inloggad.
# 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 intygandet gäller behandlar Conecto e-postadressen som verifierad: widgeten känner till namnet, chattar kopplas till rätt kontakt och flöden som annars skulle kräva en kod via e-post, som beställningsuppslag, återbetalningar, kontouppgifter eller verifierade pluginåtgärder, fortsätter utan kod.
Intyga bara sessioner som er backend faktiskt har autentiserat. Ert intygande behandlas som lika starkt som vår egen kod via e-post, eftersom ni verkligen har autentiserat personen. Det är tidsbegränsat och knutet till sessionen, så en ny webbläsare behöver ett nytt intygande. Webbläsaren kan aldrig själv markera sig som verifierad; anropet är autentiserat och hör hemma på er server.
Webhooks och händelser
Bot gör allt detta åt er. Använd low-level-hjälparna när ni vill verifiera vid gränsen och behandla 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.verifiedVerifiera mot det obehandlade innehållet. 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ökare startade en konversation.message.createdEn besökare skickade ett meddelande. Botens trigger.conversation.closedEn konversation stängdes.conversation.assignedTilldelad till eller borttagen från en teammedlem.conversation.handoffMarkerad som att mänsklig hjälp behövs.conversation.ratedEtt CSAT-betyg kom in (1 till 5 samt en kommentar).ticket.createdEtt ärende öppnades, oavsett källa.ticket.updatedStatus, prioritet eller tilldelad person för ett ärende ändrades.contact.createdEn ny person lades till i CRM-systemet.visitor.identifiedEn session identifierades via API:t.Leveranssemantik
Leveranser sker minst en gång och utan garanterad ordning: 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-Timestampgör att ni kan avvisa gamla leveranser; SDK:n kontrollerar dem som standard med fem minuters tolerans.
signature = webhooks.sign(SECRET, raw_body) # what we send
webhooks.verify(raw_body, header_value, SECRET) # True / False, never raisesIntegrationsanrop som vi gör till your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.
Fel och nya försö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")| Undantag | Status | Betyder |
|---|---|---|
AuthenticationError | 401 | Saknad, okänd eller återkallad autentiseringsuppgift. |
InvalidRequestError | 400 | Innehållet klarade inte valideringen; meddelandet anger varför. |
NotFoundError | 404 | Objektet finns inte i denna arbetsyta. |
ConflictError | 409 | Den identifieraren används redan. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | En gräns för abonnemanget eller arbetsytan. |
PlanRequired | 403 | Abonnemanget omfattar inte denna funktion. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | Ett beroende är inte konfigurerat. |
ServerError | 5xx | Vårt fel. Säkert att försöka igen. |
BlockError | — | Utlöst lokalt av en block-builder, före någon begäran. |
SignatureError | 401 | Verifieringen av ett webhook- eller integrationsanrop misslyckades. |
APIConnectionError · APITimeoutError | — | Begäran fick aldrig något svar. |
Every exception carries code, message, status and the raw response.
Nya försö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.
Skrivningar försöks också igen, vilket normalt är farligt. Här är det säkert eftersom varje skrivning har en idempotensnyckel och ett nytt försök återanvänder samma nyckel. Servern känner igen det andra försöket som samma anrop och upprepar det inte.
client = Conecto(max_retries=0) # opt out entirelyGränser som är bra att känna till
| Gräns | Värde |
|---|---|
| Begäranden per minut och autentiseringsuppgift | 300 |
| Meddelandeinnehåll | 4 000 tecken |
| Block per meddelande | 10 |
| Snabbsvarsknappar | 6 |
| Ladda upp media | 10 MB |
| Integrationer per arbetsyta | 20 |
| Åtgärder per integration | 40 |
| Webhooks per arbetsyta | 10 |
client.meta.schema()["limits"] innehåller alltid de aktuella siffrorna.
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.
Testar
The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete och 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/"För botar och pluginer ska ni hoppa över signaturverifieringen i tester i stället för att signera fixtures:
bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)Metodreferens
Samtal
client.conversations.list(status, widget_id, session, limit, before_id)Konversationer, senaste aktivitet först. Returnerar en Page.
client.conversations.get(id, since_id=None)En konversation med sina meddelanden. since_id returnerar bara det som är nyare.
client.conversations.messages(id, since_id=None)Endast meddelandena.
client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)Skicka ett botmeddelande. Finns även som send().
client.conversations.typing(id, on=True, name="Bot")Visa eller rensa skrivindikatorn.
client.conversations.handoff(id)Markera som att mänsklig hjälp behövs.
client.conversations.assign(id, user_id)None tar bort tilldelningen.
client.conversations.close(id) · reopen(id) · set_status(id, status)Ändra status.
Kontakter
client.contacts.list(email, query, limit, before_id) · get(id) · find(email)find() returnerar en kontakt eller None.
client.contacts.upsert(email, name, title, company, location, phone, notes, custom_fields)Skapa eller uppdatera via e-postadress. Finns även som create().
client.contacts.update(id, **fields) · delete(id)Besökare
client.visitors.get(widget_id, session)Identitet och verifieringsstatus.
client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)Koppla er användare; verified=True intygar identiteten.
client.visitors.unverify(widget_id, session)Återkalla intygandet. Anropa vid utloggning.
client.visitors.message(widget_id, session, body, blocks, buttons, products)Ett proaktivt meddelande.
Integrationer
client.integrations.list() · get(slug) · actions(slug)actions() returnerar även katalogen över reserverade åtgärder.
client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)Registrera en. Svaret innehåller signing_secret.
client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)replace_actions gör er lista styrande.
client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)Skapa eller uppdatera och installera sedan. Idempotent, så lägg det i ert deployskript.
client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)actions är en tillåtelselista; [] exponerar ingenting.
client.integrations.run(slug, action, arguments, conversation_id)Anropa via den verkliga anropsvägen.
client.integrations.delete(slug) · remove_action(slug, name)Ärenden, artiklar, widgetar, media, webhooks och metadata
client.tickets.list · get · create · update · reply · categoriesclient.articles.list · get · create · update · deleteclient.widgets.get(id) · update(id, **fields)Den fullständiga konfigurationen finns i widget.raw.
client.media.upload(file, filename, content_type)Returnerar ett Media-objekt som kan skickas till en block-builder.
client.webhooks.list() · create(url, events, widget_id) · delete(id)Okända händelsenamn avvisas 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 sentRecept
Undvik ett ärende med hjälp av ert hjälpcenter
@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)Kvalificera ett lead och dirigera 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()Rädda ett dåligt betyg
@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 butik vi aldrig har hört talas 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 är hela integrationen. Produktkort, Home-visningen och regeln som hindrar modellen från att klistra in obehandlade URL:er följer med det reserverade namnet.
Bygger ni något? Prata med oss , vi vill att människor ska bygga med Conecto och prioriterar det som utvecklare efterfrågar. SDK:n har MIT-licens och är open source; issues och pull requests är välkomna på GitHub.