Developers / Python

De Python-SDK

Lees en beantwoord chats, stuur afbeeldingen en video, bouw bots en laat de AI-agent vanuit Python gegevens in uw eigen systemen opzoeken. Alles wat de REST API doet, met de lastige onderdelen al geregeld: controleren of een verzoek echt van ons kwam, niet tweemaal op dezelfde gebeurtenis antwoorden, veilig opnieuw proberen en een ongeldig bericht afvangen voordat het uw proces verlaat.

Nieuw bij Conecto? Begin hier , vijf minuten terminologie, waarna de rest van deze pagina voor zich spreekt.

pip install conectov0.1.0Python 3.9+MIT

One dependency (requests) · Type hints throughout · Op PyPI

Begin hier

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Als u Conecto nog nooit hebt gebruikt, bevat dit gedeelte het volledige denkmodel. Vijf minuten hier maakt de rest van de pagina duidelijk.

Wat Conecto is

Een chatwidget op uw website. Bezoekers typen erin; uw team, een AI-agent of uw eigen code antwoordt. Alles hieronder is een manier om uw code in die cyclus.

De vijf zelfstandige naamwoorden

Bijna elke methode in deze SDK accepteert of retourneert een van deze objecten.

Workspace

Uw account. Uw referentie behoort tot precies één account en kan nooit een ander account zien.

Widget

One installed chat box. You might have one per website or brand. Has an id.

Bezoeker

A browser on your site, identified by a session string stored in that browser.

Gesprek

One thread between a visitor and you. Has an id, and a status of open or closed.

Bericht

One bubble in a thread. Sent by a visitor, an agent (human), a bot, or the system.

De drie dingen die u kunt bouwen

1. Een script

Wordt volgens uw planning uitgevoerd. Synchroniseert contacten, opent tickets en verstuurt campagneberichten. U hoeft niets te hosten; het script roept alleen de API aan.

2. Een bot

Een webserver van u die we informeren wanneer een bezoeker schrijft. U bepaalt het antwoord. U het gesprek te sturen.

3. Een plugin

Een webserver van u die we aanroepen wanneer de AI heeft een gegeven nodig dat alleen u bezit. De AI stuurt het gesprek; u levert de zoekopdrachten.

Bot of plugin? Wilt u de woorden schrijven die de bezoeker leest, bouw dan een bot. Wilt u liever dat de AI praat en alleen toegang krijgt tot uw gegevens, zoals bestellingen, abonnementen en voorraadniveaus, bouw dan een plugin. De meeste teams kiezen uiteindelijk voor een plugin.

Woorden die deze pagina gebruikt

Jargon, eenmaal gedefinieerd. Niets hier is specifiek voor Conecto, behalve de laatste twee termen.

WebhookWanneer er iets gebeurt, sturen we een HTTP-verzoek naar een URL van u in plaats van u herhaaldelijk te laten navragen. U hebt een server nodig die vanaf internet bereikbaar is.
HandtekeningEen hash die we aan elk verzoek toevoegen, berekend met een geheim dat alleen u en wij kennen. Controle ervan bewijst dat het verzoek echt van ons kwam en niet is gewijzigd. De SDK doet dit voor u.
Ten minste eenmaalAls we niet kunnen vaststellen of u iets hebt ontvangen, sturen we het opnieuw. U kunt dezelfde gebeurtenis dus af en toe tweemaal ontvangen. Uw code moet dat herkennen in plaats van tweemaal te handelen.
IdempotentTweemaal uitvoeren geeft hetzelfde resultaat als eenmaal uitvoeren. Stuurt u tweemaal een bericht met dezelfde sleutel, dan retourneert de tweede aanroep het eerste bericht in plaats van een duplicaat te plaatsen.
CursorpagineringLange lijsten komen in pagina’s binnen. In plaats van «pagina 3» geeft u de ID door die u de vorige keer ontving. De SDK verbergt dit proces; u hoeft alleen te itereren.
BlokEen element met rijke inhoud in een bericht, zoals een afbeelding, video, kaart of rij knoppen. Zie Rich content.
Geverifieerde bezoekerIemand van wie we het e-mailadres hebben geverifieerd , door een code per e-mail te sturen of omdat uw site ons heeft laten weten dat de persoon is ingelogd. Alles wat de privégegevens van één persoon raakt, wacht op deze bevestiging.

Welke sectie heb ik nodig?

Ik wil…Ga naar
Chats lezen of beantwoorden vanuit een scriptGesprekken
Een afbeelding, GIF, video of kaart verzendenRich content
Bezoekers automatisch antwoorden met mijn eigen logicaEen bot bouwen
De AI gegevens in mijn systemen laten opzoekenEen plugin bouwen
Een winkel koppelen waarin de AI kan zoekenEen plugin bouwen
De e-mailcode overslaan voor reeds ingelogde gebruikersIdentiteit
Mijn CRM gesynchroniseerd houdenContacten en bezoekers
Weten wat ik moet afvangen als iets misluktFouten en nieuwe pogingen

