Programadores / Python

O SDK de Python

Leia e responda a chats, envie imagens e vídeo, crie bots e permita que o agente de IA consulte os seus sistemas através de Python. Tudo o que a API REST faz, com os pormenores mais delicados já tratados: confirmar que um pedido veio realmente de nós, evitar responder duas vezes ao mesmo evento, repetir pedidos em segurança e detetar uma mensagem malformada antes de sair do seu processo.

Ainda não usa a Conecto? Comece aqui , cinco minutos de vocabulário e o resto desta página torna-se evidente.

pip install conectov0.1.0Python 3.9+MIT

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

Comece aqui

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Se nunca utilizou o Conecto, esta secção contém todo o modelo mental. Cinco minutos aqui tornam evidente o resto da página.

O que é o Conecto

Um widget de chat no seu site. Os visitantes escrevem nele; a sua equipa, um agente de IA ou o seu próprio código responde. Tudo o que se segue é uma forma de colocar o seu código nesse ciclo.

Os cinco substantivos

Quase todos os métodos deste SDK recebem ou devolvem um destes elementos.

Espaço de trabalho

A sua conta. A sua credencial pertence exatamente a uma conta e nunca consegue ver outra.

Widget

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

Visitante

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

Conversa

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

Mensagem

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

As três coisas que pode criar

1. Um script

É executado segundo o seu horário. Sincroniza contactos, abre tickets e envia mensagens de campanha. Não é necessário alojar nada; limita-se a chamar a API.

2. Um bot

Um servidor Web seu que notificamos quando um visitante escreve. É o utilizador que decide a resposta. O utilizador controlar a conversa.

3. Um plugin

Um servidor Web seu que chamamos quando o IA precisa de um dado que apenas o utilizador possui. A IA controla a conversa; o utilizador fornece as consultas.

Bot ou plugin? Se quiser escrever as palavras que o visitante lê, crie um bot. Se preferir deixar a IA falar e apenas lhe dar acesso aos seus dados, como encomendas, subscrições e stock, crie um plugin. A maioria das equipas acaba por precisar de um plugin.

Palavras utilizadas nesta página

Jargão definido uma única vez. Nada aqui é específico do Conecto, exceto os dois últimos termos.

WebhookEnviamos um pedido HTTP para um URL seu quando algo acontece, em vez de o obrigarmos a consultar-nos repetidamente. Precisa de um servidor acessível através da Internet.
AssinaturaUm hash que anexamos a cada pedido, calculado com um secret que apenas nós e o utilizador conhecemos. A verificação prova que o pedido veio realmente de nós e que ninguém o alterou. O SDK faz isto por si.
Pelo menos uma vezSe não conseguirmos determinar se recebeu algo, enviamos novamente. Por isso, pode receber ocasionalmente o mesmo evento duas vezes, e o seu código deve detetá-lo em vez de agir duas vezes.
IdempotenteFazer duas vezes produz o mesmo resultado que fazer uma vez. Se enviar duas mensagens com a mesma chave, a segunda chamada devolve a primeira mensagem em vez de publicar um duplicado.
Paginação por cursorAs listas longas chegam em páginas. Em vez de «página 3», passa o ID que recebeu da última vez. O SDK oculta este processo; basta iterar.
BlocoUm elemento de rich content numa mensagem: uma imagem, um vídeo, um cartão ou uma linha de botões. Consulte Conteúdo enriquecido.
Visitante verificadoAlguém cujo endereço de e-mail foi confirmado , através de um código enviado por e-mail ou porque o seu site nos informou de que a pessoa tem sessão iniciada. Qualquer ação que envolva os dados privados de uma pessoa aguarda esta confirmação.

De que secção preciso?

Quero…Vá a
Ler ou responder a chats a partir de um scriptConversas
Enviar uma imagem, GIF, vídeo ou cartãoConteúdo enriquecido
Responder automaticamente aos visitantes com a minha própria lógicaCriar um bot
Permitir que a IA consulte os meus sistemasCriar um plugin
Ligar uma loja que a IA possa pesquisarCriar um plugin
Ignorar o código por e-mail para utilizadores com sessão iniciadaIdentidade
Manter o meu CRM sincronizadoContactos e visitantes
Saber o que capturar quando algo falhaErros e novas tentativas

