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
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.
De que secção preciso?
| Quero… | Vá a |
|---|---|
| Ler ou responder a chats a partir de um script | Conversas |
| Enviar uma imagem, GIF, vídeo ou cartão | Conteúdo enriquecido |
| Responder automaticamente aos visitantes com a minha própria lógica | Criar um bot |
| Permitir que a IA consulte os meus sistemas | Criar um plugin |
| Ligar uma loja que a IA possa pesquisar | Criar um plugin |
| Ignorar o código por e-mail para utilizadores com sessão iniciada | Identidade |
| Manter o meu CRM sincronizado | Contactos e visitantes |
| Saber o que capturar quando algo falha | Erros e novas tentativas |
Instalar e autenticar
pip install conectoCrie 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_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.
Início rápido
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
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ável | Define |
|---|---|
CONECTO_CLIENT_ID | O ID do cliente. |
CONECTO_SECRET | O secret. |
CONECTO_BASE_URL | URL base da API. Raramente necessária. |
Recursos
| Atributo | Abrange |
|---|---|
client.conversations | Listar, ler, responder, indicar escrita, transferir, atribuir e fechar. |
client.contacts | O CRM: criar ou atualizar por e-mail, pesquisar, atualizar e eliminar. |
client.visitors | Sessões, confirmação de identidade e mensagens proativas. |
client.tickets | Criar, atualizar, responder e gerir categorias. |
client.articles | Pesquisar e publicar conteúdo do centro de ajuda. |
client.integrations | Registar, instalar, executar e fazer o deployment de plugins. |
client.widgets | Ler e atualizar a configuração do widget. |
client.media | Fazer upload de ficheiros para utilizar em blocos. |
client.webhooks | Subscrever endpoints a eventos. |
client.members | Colegas de equipa, para atribuição. |
client.meta | me(), 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"] >= 4client.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
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.
| Argumento | O que faz |
|---|---|
body | O texto, até 4000 caracteres. |
blocks | Rich content, consulte Conteúdo enriquecido. |
buttons | Até 6 respostas rápidas. |
products | Até 4 cartões de produto, tal como os anexados pela IA integrada. |
ask_email | Apresenta o formulário nativo de recolha de e-mail. |
ticket_form | Anexa o formulário nativo para abrir um ticket. |
internal | Uma nota apenas para agentes. O visitante nunca a vê. |
idempotency_key | Passe 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
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
| Builder | Notas |
|---|---|
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 tabAtribua 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
videoblock raises, and tells you to useembed.
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
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
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.verifiedMensagens 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
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 noteUma 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
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 deliveriesHandlers
| Decorator | É acionado por |
|---|---|
@bot.on_message | Uma 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_started | conversation.created , cumprimentar ou definir o estado. |
@bot.on_rating | conversation.rated. ctx.event.rating has score e 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 | Um 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.text | O texto da mensagem do visitante. |
ctx.verified · ctx.verified_email | Se a identidade está confirmada e qual é o endereço confirmado. |
ctx.conversation_id · ctx.widget_id · ctx.session | IDs 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.event | O 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
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 environmentdeploy() 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
| Risco | Significa |
|---|---|
public_read | Catálogo, disponibilidade e documentação. Não é necessária identidade. |
verified_read | Dados de uma pessoa. Recusados até o respetivo e-mail ser confirmado. |
public_write | Uma alteração que não exige identidade, como a subscrição de uma newsletter ou a captação de um lead. |
write | Uma 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.
| Nome | O que recebe |
|---|---|
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 | Estado da encomenda, sempre sujeito a verificação. |
orders.get_tracking | Acompanhamento de envios, sempre verificado. |
orders.list_recent | As encomendas recentes do visitante, sempre verificadas. |
account.lookup | Qualquer 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
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
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.verifiedVerificar 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 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-Timestamppermite 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 raisesChamadas 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
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ção | Estado | Significa |
|---|---|---|
AuthenticationError | 401 | Credencial em falta, desconhecida ou revogada. |
InvalidRequestError | 400 | O corpo não passou na validação; a mensagem indica o motivo. |
NotFoundError | 404 | O objeto não existe neste espaço de trabalho. |
ConflictError | 409 | Esse identificador já está a ser utilizado. |
ConversationClosed | 409 | Reopen the thread first. A subclass of ConflictError. |
LimitReached | 403 | Um limite do plano ou do espaço de trabalho. |
PlanRequired | 403 | O plano não inclui esta funcionalidade. |
RateLimited | 429 | Over 300 requests/minute. Carries retry_after. |
ServiceUnavailable | 503 | Uma dependência não está configurada. |
ServerError | 5xx | A falha é nossa. Pode repetir em segurança. |
BlockError | — | Gerada localmente por um builder de blocos, antes de qualquer pedido. |
SignatureError | 401 | A verificação de uma chamada de webhook ou integração falhou. |
APIConnectionError · APITimeoutError | — | O 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 entirelyLimites que deve conhecer
| Limite | Valor |
|---|---|
| Pedidos por minuto e por credencial | 300 |
| Corpo da mensagem | 4000 caracteres |
| Blocos por mensagem | 10 |
| Botões de resposta rápida | 6 |
| Upload de multimédia | 10 MB |
| Integrações por espaço de trabalho | 20 |
| Ações por integração | 40 |
| Webhooks por espaço de trabalho | 10 |
client.meta.schema()["limits"] tem sempre os valores atuais.
Paginação
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.
A testar
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
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 · categoriesclient.articles.list · get · create · update · deleteclient.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 sentReceitas
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.