Installeren en verifiëren

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok
pip install conecto

Maak een referentie in Instellingen → Ontwikkelaars. U ontvangt een client-ID (ck_…) and a geheim (cs_…). The secret is shown once and stored hashed, so put it somewhere your code can read and your repository cannot.

export CONECTO_CLIENT_ID=ck_your_client_id
export CONECTO_SECRET=cs_your_secret
from conecto import Conecto

client = Conecto()                    # reads the environment
client = Conecto("ck_...", "cs_...")  # or pass them directly

print(client.ping()["workspace"]["name"])

Call ping() once at startup. It raises AuthenticationError immediately if the credential is wrong or revoked — much better than finding out mid-conversation. A credential can cover the workspace or be scoped to one widget; a scoped one simply cannot see other widgets' conversations.

Snelstart

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Drie dingen die u in de eerste vijf minuten kunt doen.

Lezen wat er gebeurt

for convo in client.conversations.list(status="open"):
    who = convo.visitor.email if convo.visitor else "anonymous"
    print(f"#{convo.id}  {who}  {convo.preview}")

Iemand antwoorden

convo = client.conversations.get(4821)
print(convo.messages[-1].body)          # what they said last
convo.reply("On it — give me one moment.")

Iets anders dan tekst verzenden

from conecto import blocks

convo.reply("Here's how to reset it:", blocks=[
    blocks.image("https://cdn.you.com/reset.gif", alt="Reset flow"),
    blocks.buttons([
        blocks.reply_button("That worked"),
        blocks.link_button("Full guide", "https://docs.you.com/reset"),
    ]),
])

Dit is de opbouw van de hele bibliotheek: een client met resources, objecten die zelf acties kunnen uitvoeren en builders voor alles met structuur.

De client

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Maak er één en bewaar deze. De client hergebruikt verbindingen en kan veilig tussen threads worden gedeeld.

client = Conecto(
    client_id="ck_...",
    secret="cs_...",
    base_url="https://conecto.chat/api/v1",  # override for staging
    timeout=30.0,        # seconds per request
    max_retries=2,       # timeouts, 429s and 5xx
    app="acme-billing",  # added to the User-Agent; shows up in our logs
)

Omgeving

VariabeleStelt in
CONECTO_CLIENT_IDDe client-ID.
CONECTO_SECRETHet geheim.
CONECTO_BASE_URLBasis-URL van de API. Zelden nodig.

Bronnen

AttribuutOmvat
client.conversationsWeergeven, lezen, antwoorden, typen, overdragen, toewijzen en sluiten.
client.contactsHet CRM: maken of bijwerken op e-mailadres, zoeken, bijwerken en verwijderen.
client.visitorsSessies, identiteitsbevestiging en proactieve berichten.
client.ticketsMaken, bijwerken, antwoorden en categorieën beheren.
client.articlesHelpcenterinhoud zoeken en publiceren.
client.integrationsPlugins registreren, installeren, uitvoeren en deployen.
client.widgetsWidgetconfiguratie lezen en bijwerken.
client.mediaBestanden uploaden voor gebruik in blokken.
client.webhooksEndpoints op gebeurtenissen abonneren.
client.membersTeamleden, voor toewijzing.
client.metame(), schema(), stats().

Versieverschil

Elke reactie bevat de API-versie van de server. Registreer deze bij het opstarten. Als de versie verandert en een gebruikt veld verdwijnt, is dit het eerste wat u wilt weten.

print(client.api_version)   # what the server last reported
print(client.sdk_version)   # what this SDK was built against

limits = client.meta.schema()["limits"]
assert limits["blocks_per_message"] >= 4

client.meta.schema() retourneert de volledige API als JSON, met endpoints, gebeurtenissen, bloktypen, gereserveerde acties, foutcodes en alle limieten. Deze wordt geleverd door dezelfde code die de regels afdwingt en kan daardoor niet van de werkelijkheid afwijken.

Gesprekken

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

A thread between a visitor and your workspace. Messages have a sender van visitor, agent, bot van system.

Weergeven en lezen

page = client.conversations.list(status="open", limit=25)

page.items                     # this page only
for convo in page:             # every page, fetched lazily
    ...

convo = client.conversations.get(4821)
convo.status, convo.visitor.email, convo.messages[-1].body

# Poll cheaply: only what is newer than what you already have.
fresh = client.conversations.messages(4821, since_id=last_seen_id)

Antwoorden

convo.reply("I can refund order #1284 — confirm?",
            buttons=["Yes, refund it", "Talk to a human"])

Een tik op een snelantwoordknop komt terug als een gewoon bezoekersbericht met die tekst. Uw handler heeft er dus geen speciaal geval voor nodig.