Instalar e autenticar

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok
pip install conecto

Crie uma credencial em Definições → Programadores. Recebe um ID de cliente (ck_…) and a segredo (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.

Início rápido

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Três coisas que pode fazer nos primeiros cinco minutos.

Ler o que está a acontecer

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

Responder a alguém

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

Enviar algo que não seja texto

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"),
    ]),
])

Esta é a estrutura de toda a biblioteca: um cliente com recursos, objetos que sabem agir sobre si próprios e builders para tudo o que é estruturado.

O cliente

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Crie um e mantenha-o. Reutiliza ligações e pode ser partilhado em segurança entre threads.

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
)

Ambiente

VariávelDefine
CONECTO_CLIENT_IDO ID do cliente.
CONECTO_SECRETO secret.
CONECTO_BASE_URLURL base da API. Raramente necessária.

Recursos

AtributoAbrange
client.conversationsListar, ler, responder, indicar escrita, transferir, atribuir e fechar.
client.contactsO CRM: criar ou atualizar por e-mail, pesquisar, atualizar e eliminar.
client.visitorsSessões, confirmação de identidade e mensagens proativas.
client.ticketsCriar, atualizar, responder e gerir categorias.
client.articlesPesquisar e publicar conteúdo do centro de ajuda.
client.integrationsRegistar, instalar, executar e fazer o deployment de plugins.
client.widgetsLer e atualizar a configuração do widget.
client.mediaFazer upload de ficheiros para utilizar em blocos.
client.webhooksSubscrever endpoints a eventos.
client.membersColegas de equipa, para atribuição.
client.metame(), schema(), stats().

Desvio entre versões

Cada resposta inclui a versão da API do servidor. Registe-a no arranque. Se mudar e desaparecer um campo que utiliza, esta será a primeira informação de que vai precisar.

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() devolve toda a API em JSON, incluindo endpoints, eventos, tipos de bloco, ações reservadas, códigos de erro e todos os limites. É servido pelo mesmo código que aplica essas regras, pelo que não se pode afastar da realidade.

Conversas

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

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

Listar e ler

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)

Responder

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

Um toque num botão de resposta rápida regressa como uma mensagem normal do visitante com esse texto, por isso o seu handler não precisa de um caso especial.

ArgumentoO que faz
bodyO texto, até 4000 caracteres.
blocksRich content, consulte Conteúdo enriquecido.
buttonsAté 6 respostas rápidas.
productsAté 4 cartões de produto, tal como os anexados pela IA integrada.
ask_emailApresenta o formulário nativo de recolha de e-mail.
ticket_formAnexa o formulário nativo para abrir um ticket.
internalUma nota apenas para agentes. O visitante nunca a vê.
idempotency_keyPasse o ID de uma entrega de webhook para que uma nova tentativa não publique duas vezes.

Orientação do tópico

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 Erros.

Conteúdo enriquecido

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Qualquer mensagem pode incluir até dez blocos: imagens e GIFs, vídeo, áudio, ficheiros, conteúdos incorporados de terceiros, carrosséis de cartões, listas de etiquetas e valores, botões e separadores. São apresentados no widget e na caixa de entrada do agente.

Os blocos são dicionários simples durante a transmissão, pelo que pode escrevê-los manualmente. Os builders existem porque uma chave mal escrita acaba por levar um cliente a dizer que a imagem nunca apareceu. Aplicam as mesmas regras do servidor no ponto em que o traceback ainda aponta para o seu código.

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")]),
    ]),
])

Todos os tipos de bloco

BuilderNotas
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 e Spotify. Passe o link de partilha normal.
blocks.audio(url, title).mp3, .wav, .m4a
blocks.file(url, filename, size)Uma linha de download.
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 e link_button.
blocks.text(body)Um parágrafo adicional por baixo de outros blocos.
blocks.divider()Uma linha horizontal.

Dois tipos de botão

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

Atribua um valor explícito a um botão de resposta quando a etiqueta deve ser clara, mas o texto que o bot reconhece não deve mudar sempre que alguém altera o conteúdo.

Enviar um 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)