ArgumentDoet
bodyDe tekst, maximaal 4000 tekens.
blocksRijke inhoud, zie Rich content.
buttonsMaximaal 6 snelle antwoorden.
productsMaximaal 4 productkaarten, gelijk aan wat de ingebouwde AI toevoegt.
ask_emailToont het ingebouwde formulier voor het verzamelen van e-mailadressen.
ticket_formVoegt het ingebouwde formulier voor het openen van een ticket toe.
internalEen notitie die alleen medewerkers zien. De bezoeker ziet deze nooit.
idempotency_keyGeef de ID van een webhookbezorging door, zodat een nieuwe poging niet tweemaal kan plaatsen.

De thread sturen

convo.typing()                  # "…is typing", expires after ~8s
convo.reply("Working on it…")

convo.reply("Escalating: refund over policy limit.", internal=True)
convo.handoff()                 # flag for a human; routing rules apply
convo.assign(user_id=12)        # ids come from client.members.list()

convo.close()
convo.reopen()

Writing to a closed thread raises ConversationClosed. Reopen it first, or catch it — see Fouten.

Rich content

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Elk bericht kan maximaal tien bevatten blokken: afbeeldingen en GIF’s, video, audio, bestanden, ingesloten inhoud van derden, kaartcarrousels, label/waardelijsten, knoppen en scheidingslijnen. Ze worden weergegeven in de widget en in de inbox van de medewerker.

Blokken zijn tijdens de overdracht gewone dictionaries, dus u zou ze handmatig kunnen schrijven. De builders bestaan omdat een verkeerd gespelde sleutel ertoe leidt dat een klant meldt dat de afbeelding nooit verscheen. Ze passen dezelfde regels als de server toe op het punt waar de traceback nog naar uw code wijst.

from conecto import blocks

convo.reply("Here's your order:", blocks=[
    blocks.list_block([
        blocks.row("Placed", "12 July 2026"),
        blocks.row("Status", "Shipped"),
        blocks.row("Carrier", "DHL", subtitle="Tracking 4Z8871",
                   url="https://track.dhl.com/4Z8871"),
    ], title="Order #1042"),
    blocks.divider(),
    blocks.cards([
        blocks.card("Trail Runner 2",
                    subtitle="Road · Neutral",
                    price="89.00 USD",
                    badge="Back in stock",
                    image="https://cdn.you.com/tr2.jpg",
                    url="https://shop.you.com/p/tr2",
                    buttons=[blocks.link_button("Buy", "https://shop.you.com/cart")]),
    ]),
])

Elk bloktype

BuilderNotities
blocks.image(url, alt, caption, link)Animated GIFs are images — point at the .gif. blocks.gif is an alias.
blocks.video(url, poster, autoplay, loop, muted)Direct files only (.mp4, .webm, .mov…).
blocks.embed(url)YouTube, Vimeo, Loom, Wistia en Spotify. Geef de gewone deellink door.
blocks.audio(url, title).mp3, .wav, .m4a
blocks.file(url, filename, size)Een downloadrij.
blocks.cards([...], layout)carousel scrolls sideways, list stacks full width.
blocks.list_block([...], title)Label/value rows. blocks.rows is an alias.
blocks.buttons([...])Mix reply_button en link_button.
blocks.text(body)Een extra alinea onder andere blokken.
blocks.divider()Een horizontale scheidingslijn.

Twee soorten knoppen

blocks.reply_button("Yes, refund it")                    # sends that text back
blocks.reply_button("Yes, refund it", "confirm_refund")  # nice label, stable value
blocks.link_button("Read the policy", "https://you.com/refunds")  # opens a tab

Geef een antwoordknop een expliciete waarde wanneer het label goed leesbaar moet zijn, maar de tekst waarop uw bot reageert niet telkens mag veranderen wanneer iemand de tekst herschrijft.

Een GIF verzenden

A .gif works. A muted, looping, autoplaying video is usually the better trade — a fraction of the bytes, and nobody can tell.

blocks.video("https://cdn.you.com/reset.mp4",
             autoplay=True, loop=True, muted=True)

Regels die de builders afdwingen

  • Every URL must be https. The widget runs on an https page, so anything else is blocked by the browser anyway.
  • Tien blokken per bericht, tien kaarten, twaalf lijstrijen en zes knoppen.
  • Long strings are truncated, not rejected. Structural mistakes raise BlockError.
  • A YouTube link in a video block raises, and tells you to use embed.

Validatie vindt lokaal plaats. reply(blocks=…) checks the list before sending, so a typo raises BlockError from your line rather than coming back as an HTTP 400 from inside the client.

Bestanden uploaden

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Geen openbare URL voor die GIF, bon of datasheet? Upload het bestand, tot 10 MB, en gebruik het resultaat.

media = client.media.upload("celebrate.gif")     # a path
media = client.media.upload(open("a.pdf", "rb")) # a file object
media = client.media.upload(raw_bytes, filename="receipt.png")

convo.reply("All set!", blocks=[blocks.image(media)])

A Media can be passed straight to blocks.image, blocks.video van blocks.file.

Use media.url, not media.absolute_url. url is een pad dat de widget ten opzichte van zijn eigen oorsprong oplost. Hetzelfde bericht werkt daardoor tijdens ontwikkeling, in een previewdeployment en in productie, ook wanneer uw domein verandert.

Contacten en bezoekers

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Contacten

The CRM. upsert is keyed on email, so it is safe to point a nightly sync at.

client.contacts.upsert(
    "maya@acme.io",
    name="Maya Silva",
    company="Acme",
    custom_fields={"plan": "scale", "mrr": "480"},
)

contact = client.contacts.find("maya@acme.io")     # or None
for c in client.contacts.list(query="acme"):
    print(c.email, c.custom_fields.get("plan"))

custom_fields is samengevoegd, niet vervangen. Twee taken kunnen elk hun eigen sleutels beheren zonder elkaar te overschrijven. Maximaal 30 sleutels per aanroep.

Bezoekers

A browser session on a widget, keyed by (widget_id, session). The session id lives in the visitor's browser; read it client-side and send it to your backend.

visitor = client.visitors.get(widget_id=7, session=session_id)
visitor.email, visitor.verified

Proactieve berichten

Stuur iets naar een sessie zonder op een vraag te wachten, zoals verzendupdates, proefperiodeherinneringen of winkelwagenherstel.

client.visitors.message(
    widget_id=7, session=session_id,
    body="Still thinking it over? Your cart is saved:",
    blocks=[blocks.cards([
        blocks.card("Trail Runner 2", price="89.00 USD",
                    image="https://cdn.you.com/tr2.jpg",
                    url="https://shop.you.com/cart"),
    ])],
)

De actieve conversatie van de sessie wordt hergebruikt of er wordt een nieuwe geopend. Een geopende widget toont deze bij de volgende poll; een gesloten widget toont deze bij het opnieuw openen als ongelezen.

Tickets en artikelen

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Tickets

ticket = client.tickets.create(
    email="maya@acme.io",
    message="Card was declined on renewal.",
    priority="high",          # low · normal · high · urgent
)

client.tickets.update(ticket.id, status="pending", assignee_user_id=12)
ticket.reply("We've fixed the card on file — try again?")   # emails the requester
ticket.reply("Billing confirmed the retry.", internal=True)  # private note

Een openbaar antwoord heropent een opgelost ticket, omdat een antwoord op iets dat als afgerond is gemarkeerd vrijwel altijd een nieuwe ronde betekent.

Helpcenterartikelen

Nuttig in beide richtingen: zoek erin vanuit een bot voordat u een ticket opent en stuur documentatie vanuit de omgeving waarin u die opstelt.

hits = client.articles.list(query="refund", published=True)
if hits:
    convo.reply(f"This might help: “{hits[0].title}”")

client.articles.create("Shipping times", "<p>2-4 business days.</p>",
                       published=True)

HTML wordt aan de serverzijde opgeschoond aan de hand van een toelatingslijst met veilige tags, via hetzelfde proces als in de dashboardeditor. Scripts en stijlen worden verwijderd voordat iets wordt opgeslagen.

Een bot bouwen

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

A bot is a webhook and a reply. Bot handles the five things that are easy to get subtly wrong — signature verification, delivery deduplication, event routing, filtering out your own messages, and answering fast — so your code is the part that is yours.

from conecto import Conecto, Bot, blocks

client = Conecto()
bot = Bot(client, secret=WEBHOOK_SECRET)

@bot.on_message
def handle(ctx):
    text = ctx.text.lower()

    if "human" in text:
        ctx.reply("Of course — connecting you now.")
        ctx.handoff()
        return

    if "order" in text:
        if not ctx.verified:
            ctx.reply("What's the email on the order?", ask_email=True)
            return
        order = lookup_order(ctx.verified_email)     # your system
        ctx.reply("Here it is:", blocks=[
            blocks.list_block([
                blocks.row("Status", order["status"]),
                blocks.row("Carrier", order["carrier"]),
            ], title=order["number"]),
        ])
        return

    ctx.reply("I'm not sure about that one — let me get someone who is.")
    ctx.note(f"Bot could not classify: {ctx.text!r}")
    ctx.handoff()

Beschikbaar stellen

# Flask
app.add_url_rule("/conecto", view_func=bot.flask_view(), methods=["POST"])

# FastAPI (handlers run in a worker thread, so they never stall the loop)
app.post("/conecto")(bot.fastapi_route())

# Django (urls.py)
path("conecto/", csrf_exempt(bot.django_view()))

# No framework at all
from wsgiref.simple_server import make_server
make_server("", 8000, bot.wsgi_app()).serve_forever()

Abonneer vervolgens het endpoint:

hook = client.webhooks.create(
    "https://bots.you.com/conecto",
    ["message.created", "conversation.created", "conversation.rated"],
)
print(hook.secret)   # store this — it is how you verify deliveries

Handlers