Regras aplicadas pelos builders

  • Every URL must be https. The widget runs on an https page, so anything else is blocked by the browser anyway.
  • Dez blocos por mensagem, dez cartões, doze linhas de lista e seis botões.
  • Long strings are truncated, not rejected. Structural mistakes raise BlockError.
  • A YouTube link in a video block raises, and tells you to use embed.

A validação ocorre localmente. reply(blocks=…) checks the list before sending, so a typo raises BlockError from your line rather than coming back as an HTTP 400 from inside the client.

Upload de ficheiros

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Não existe um URL público para esse GIF, recibo ou ficha técnica? Faça o upload, até 10 MB, e utilize o resultado.

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 ou blocks.file.

Use media.url, not media.absolute_url. url é um caminho resolvido pelo widget em relação à sua própria origem, pelo que a mesma mensagem funciona em desenvolvimento, num deployment de pré-visualização e em produção, mesmo quando o seu domínio mudar.

Contactos e visitantes

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Contactos

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 é combinadas, não substituídas. Dois processos podem gerir as suas próprias chaves sem se sobreporem. Até 30 chaves por chamada.

Visitantes

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

Mensagens proativas

Envie para uma sessão sem esperar que lhe perguntem, por exemplo atualizações de envio, lembretes de trial ou recuperação de carrinho.

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"),
    ])],
)

Reutiliza a conversa ativa da sessão ou abre uma nova. Um widget aberto mostra-a na próxima consulta; um widget fechado mostra-a quando voltar a abrir, assinalada como não lida.

Tickets e artigos

Precisa de ajuda? Pergunte a uma IA sobre esta secção: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

Uma resposta pública reabre um ticket resolvido, porque responder a algo marcado como concluído quase sempre inicia uma nova interação.

Artigos do centro de ajuda

Útil nos dois sentidos: pesquise a partir de um bot antes de abrir um ticket e envie documentação a partir da ferramenta em que a escreve.

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)

O HTML é limpo no servidor com base numa lista de tags seguras, através do mesmo processo utilizado pelo editor do dashboard. Scripts e estilos são removidos antes de qualquer conteúdo ser guardado.

Criar um bot

Precisa de ajuda? Pergunte a uma IA sobre esta secção: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()

Disponibilização

# 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()

Depois subscreva o 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

DecoratorÉ acionado por
@bot.on_messageUma mensagem de um visitante. As mensagens de bots e agentes nunca chegam a este evento, o que impede um bot de responder a si próprio.
@bot.on_conversation_startedconversation.created , cumprimentar ou definir o estado.
@bot.on_ratingconversation.rated. ctx.event.rating has score e comment.
@bot.on_ticket · @bot.on_contactticket.created · contact.created.
@bot.on("event.name")Any event by name. @bot.on("*") catches everything.
@bot.on_errorUm handler gerou uma exceção. Sem um handler próprio, os erros são registados e ignorados.

O que um handler recebe

Em ctxÉ
ctx.textO texto da mensagem do visitante.
ctx.verified · ctx.verified_emailSe a identidade está confirmada e qual é o endereço confirmado.
ctx.conversation_id · ctx.widget_id · ctx.sessionIDs sobre os quais pode atuar.
ctx.reply(...)Answer. Takes everything conversations.reply does.
ctx.note(...)Uma nota apenas para agentes.
ctx.typing() · ctx.handoff() · ctx.assign() · ctx.close()Orientar o tópico.
ctx.history()Obter todo o histórico de mensagens. Um único pedido.
ctx.client · ctx.eventO cliente de baixo nível e o evento processado, para tudo o resto.

Executa mais do que um 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())

Criar um plugin

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Um bot responde a webhooks. Um plugin funciona no sentido oposto: declara o que os seus sistemas podem fazer e o agente de IA decide quando os utilizar enquanto fala com alguém. Nunca escreve a lógica da conversa, apenas a consulta.

É assim que uma loja, sistema de faturação ou CRM que não conhecemos se torna uma integração de primeira classe.

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()

Disponibilizar e registar

# 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() cria ou atualiza a integração para corresponder ao seu código, elimina as ações que removeu e instala-a nos seus widgets. Nada fica exposto à IA antes da instalação.

As cinco respostas

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.

Níveis de risco

RiscoSignifica
public_readCatálogo, disponibilidade e documentação. Não é necessária identidade.
verified_readDados de uma pessoa. Recusados até o respetivo e-mail ser confirmado.
public_writeUma alteração que não exige identidade, como a subscrição de uma newsletter ou a captação de um lead.
writeUma alteração na conta de uma pessoa. Verificada, auditada e nunca repetida silenciosamente.

req.verified_email é a única identidade em que deve confiar. An email in req.arguments is whatever the model parsed out of a chat. For verified_read e 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.

Ações reservadas

Alguns nomes têm um significado próprio para a plataforma. Declare um deles e o seu serviço fica ligado à funcionalidade que já existe para esse nome.

NomeO que recebe
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_statusEstado da encomenda, sempre sujeito a verificação.
orders.get_trackingAcompanhamento de envios, sempre verificado.
orders.list_recentAs encomendas recentes do visitante, sempre verificadas.
account.lookupQualquer registo simples sobre o visitante verificado, como plano, créditos ou data de renovação.

Teste antes de um visitante o fazer

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

Isto executa o real 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.

Escrever uma boa descrição

O 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.

Fraco: «Consultar encomenda.»
Forte: «Consulte o estado e a data de entrega de uma encomenda do próprio visitante através do respetivo número. Utilize esta ação sempre que a pessoa perguntar onde está algo ou quando irá chegar.»

O que a plataforma garante

  • 8 segundos timeout, 128 KB limite da resposta.
  • Calls are signed with your signing_secret — same scheme as webhooks.
  • O seu URL é resolvido uma vez e fixado a um IP público; os redirecionamentos são recusados.
  • Tudo o que devolve chega ao modelo como dados, nunca como instruções, e as chaves que parecem conter secrets são removidas.
  • As integrações personalizadas exigem um plano pago com Agente de IA. São permitidas 20 por espaço de trabalho e 40 ações em cada uma.

Identidade

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Tudo o que envolve os dados de uma pessoa aguarda até sabermos quem ela é. Isso acontece de uma de duas formas: através de um código enviado por e-mail ou de a confirmação do seu site para um utilizador que já tem sessão iniciada.

# 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)

Enquanto a confirmação for válida, o Conecto considera o e-mail confirmado: o widget conhece o nome da pessoa, os chats são associados ao contacto correto e os fluxos que, de outro modo, exigiriam um código por e-mail, como consultas de encomendas, reembolsos, dados da conta ou ações verificadas do seu plugin, avançam sem esse código.

Confirme apenas sessões que o seu backend tenha realmente autenticado. A sua confirmação é considerada tão forte como o nosso próprio código por e-mail, porque autenticou realmente a pessoa. Tem duração limitada e está associada à sessão, por isso um novo browser precisa de uma nova confirmação. O browser nunca se pode marcar como verificado; essa chamada é autenticada e deve ser feita no seu servidor.

Webhooks e eventos

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Bot faz tudo isto por si. Utilize os helpers de baixo nível quando quiser verificar na periferia e processar numa fila.

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

Verificar com base no corpo original. 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.

Eventos

conversation.createdUm visitante iniciou uma conversa.
message.createdUm visitante enviou uma mensagem. O trigger do bot.
conversation.closedUma conversa foi encerrada.
conversation.assignedAtribuído a um colega de equipa ou removido desse colega.
conversation.handoffMarcado como necessitando de intervenção humana.
conversation.ratedFoi recebida uma classificação CSAT (1 a 5 e um comentário).
ticket.createdFoi aberto um ticket, a partir de qualquer origem.
ticket.updatedUm ticket mudou de estado, prioridade ou responsável.
contact.createdUma nova pessoa entrou no CRM.
visitor.identifiedUma sessão foi identificada através da API.

Semântica das entregas

As entregas são feitas pelo menos uma vez e sem ordem garantida: 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 permite rejeitar entregas antigas; por predefinição, o SDK verifica-as com uma tolerância de cinco minutos.
signature = webhooks.sign(SECRET, raw_body)      # what we send
webhooks.verify(raw_body, header_value, SECRET)  # True / False, never raises

Chamadas de integração que fazemos às your plugin use the identical scheme with the integration's signing_secret — one verification routine covers both directions.

Erros e novas tentativas