DecoratorWordt geactiveerd bij
@bot.on_messageEen bericht van een bezoeker. Berichten van bots en medewerkers bereiken deze nooit, waardoor een bot niet op zichzelf kan antwoorden.
@bot.on_conversation_startedconversation.created , begroeten of een status instellen.
@bot.on_ratingconversation.rated. ctx.event.rating has score en comment.
@bot.on_ticket · @bot.on_contactticket.created · contact.created.
@bot.on("event.name")Any event by name. @bot.on("*") catches everything.
@bot.on_errorEen handler heeft een fout gegenereerd. Zonder eigen handler worden fouten vastgelegd en genegeerd.

Wat een handler ontvangt

Op ctxIs
ctx.textDe tekst van het bezoekersbericht.
ctx.verified · ctx.verified_emailOf de identiteit is geverifieerd en welk adres is geverifieerd.
ctx.conversation_id · ctx.widget_id · ctx.sessionID’s waarop u kunt handelen.
ctx.reply(...)Answer. Takes everything conversations.reply does.
ctx.note(...)Een notitie die alleen medewerkers zien.
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close()De thread sturen.
ctx.history()De volledige berichtgeschiedenis ophalen. Eén verzoek.
ctx.client · ctx.eventDe low-level client en de verwerkte gebeurtenis, voor al het overige.

Gebruikt u meer dan één worker? Deduplication is in-process by default, so two workers can each handle the same delivery once. Pass your own store — anything with add(id) -> bool, backed by Redis or your database.

class RedisSeen:
    def add(self, delivery_id):
        # True if new, False if we have handled it before
        return bool(redis.set(f"conecto:{delivery_id}", 1, nx=True, ex=86400))

bot = Bot(client, secret=WEBHOOK_SECRET, seen=RedisSeen())

Een plugin bouwen

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Een bot beantwoordt webhooks. Een plugin werkt in de andere richting: u geeft aan wat uw systemen kunnen en de AI-agent bepaalt wanneer die mogelijkheden nodig zijn tijdens een gesprek. U schrijft nooit de gesprekslogica, alleen de zoekopdracht.

Zo wordt een winkel, facturatiesysteem of CRM dat we niet kennen een volwaardige integratie.

from conecto import Conecto, Plugin, parameter

plugin = Plugin("acme-store", base_url="https://api.acme.com/conecto")

@plugin.action(
    "catalog.search_products",
    description="Search the Acme catalog by keyword.",
    parameters=[parameter("query", required=True,
                          description="Search words from the visitor.")],
)
def search(req):
    hits = catalog.search(req.get("query"), limit=req.get("limit", 4))
    if not hits:
        return req.not_found()
    return req.ok(products=[
        {"title": p.name, "url": p.url, "image": p.image,
         "price_from": f"{p.price:.2f}", "currency": "USD",
         "available": p.stock > 0}
        for p in hits
    ])

@plugin.action(
    "orders.get_status",
    risk="verified_read",
    description="Status of one of the visitor's own orders.",
    parameters=[parameter("order_number", required=True)],
)
def order_status(req):
    order = orders.find(req.get("order_number"), email=req.verified_email)
    return req.ok(**order) if order else req.not_found()

Beschikbaar stellen en registreren

# One route serves every action.
app.add_url_rule("/conecto/<path:action>", view_func=plugin.flask_view(),
                 methods=["POST"])

# Tell Conecto it exists. Idempotent — run it on every release.
integration = plugin.deploy(Conecto())
print(integration.signing_secret)   # set this in your environment

deploy() maakt of werkt de integratie bij zodat deze met uw code overeenkomt, verwijdert acties die u hebt geschrapt en installeert de integratie op uw widgets. Niets wordt aan de AI beschikbaar gesteld voordat de integratie is geïnstalleerd.

De vijf antwoorden

return req.ok(status="Shipped", carrier="DHL")   # success
return req.ok(result, blocks=[...])              # …plus rich content in the reply
return req.not_found()                           # nothing matched — a normal outcome
return req.verify_required()                     # "I need a proven identity"
return req.error("That subscription was already cancelled.")

Write error messages for the visitor, not for your logs: the AI may relay them. Returning a plain dict is also fine — it is treated as the result.

Risiconiveaus

RisicoBetekent
public_readCatalogus, beschikbaarheid en documentatie. Geen identiteit nodig.
verified_readGegevens van één persoon. Geweigerd totdat het e-mailadres is geverifieerd.
public_writeEen wijziging waarvoor geen identiteit nodig is, zoals een nieuwsbriefinschrijving of leadregistratie.
writeEen wijziging in het account van één persoon. Geverifieerd, gecontroleerd en nooit ongemerkt opnieuw geprobeerd.

req.verified_email is de enige identiteit die u kunt vertrouwen. An email in req.arguments is whatever the model parsed out of a chat. For verified_read en write the platform will not call you at all until we have proven who the visitor is, and the proven address is the one we hand you.

Gereserveerde acties

Enkele namen hebben binnen het platform een eigen betekenis. Declareer er een en uw service wordt gekoppeld aan de bestaande functie voor die naam.

NaamWat u ontvangt
catalog.search_productsResults 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_statusBestelstatus, altijd achter verificatie.
orders.get_trackingZending volgen, altijd geverifieerd.
orders.list_recentDe recente bestellingen van de bezoeker, altijd geverifieerd.
account.lookupElke platte record over de geverifieerde bezoeker, zoals abonnement, tegoed of verlengdatum.

Test het voordat een bezoeker dat doet

client.integrations.run("acme-store", "catalog.search_products",
                        {"query": "trail shoes"})
# {'status': 'ok', 'result': {...}, 'blocks': []}

Dit voert het werkelijke call path — the signature, your credential, the checks that stop your URL pointing anywhere private, the timeout, the parsing — so what you see is exactly what the AI will get. Pass conversation_id= to run in the context of a live chat, which is the only way a verified action can succeed.

Een goede beschrijving schrijven

De description is the highest-leverage string in your integration: it is what the AI reads to decide whether this action answers the question in front of it. Write it as an instruction to a capable colleague who cannot see your code.

Zwak: «Bestelling opzoeken.»
Sterk: «Zoek de status en bezorgdatum van een eigen bestelling van de bezoeker op aan de hand van het bestelnummer. Gebruik dit telkens wanneer de bezoeker vraagt waar iets is of wanneer het aankomt.»

Wat het platform garandeert

  • 8 seconden time-out, 128 KB reactielimiet.
  • Calls are signed with your signing_secret — same scheme as webhooks.
  • Uw URL wordt eenmaal omgezet en aan een openbaar IP-adres gekoppeld; omleidingen worden geweigerd.
  • Alles wat u retourneert bereikt het model als gegevens, nooit als instructies, en sleutels die op geheimen lijken worden verwijderd.
  • Aangepaste integraties vereisen een betaald abonnement met AI-agent. Per workspace zijn 20 integraties toegestaan, elk met 40 acties.

Identiteit

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Alles wat de gegevens van één persoon raakt, wacht totdat we weten wie die persoon is. Dat gebeurt op een van twee manieren: met een code per e-mail of via de bevestiging door uw site voor een gebruiker die al is ingelogd.

# your backend, right after your own auth check
client.visitors.identify(
    widget_id=7,
    session=conecto_session,          # read from the visitor's browser
    email=user.email,
    name=user.name,
    verified=True,                    # the vouch
    verify_hours=24,                  # max 72, default 12
    data={"plan": user.plan, "customer_since": "2024"},
)

# when they log out
client.visitors.unverify(widget_id=7, session=conecto_session)

Zolang de bevestiging geldig is, behandelt Conecto het e-mailadres als geverifieerd: de widget kent de naam, chats worden aan het juiste contact gekoppeld en processen die anders een e-mailcode vereisen, zoals bestellingen opzoeken, terugbetalingen, accountgegevens of geverifieerde pluginacties, gaan zonder code verder.

Sta alleen in voor sessies die uw backend daadwerkelijk heeft geverifieerd. Uw bevestiging geldt als even sterk als onze eigen e-mailcode, omdat u de persoon werkelijk hebt geverifieerd. Deze is tijdgebonden en sessiegebonden, dus een nieuwe browser heeft een nieuwe bevestiging nodig. De browser kan zichzelf nooit als geverifieerd markeren; die aanroep is beveiligd en hoort op uw server.

Webhooks en gebeurtenissen

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Bot doet dit allemaal voor u. Gebruik de low-level helpers wanneer u aan de rand wilt verifiëren en via een wachtrij wilt verwerken.

from conecto import webhooks

event = webhooks.parse(request.get_data(), request.headers, secret=SECRET)

event.id                  # the delivery id — dedupe on this
event.event               # "message.created"
event.conversation_id
event.text
event.is_visitor_message  # excludes your own bot's messages
event.visitor.verified

Verifiëren aan de hand van de onbewerkte inhoud. Re-serializing JSON changes the bytes and the signature stops matching — this is the most common first-integration bug there is. Use request.get_data() in Flask, request.body in Django, await request.body() in FastAPI. Passing a parsed dict raises with this exact advice.

Gebeurtenissen

conversation.createdEen bezoeker is een gesprek gestart.
message.createdEen bezoeker heeft een bericht gestuurd. De trigger voor de bot.
conversation.closedEen gesprek is gesloten.
conversation.assignedToegewezen aan of losgekoppeld van een teamlid.
conversation.handoffGemarkeerd als hulp van een medewerker vereist.
conversation.ratedEr is een CSAT-beoordeling ontvangen (1 tot 5 plus een opmerking).
ticket.createdEr is vanuit een willekeurige bron een ticket geopend.
ticket.updatedDe status, prioriteit of toegewezen medewerker van een ticket is gewijzigd.
contact.createdEen nieuwe persoon is aan het CRM toegevoegd.
visitor.identifiedEen sessie is via de API geïdentificeerd.

Betekenis van bezorgingen