Precisa de ajuda? Pergunte a uma IA sobre esta secção: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")
ExceçãoEstadoSignifica
AuthenticationError401Credencial em falta, desconhecida ou revogada.
InvalidRequestError400O corpo não passou na validação; a mensagem indica o motivo.
NotFoundError404O objeto não existe neste espaço de trabalho.
ConflictError409Esse identificador já está a ser utilizado.
ConversationClosed409Reopen the thread first. A subclass of ConflictError.
LimitReached403Um limite do plano ou do espaço de trabalho.
PlanRequired403O plano não inclui esta funcionalidade.
RateLimited429Over 300 requests/minute. Carries retry_after.
ServiceUnavailable503Uma dependência não está configurada.
ServerError5xxA falha é nossa. Pode repetir em segurança.
BlockErrorGerada localmente por um builder de blocos, antes de qualquer pedido.
SignatureError401A verificação de uma chamada de webhook ou integração falhou.
APIConnectionError · APITimeoutErrorO pedido nunca recebeu uma resposta.

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

Novas tentativas

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.

As operações de escrita também são repetidas, o que normalmente seria perigoso. Aqui é seguro porque cada escrita inclui uma chave de idempotência e uma nova tentativa reutiliza a mesma chave. O servidor reconhece a segunda tentativa como a mesma chamada e não a repete.

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

Limites que deve conhecer

LimiteValor
Pedidos por minuto e por credencial300
Corpo da mensagem4000 caracteres
Blocos por mensagem10
Botões de resposta rápida6
Upload de multimédia10 MB
Integrações por espaço de trabalho20
Ações por integração40
Webhooks por espaço de trabalho10

client.meta.schema()["limits"] tem sempre os valores atuais.

Paginação

Precisa de ajuda? Pergunte a uma IA sobre esta secção: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.

A testar

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

The client takes a transport, which is the seam for tests. Anything with get, post, patch, delete e 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/"

Para bots e plugins, desative a verificação de assinaturas nos testes em vez de assinar fixtures:

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

Referência de métodos

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Conversas

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

Conversas, com a atividade mais recente primeiro. Devolve uma Page.

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

Uma conversa com as respetivas mensagens. since_id devolve apenas o que for mais recente.

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

Apenas as mensagens.

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

Enviar uma mensagem de bot. Também disponível como send().

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

Mostrar ou limpar o indicador de escrita.

client.conversations.handoff(id)

Marcar como necessitando de intervenção humana.

client.conversations.assign(id, user_id)

None remove a atribuição.

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

Alterar o estado.

Contactos

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

find() devolve um contacto ou None.

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

Crie ou atualize por e-mail. Também disponível como create().

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

Visitantes

client.visitors.get(widget_id, session)

Identidade e estado de verificação.

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

Associe o seu utilizador; verified=True confirma a identidade.

client.visitors.unverify(widget_id, session)

Revogue a confirmação. Chame este método no logout.

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

Uma mensagem proativa.

Integrações

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

actions() também devolve o catálogo de ações reservadas.

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

Registe um. A resposta inclui signing_secret.

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

replace_actions torna a sua lista definitiva.

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

Crie ou atualize e depois instale. É idempotente, por isso pode colocá-lo no seu script de deployment.

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

actions é uma lista de permissões; [] não expõe nada.

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

Invocar através do percurso real da chamada.

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

Tickets, artigos, widgets, multimédia, webhooks e metadados

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

A configuração completa encontra-se em widget.raw.

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

Devolve um objeto Media que pode passar a um builder de blocos.

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

Os nomes de eventos desconhecidos são rejeitados localmente.

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

Modelos

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

Receitas

Precisa de ajuda? Pergunte a uma IA sobre esta secção:ClaudeChatGPTGrok

Evitar um ticket com o seu centro de ajuda

@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)

Qualificar um lead e encaminhá-lo

@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()

Recuperar uma classificação negativa

@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",
        )

Sincronização noturna de contactos

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)},
    )

Uma loja que não conhecemos

@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()

Esta é toda a integração. Os cartões de produto, a montra da página inicial e a regra que impede o modelo de colar URL em bruto são incluídos com o nome reservado.

Está a desenvolver algo? Fale connosco , queremos que as pessoas criem com o Conecto e damos prioridade ao que os programadores pedem. O SDK tem licença MIT e é open source; issues e pull requests são bem-vindos no GitHub.