Bezorgingen vinden plaats ten minste eenmaal en zonder gegarandeerde volgorde: if we cannot confirm you got one we send it again, and they will not always arrive in the order things happened. They also time out after 2.5 seconds — so answer 200 immediately and do the work afterwards.

  • Every payload carries an id (also in X-Conecto-Delivery). Store it and skip ids you have already handled.
  • Pass that same id as idempotency_key when you reply, and a redelivery can never make your bot speak twice.
  • X-Conecto-Timestamp laat u verouderde bezorgingen weigeren; de SDK controleert ze standaard met een tolerantie van vijf minuten.
signature = webhooks.sign(SECRET, raw_body)      # what we send
webhooks.verify(raw_body, header_value, SECRET)  # True / False, never raises

Integratieaanroepen die wij uitvoeren tot your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.

Fouten en nieuwe pogingen

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

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")
UitzonderingStatusBetekent
AuthenticationError401Ontbrekende, onbekende of ingetrokken referentie.
InvalidRequestError400De inhoud is niet door de validatie gekomen; het bericht vermeldt waarom.
NotFoundError404Het object bestaat niet in deze workspace.
ConflictError409Die identificatie is al in gebruik.
ConversationClosed409Reopen the thread first. A subclass of ConflictError.
LimitReached403Een limiet van het abonnement of de workspace.
PlanRequired403Het abonnement bevat deze functie niet.
RateLimited429Over 300 requests/minute. Carries retry_after.
ServiceUnavailable503Een afhankelijkheid is niet geconfigureerd.
ServerError5xxOnze fout. U kunt veilig opnieuw proberen.
BlockErrorLokaal gegenereerd door een blokbuilder, vóór enig verzoek.
SignatureError401De verificatie van een webhook- of integratieaanroep is mislukt.
APIConnectionError · APITimeoutErrorHet verzoek heeft nooit antwoord gekregen.

Every exception carries code, message, status and the raw response.

Nieuwe pogingen

Timeouts, rate limits (429) and server errors (5xx) are retried for you. Each retry waits a little longer than the last, with a random amount added so that a thousand clients recovering at once do not all come back in the same instant — and if we send a Retry-After, that wins.

Schrijfbewerkingen worden ook opnieuw geprobeerd, wat normaal gevaarlijk is. Hier is het veilig omdat elke schrijfbewerking een idempotentiesleutel bevat en een nieuwe poging dezelfde sleutel hergebruikt. De server herkent de tweede poging als dezelfde aanroep en voert deze niet opnieuw uit.

client = Conecto(max_retries=0)   # opt out entirely

Belangrijke limieten

LimietWaarde
Verzoeken per minuut, per referentie300
Berichtinhoud4000 tekens
Blokken per bericht10
Snelantwoordknoppen6
Media uploaden10 MB
Integraties per workspace20
Acties per integratie40
Webhooks per workspace10

client.meta.schema()["limits"] bevat altijd de actuele cijfers.

Paginering

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

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 list

Iterating is lazy: next(iter(page)) costs exactly one request no matter how many contacts exist. all() is fine for contacts and tickets; think twice on a busy workspace's conversations, where iterating and stopping early is usually what you meant.

Wordt getest

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete en request works — no network, no credentials, no mocking library.

class FakeTransport:
    base_url = "https://conecto.test/api/v1"

    def __init__(self):
        self.calls = []
        self.responses = {}

    def request(self, method, path, **kw):
        self.calls.append((method, path, kw.get("json_body")))
        return self.responses.get(f"{method} {path}", {})

    def get(self, p, **kw): return self.request("GET", p, **kw)
    def post(self, p, json_body=None, **kw): return self.request("POST", p, json_body=json_body, **kw)
    def patch(self, p, json_body=None, **kw): return self.request("PATCH", p, json_body=json_body, **kw)
    def delete(self, p, **kw): return self.request("DELETE", p, **kw)


def test_bot_answers_refunds():
    transport = FakeTransport()
    client = Conecto(transport=transport)
    client.conversations.reply(1, "Refunded.")
    assert transport.calls[0][1] == "/conversations/1/messages/"

Sla bij bots en plugins de handtekeningverificatie in tests over in plaats van fixtures te ondertekenen:

bot = Bot(client, verify=False)
plugin = Plugin("acme", base_url="https://x.test", verify_signatures=False)

Methodereferentie

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Gesprekken

client.conversations.list(status, widget_id, session, limit, before_id)

Gesprekken, nieuwste activiteit eerst. Retourneert een Page.

client.conversations.get(id, since_id=None)

Eén gesprek met de bijbehorende berichten. since_id retourneert alleen wat nieuwer is.

client.conversations.messages(id, since_id=None)

Alleen de berichten.

client.conversations.reply(id, body, blocks, buttons, products, ask_email, ticket_form, internal, idempotency_key)

Een botbericht verzenden. Ook beschikbaar als send().

client.conversations.typing(id, on=True, name="Bot")

De typindicator tonen of wissen.

client.conversations.handoff(id)

Markeren als hulp van een medewerker vereist.

client.conversations.assign(id, user_id)

None verwijdert de toewijzing.

client.conversations.close(id) · reopen(id) · set_status(id, status)

Status wijzigen.

Contacten

client.contacts.list(email, query, limit, before_id) · get(id) · find(email)

find() retourneert één contact of None.

client.contacts.upsert(email, name, title, company, location, phone, notes, custom_fields)

Maken of bijwerken op e-mailadres. Ook beschikbaar als create().

client.contacts.update(id, **fields) · delete(id)

Bezoekers

client.visitors.get(widget_id, session)

Identiteit en verificatiestatus.

client.visitors.identify(widget_id, session, email, name, data, verified, verify_hours)

Koppel uw gebruiker; verified=True staat voor diens identiteit in.

client.visitors.unverify(widget_id, session)

Trek de bevestiging in. Roep aan bij het uitloggen.

client.visitors.message(widget_id, session, body, blocks, buttons, products)

Een proactief bericht.

Integraties

client.integrations.list() · get(slug) · actions(slug)

actions() retourneert ook de catalogus met gereserveerde acties.

client.integrations.create(slug, base_url, name, description, auth_type, credential, actions)

Registreer er één. De reactie bevat signing_secret.

client.integrations.update(slug, actions, replace_actions, rotate_signing_secret, **fields)

replace_actions maakt uw lijst bepalend.

client.integrations.deploy(slug, base_url, actions, widget_ids, install=True)

Maken of bijwerken en vervolgens installeren. Idempotent, dus geschikt voor uw deployscript.

client.integrations.install(slug, widget_ids, actions, enabled) · uninstall(slug, widget_ids)

actions is een toelatingslijst; [] stelt niets beschikbaar.

client.integrations.run(slug, action, arguments, conversation_id)

Aanroepen via het werkelijke aanroeppad.

client.integrations.delete(slug) · remove_action(slug, name)

Tickets, artikelen, widgets, media, webhooks en metadata

client.tickets.list · get · create · update · reply · categories
client.articles.list · get · create · update · delete
client.widgets.get(id) · update(id, **fields)

De volledige configuratie staat in widget.raw.

client.media.upload(file, filename, content_type)

Retourneert een Media-object dat u aan een blokbuilder kunt doorgeven.

client.webhooks.list() · create(url, events, widget_id) · delete(id)

Onbekende gebeurtenisnamen worden lokaal geweigerd.

client.members.list() · client.meta.me() · schema() · stats()

Modellen

Every returned object keeps the payload it was built from. Attribute access is the nice path; obj.raw is always there for a field this SDK version does not know about yet, so a release is never what stands between you and a new API field.

convo.status          # typed
convo.raw["status"]   # the same thing
convo["status"]       # shorthand for the raw payload
convo.to_dict()       # exactly what the API sent

Recepten

Komt u niet verder? Vraag een AI naar deze sectie:ClaudeChatGPTGrok

Een ticket voorkomen met uw helpcenter

@bot.on_message
def deflect(ctx):
    hits = ctx.client.articles.list(query=ctx.text, published=True)
    if hits:
        ctx.reply(f"This might help: “{hits[0].title}”. Did that solve it?",
                  buttons=["Solved it", "Open a ticket"])
    else:
        ctx.reply("Let's get this to the team.", ticket_form=True)

Een lead kwalificeren en doorsturen

@bot.on_message
def qualify(ctx):
    if not ctx.event.visitor or not ctx.event.visitor.email:
        ctx.reply("Happy to help — what's your work email?", ask_email=True)
        return

    ctx.client.contacts.upsert(
        ctx.event.visitor.email,
        custom_fields={"lead_source": "chat", "intent": classify(ctx.text)},
    )
    ctx.note("Qualified lead — routing to sales.")
    ctx.assign(SALES_USER_ID)
    ctx.handoff()

Een slechte beoordeling herstellen

@bot.on_rating
def follow_up(ctx):
    rating = ctx.event.rating or {}
    if rating.get("score", 5) <= 2:
        ctx.client.tickets.create(
            email=ctx.event.visitor.email,
            message=f"Low CSAT ({rating['score']}/5): {rating.get('comment', '')}",
            priority="high",
        )

Nachtelijke contactsynchronisatie

for user in your_database.active_users():
    client.contacts.upsert(
        user.email, name=user.name, company=user.company,
        custom_fields={"plan": user.plan, "mrr": str(user.mrr)},
    )

Een winkel die we niet kennen

@plugin.action("catalog.search_products",
               description="Search the catalog by keyword.",
               parameters=[parameter("query", required=True)])
def search(req):
    hits = your_catalog.search(req.get("query"))
    return req.ok(products=[to_conecto(p) for p in hits]) if hits else req.not_found()

Dit is de volledige integratie. Productkaarten, de Home-showcase en de discipline die voorkomt dat het model onbewerkte URL’s plakt, worden allemaal geleverd door de gereserveerde naam.

Bouwt u iets? Praat met ons , we willen dat mensen met Conecto bouwen en geven prioriteit aan wat ontwikkelaars vragen. De SDK heeft een MIT-licentie en is open source; issues en pull requests zijn welkom op GitHub.