Construire sur Conecto
Une API REST claire pour tout ce que fait la plateforme: créez des bots qui ne se contentent pas de répondre, mais peuvent agir , dans votre base de données, votre facturation et votre produit, envoyez des images, GIF, vidéos et cartes dans le chat, et créer vos propres intégrations que l’agent IA appelle en cours de conversation, quel que soit le logiciel sous-jacent. Si le widget peut le faire, l’API peut le piloter.
En production · Webhooks signés · 300 requêtes/min
Les bases
Base URL https://conecto.chat/api/v1. Create a credential in Paramètres → Développeurs , vous obtenez un identifiant client et un secret (shown once, stored hashed). Authenticate with HTTP Basic — client ID as username, secret as password — or Authorization: Bearer <client_id>:<secret>. A credential can cover the whole workspace or be scoped to a single widget.
curl https://conecto.chat/api/v1/me/ -u "ck_your_client_id:cs_your_secret"Vous utilisez Python? Le SDK officiel (pip install conecto) wraps all of this, and does the parts that are easy to get subtly wrong for you: webhook signature verification, delivery deduplication, idempotent writes, retries, and block validation that fires before a request leaves your process. Everything below still applies — it is the same API underneath.
Limite de débit 300 requêtes/min per credential (429 + Retry-After beyond it). Errors are always {"error": {"code", "message"}}. List endpoints paginate with limit et before_id, returning next_before_id. Write endpoints accept an Idempotency-Key header (any UUID): retry the same key and you get the message that was already created, with a 200 instead of a 201, rather than a second copy in front of the visitor.
Un point de terminaison décrit tous les autres
GET /schema/ renvoie toute la surface sous forme de JSON: chaque point de terminaison, événement, type de bloc, action réservée, code d’erreur et limite numérique. La réponse est servie par le même code qui applique ces limites; elle ne peut donc pas s’écarter du serveur en cours d’exécution comme une page de documentation. Générez votre client à partir de cette réponse ou vérifiez au démarrage que la fonctionnalité requise est déployée.
curl https://conecto.chat/api/v1/schema/ -u "ck_...:cs_..." | jq '.limits, .events[].name'Every response carries X-Conecto-Api-Version. Pin it in your client and log a warning when it changes — that header is how you find out a field moved before your users do.
Contenu enrichi: images, GIF, vidéos et cartes
Any message you send can carry blocks — an ordered list of typed content rendered under the bubble in the widget, and in the agent's inbox exactly as the visitor saw it. Up to 10 per message, and every field is validated and re-projected server-side, so you never hand us markup and we never hand the browser a string.
curl -X POST https://conecto.chat/api/v1/conversations/42/messages/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{
"body": "Here is how to reset it — takes about 20 seconds:",
"blocks": [
{"type": "image", "url": "https://cdn.you.com/reset.gif", "alt": "Reset flow"},
{"type": "buttons", "items": [
{"label": "That worked"},
{"label": "Full guide", "url": "https://docs.you.com/reset"}
]}
]
}'Les types de blocs
imageurl, alt, caption, link — animated GIFs are just imagesvideourl to an .mp4/.webm/.mov, plus poster, autoplay, loop, mutedembedprovider youtube · vimeo · loom · wistia · spotify et le lien de partage habituel urlaudiourl vers un fichier .mp3/.wav/.m4a, facultatif titlefileurl, filename, size — rendered as a download rowcardsitems[] of title · subtitle · text · image · url · price · badge · buttons; layout carousel or listlistrows[] composé de libellé · valeur · sous-titre · image · URL, pour résumés de commande et étapes d’expéditionbuttonsitems[]: {label, value} replies as the visitor, {label, url} opens a linktext · dividerparagraphes supplémentaires et ligne horizontaleURLs must be https. A block that can't be built is a 400 naming the block and the reason, never a silent drop — an image that vanishes without explanation costs an afternoon to track down.
Envoyer un GIF sans emplacement d’hébergement
POST /media/ takes a multipart file (≤10 MB) and hands back a URL you can drop straight into a block. Use the returned url — it is a path, resolved against the widget's own origin, so the same message works in development and in production.
URL=$(curl -s -X POST https://conecto.chat/api/v1/media/ -u "ck_...:cs_..." \
-F "file=@celebrate.gif" | jq -r .media.url)
curl -X POST https://conecto.chat/api/v1/conversations/42/messages/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d "{\"body\": \"All set!\", \"blocks\": [{\"type\": \"image\", \"url\": \"$URL\"}]}"Shipping a GIF as a muted, looping video block is usually the better trade: a tenth of the bytes, and the visitor cannot tell the difference.
Un carrousel de cartes
{"blocks": [{
"type": "cards",
"items": [
{"title": "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": [{"label": "Add to cart", "url": "https://shop.you.com/cart/add/tr2"}]}
]
}]}Créer votre propre intégration
Shopify, Stripe et BigCommerce sont des intégrations que nous avons écrites. Voici celle que vous écrire. Décrivez votre service avec une URL de base et une liste d’actions, installez-le sur un widget, et l’agent IA l’appelle en cours de conversation. Les systèmes en aval ne peuvent pas le distinguer d’une intégration native. C’est précisément le but: si votre boutique, votre facturation ou votre CRM fonctionne sur une solution que nous ne connaissons pas, vous n’avez plus besoin d’attendre que nous la prenions en charge.
1 · L’enregistrer
curl -X POST https://conecto.chat/api/v1/integrations/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{
"slug": "acme-store",
"name": "Acme Store",
"base_url": "https://api.acme.example/conecto",
"auth_type": "bearer",
"credential": "sk_live_...",
"actions": [
{"name": "catalog.search_products", "path": "/search",
"description": "Search the Acme catalog by keyword.",
"parameters": [{"name": "query", "type": "string", "required": true},
{"name": "limit", "type": "integer"}]},
{"name": "orders.get_status", "path": "/orders/status", "risk": "verified_read",
"description": "Look up one of the visitor's orders by number.",
"parameters": [{"name": "order_number", "type": "string", "required": true}]},
{"name": "warranty.register", "path": "/warranty", "risk": "write",
"description": "Register a warranty for a product the visitor owns.",
"parameters": [{"name": "serial", "type": "string", "required": true}]}
]
}'The response includes a signing_secret. You need it to verify our calls — it is readable on every later GET too, and {"rotate_signing_secret": true} rolls it.
2 · L’installer sur un widget
curl -X POST https://conecto.chat/api/v1/integrations/acme-store/install/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{"widget_ids": [7], "actions": ["catalog.search_products", "orders.get_status"]}'actions is an allowlist — omit it to enable everything you declared, or pass [] to keep the integration installed but idle. Nothing is exposed until you install it, and installing is per widget.
3 · Répondre à l’appel
We POST a signed JSON envelope to base_url + path. Verify the signature the same way you verify a webhook — one routine covers both directions:
// POST https://api.acme.example/conecto/search
{
"action": "catalog.search_products",
"integration": "acme-store",
"arguments": { "query": "trail shoes", "limit": 3 },
"source": "ai_agent",
"idempotency_key": "8f14e45fceea167a...",
"workspace": { "id": 12 },
"widget": { "id": 7, "key": "w_..." },
"conversation": { "id": 4821 },
"visitor": { "session": "…", "email": "maya@acme.io",
"verified": true, "verified_email": "maya@acme.io", "name": "Maya" }
}
// Headers: X-Conecto-Signature: sha256=<HMAC-SHA256(signing_secret, raw body)>
// X-Conecto-Timestamp, X-Conecto-Action, X-Conecto-Integration,
// X-Conecto-Idempotency-KeyRépondre avec l’une de cinq formes:
{"ok": true, "result": {...}} // success — the agent reads it
{"ok": true, "result": {...}, "blocks": [...]} // …and attach rich content to the reply
{"ok": true, "not_found": true} // nothing matched (a normal outcome)
{"verify_required": true} // "I need a verified visitor for this"
{"ok": false, "error": "Already cancelled."} // a failure the agent may relayUn simple objet JSON dépourvu de ces clés est considéré comme le résultat lui-même. Vous pouvez donc diriger une action vers un point de terminaison existant. Limites: 8 s délai d’expiration, 128 Ko de réponse. Tout ce que vous renvoyez arrive au modèle comme données dans une enveloppe JSON, jamais comme instructions. Les clés qui ressemblent à des secrets sont supprimées à l’entrée.
4 · Tester avant qu’un visiteur ne le fasse
curl -X POST https://conecto.chat/api/v1/integrations/acme-store/actions/catalog.search_products/run/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{"arguments": {"query": "trail shoes"}}'This runs the real call path — signature, credential, timeout, parsing — and shows you the envelope the agent will get. Pass conversation_id to run it in the context of a live chat, which is the only way a verified action can succeed.
Niveaux de risque et l’unique règle impossible à désactiver
public_readcatalogue, disponibilité, documentation, aucune identité requiseverified_readles données d’une personne. Refusé jusqu’à la vérification de l’adresse e-mail du visiteurpublic_writeune modification ne nécessitant aucune identité, comme l’inscription à une newsletter ou la collecte d’un prospectwriteune modification du compte d’une personne. Vérifiée et jamais retentée silencieusementPour les deux derniers, l’adresse vérifiée est fournie par nous, in visitor.verified_email — never taken from the model's arguments. Verification comes from an emailed code, or from your own site attestation d’un utilisateur connecté. Aucun réglage du widget ne peut dispenser une action de cette règle, car «à qui appartient cette commande?» n’est pas une question à laquelle un interrupteur doit répondre.
Actions réservées: votre boutique, intégrée comme une solution native
A few action names mean something to the platform itself. Declare catalog.search_products returning {"products": [{title, url, image, price_from, currency, available}]} and its results become product cards under the AI's replies and fill the widget's Home showcase — the same treatment a Shopify connection gets, from whatever your catalog actually runs on. orders.get_status, orders.get_tracking, orders.list_recent et account.lookup are reserved the same way, and are always verified reads. GET /integrations/{slug}/actions/ returns the full catalog with the shape each one expects.
Les intégrations personnalisées font partie des offres Agent IA et sont limitées à 20 par espace de travail, avec 40 actions chacune. Les appels sortants utilisent uniquement HTTPS, sont résolus puis épinglés à une adresse IP publique avant la connexion, et les redirections sont refusées. Une URL d’intégration ne peut donc jamais pointer vers une ressource privée.
Démarrage rapide: un bot personnalisé en trois étapes
A bot is a webhook and a reply. Subscribe to message.created, answer through the messages endpoint, and the widget renders it live — quick-reply buttons, email capture, the native ticket form, typing indicators. Turn the built-in AI off on the widget and your bot owns the conversation.
1. Abonner votre serveur (Paramètres → Développeurs → Webhooks, ou par API):
curl -X POST https://conecto.chat/api/v1/webhooks/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{"url": "https://bots.yourapp.com/conecto",
"events": ["message.created", "conversation.created"]}'2. Vérifier et lire l’événement. Every delivery is signed: X-Conecto-Signature: sha256=<HMAC-SHA256(secret, raw body)>.
// Node/Express
app.post('/conecto', express.raw({ type: '*/*' }), (req, res) => {
const sig = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body).digest('hex')
if (sig !== req.get('X-Conecto-Signature')) return res.sendStatus(401)
const { event, data } = JSON.parse(req.body)
if (event === 'message.created' && data.message.sender === 'visitor') {
handle(data) // your bot logic — reply async, respond 200 fast
}
res.sendStatus(200)
})3. Répondre avec des fonctionnalités.
curl -X POST https://conecto.chat/api/v1/conversations/42/messages/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{"body": "I can refund order #1284 right away — confirm?",
"buttons": ["Yes, refund it", "Talk to a human"]}'Button taps come back to your webhook as normal visitor messages containing the button text — your bot's state machine lives entirely on your side. Other reply powers: blocks sends images, GIFs, video and cards (see Contenu enrichi), {"ask_email": true} renders the email-capture form, {"ticket_form": true} attaches the native open-a-ticket form, products renders tappable product cards, {"internal": true} leaves an agent-only note, /typing/ shows “…is typing”, /handoff/ summons a human, and PATCH closes the conversation when you're done.
Livre de recettes des bots
Des recettes réellement utilisées en production. Chacune comprend un gestionnaire de webhook et quelques appels API.
1 · Le bot d’action, modifie des données dans votre logiciel
Comme le webhook atteint votre peut tout faire sur votre serveur que votre backend sait faire: écrire dans votre base de données, appeler votre système de facturation ou modifier une entrée dans votre logiciel comptable. Combiné à identité attestée vous savez exactement qui pose la question. «Classer ce reçu de 250 $ dans Marketing» peut donc être exécuté en toute sécurité:
async function handle({ conversation, visitor, message }) {
const convo = conversation.id
// "file 250 under Marketing" — your parsing, your rules
const cmd = parseAccountingCommand(message.body)
if (!cmd) return reply(convo, "Tell me e.g. 'file 250 under Marketing'.")
// Only act for users YOUR backend has vouched for (logged in on your site)
if (!visitor.verified)
return reply(convo, "Please sign in first, then ask me again.", ["Log in"])
await ledger.addEntry({ // <- YOUR accounting system
account: cmd.account,
amount: cmd.amount,
user: visitor.verified_email, // identity Conecto guarantees
})
await reply(convo,
`Done — $${cmd.amount} filed under ${cmd.account}. Anything else?`,
["Show this month's entries", "Undo that"])
}Le même modèle couvre «ajouter une place à mon offre», «renommer mon projet» ou «réserver un créneau jeudi». Le bot n’est qu’une fine couche conversationnelle sur votre propre API. Vous préférez utiliser l’IA intégrée plutôt que créer un bot? Enregistrer les mêmes points de terminaison comme intégration et l’IA de Conecto les appelle en cours de conversation, derrière la même vérification d’identité, sans machine à états à maintenir. Si vous utilisez déjà MCP, un serveur d’outils MCP distant fonctionne aussi: Tableau de bord → Agents IA → Intégrations.
2 · Bot de suivi de commande
if (/where.*order|track/i.test(message.body)) {
await typing(convo, 'OrderBot', true)
const order = await shop.lastOrder(visitor.email) // your store
await reply(convo, order
? `Order #${order.id} is ${order.state} — arriving ${order.eta}.`
: "I couldn't find an order for this email.",
order ? [`Track #${order.id}`] : undefined)
}3 · Bot de déviation des tickets
Rechercher d’abord dans votre centre d’aide; ouvrir un ticket seulement si aucun résultat ne correspond:
const { articles } = await api('GET', '/articles/?q=' + q + '&published=true')
if (articles.length)
await reply(convo, `This might help: “${articles[0].title}”. Did that solve it?`,
["Solved it", "Open a ticket"])
else
await api('POST', `/conversations/${convo}/messages/`,
{ body: "Let's get this to the team.", ticket_form: true })4 · Bot de qualification des prospects
// no email yet? capture it natively, then enrich + route
if (!visitor.email)
return api('POST', `/conversations/${convo}/messages/`,
{ body: "Happy to help — what's your work email?", ask_email: true })
await api('POST', '/contacts/', { email: visitor.email,
custom_fields: { lead_source: 'chat', intent: classify(message.body) } })
await api('POST', `/conversations/${convo}/messages/`,
{ body: "Routing you to sales…", internal: true })
await api('POST', `/conversations/${convo}/assign/`, { user_id: SALES_USER_ID })
await api('POST', `/conversations/${convo}/handoff/`)5 · Messages proactifs du cycle de vie
Envoyer à une session de visiteur sans attendre son message, par exemple mises à jour d’expédition, rappels d’essai ou récupération de panier:
curl -X POST https://conecto.chat/api/v1/widgets/7/visitors/$SESSION/message/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{"body": "Your order shipped — want live tracking?",
"buttons": ["Track my order"]}'Cart recovery works the same way, with products putting the item itself back in front of them as a tappable card:
-d '{"body": "Still thinking it over? Your cart is saved:",
"products": [{"title": "Trail Runner 2", "price_from": "89.00",
"currency": "USD", "image": "https://cdn.you.com/tr2.jpg",
"url": "https://shop.you.com/cart"}]}'6 · Une boutique inconnue, connectée à l’IA
Aucun webhook ni machine à états: déclarez l’action de catalogue réservée, dirigez-la vers votre point de terminaison de recherche, et l’IA recommande des articles de votre catalogue avec de véritables cartes produit.
// POST /conecto/search on your server
app.post('/conecto/search', verifyConectoSignature, async (req, res) => {
const { query, limit = 4 } = req.body.arguments
const hits = await catalog.search(query, { limit }) // <- YOUR catalog
res.json({ ok: hits.length > 0, not_found: hits.length === 0,
products: hits.map(p => ({
title: p.name, url: p.permalink, image: p.thumbnail,
price_from: p.price.toFixed(2), currency: p.currency,
available: p.stock > 0,
})) })
})Voilà toute l’intégration. Les cartes, la sélection Accueil, la règle «appeler à nouveau avant de mentionner un produit» et l’interdiction pour le modèle de coller des URL brutes accompagnent automatiquement le nom réservé.
7 · Suivi CSAT
Subscribe to conversation.rated; thank promoters, rescue detractors:
if (event === 'conversation.rated') {
if (data.rating.score <= 2)
await api('POST', '/tickets/', { email: data.visitor.email,
message: `Low CSAT (${data.rating.score}/5): ${data.rating.comment}`,
priority: 'high' })
else
await push(data.widget.id, data.visitor.session,
"Glad we could help! Here's 10% off your next order: THANKS10")
}Agir sur vos propres systèmes, en toute sécurité
Trois règles rendent les bots d’action prêts pour la production:
1. Commencer par l’identité. Only mutate data for sessions where visitor.verified is true — your backend vouched for them via /identify/. The vouch is time-boxed and session-scoped; revoke it with /unverify/ on logout.
2. Confirmer les étapes destructrices. Envoyer l’action sous forme de question avec des boutons («Rembourser la commande nº 1284?» · Oui / Non), le clic revient sous forme de texte du bouton, votre gestionnaire idempotent s’exécute et la transcription documente le consentement.
3. Transférer à un humain en cas de doute. POST /conversations/{id}/handoff/ flags the thread human-needed (your routing rules apply), and {"internal": true} notes give the teammate full context your bot gathered.
Référence des points de terminaison
Conversations et messages
/conversations/Newest first. Filters: status (open · pending · closed), widget_id, session; paginate with limit/before_id.
/conversations/{id}/Conversation + visitor-facing transcript (≤500 messages, since_id for increments).
/conversations/{id}/messages/body (≤4000), buttons (≤6 × 60 chars), blocks (≤10 — see Contenu enrichi), ask_email, ticket_form (native ticket form), internal (agent-only note), products (≤4 cards rendered as a mini carousel under the bubble: {title, url, price_from, currency, image} — title required, everything else optional). Honors Idempotency-Key. 409 when closed.
/conversations/{id}/typing/{"name": "OrderBot", "on": true} , expire automatiquement après environ 8 s.
/conversations/{id}/assign/{"user_id": 12} (or null to unassign) — teammates come from /members/.
/conversations/{id}/handoff/Signaler qu’une intervention humaine est nécessaire; apparaît dans la boîte de réception comme un transfert de l’IA, règles de routage comprises.
/conversations/{id}/{"status": "closed" | "open"}. Closing fires conversation.closed.
/widgets/{widget_id}/visitors/{session}/message/Proactive push: reuses the session's live conversation or opens one; body + buttons + blocks + products. Honors Idempotency-Key.
Intégrations, votre propre logiciel comme intégration de premier ordre
/integrations/ · POSTList, or register one: slug, name, base_url (https), optional description/icon_url/homepage_url, auth_type (none · bearer · api_key · basic) + credential, and actions inline. Returns the signing_secret you verify our calls with.
/integrations/{slug}/ · PATCH · DELETEPATCH takes the same fields; actions upserts by name and replace_actions: true makes your list authoritative (deploy-from-source). rotate_signing_secret rolls the secret; active: false switches it off everywhere at once.
/integrations/{slug}/actions/ · POST · DELETE /{name}/Each action: name (dotted, e.g. orders.lookup), path, method, risk, description (what the AI reads), parameters ({name, type, required, description, enum}), ai_enabled. GET also returns the reserved-action catalog.
/integrations/{slug}/install/ · /uninstall/widget_ids (all visible widgets when omitted), actions allowlist, enabled.
/integrations/{slug}/actions/{name}/run/Invoke it now through the real call path. arguments, optional conversation_id for visitor context. Returns {status, result, blocks}.
Médias
/media/Multipart file (≤10 MB) → {media: {url, absolute_url, filename, content_type, size, is_image}}. Put url in an image/video/file block.
Tickets
/tickets/Filters: status (open · pending · resolved), email; paginated.
/tickets/email, message, optional name, category_id, priority (low · normal · high · urgent), submission_id (UUID idempotency key). The requester gets the standard acknowledgement email.
/tickets/{id}/ · PATCHDetail with the comment thread; PATCH status, priority, assignee_user_id.
/tickets/{id}/reply/body — emails the requester; internal: true for a private note.
/ticket-categories/Les catégories de l’espace de travail pour la création de tickets.
Contacts, synchronisez votre base d’utilisateurs
/contacts/Upsert by email (201 created / 200 updated): profile fields + custom_fields (≤30 keys, merged).
/contacts/ · GET/PATCH/DELETE /contacts/{id}/Search with email (exact) or q; standard pagination.
Visiteurs, identité et personnalisation
/widgets/{widget_id}/visitors/{session}/identify/email (required), name, data (custom fields), verified + verify_hours (≤72, default 12) — see Identité. Les sessions peuvent être créées à l’avance.
/widgets/{widget_id}/visitors/{session}/unverify/Révoquer l’attestation, appeler lors de la déconnexion.
/widgets/{widget_id}/visitors/{session}/Identité actuelle: e-mail, nom, état de vérification.
Base de connaissances
/articles/q full-text search, published=true filter — perfect for bot answer lookups.
/articles/ · GET/PATCH/DELETE /articles/{id}/Synchronisez la documentation par programmation. Le contenu HTML est nettoyé côté serveur selon une liste d’autorisation.
Configuration et métadonnées
/widgets/{widget_id}/ · PATCHLire ou mettre à jour la configuration du widget, accueil, couleurs, boutons d’action de l’Accueil, questions rapides, horaires d’ouverture et les mêmes champs validés que l’outil de personnalisation du tableau de bord.
/members/Membres de l’équipe (user_id, name, role) pour l’attribution.
/stats/Compteurs en direct: conversations ouvertes/en attente, tickets ouverts, contacts, articles publiés.
/me/Votre espace de travail, la portée de l’identifiant et vos widgets (ID + clés d’intégration).
/schema/La description lisible par machine de tout ce qui précède: points de terminaison, événements, types de blocs, actions réservées, codes d’erreur et limites. Générez votre client à partir de celle-ci.
Identité: éviter une nouvelle vérification pour les utilisateurs connectés
Le widget conserve son identifiant de session dans le navigateur. Lisez-le côté client, envoyez-le à votre backend avec votre propre cookie de session, puis attestez l’identité de serveur à serveur:
# your backend, right after your own auth check
curl -X POST https://conecto.chat/api/v1/widgets/7/visitors/$CONECTO_SESSION/identify/ \
-u "ck_...:cs_..." -H "Content-Type: application/json" \
-d '{"email": "maya@acme.io", "name": "Maya",
"verified": true, "verify_hours": 24,
"data": {"plan": "pro", "customer_since": "2024"}}'While the vouch lasts, Conecto treats the email as verified: the widget knows their name, chats attach to the right CRM contact, and flows that normally demand an email OTP — refund lookups, order changes, sensitive account data through the AI's tools — proceed without one. Your attestation is as strong as our code-by-email, because you actually authenticated them. Only vouch sessions your backend has verified, and call /unverify/ on logout.
Webhooks
Manage in the dashboard or via GET/POST /webhooks/ et DELETE /webhooks/{id}/ (https only, ≤10 per workspace, optional per-widget scope). Deliveries carry X-Conecto-Event, X-Conecto-Delivery, X-Conecto-Timestamp and the HMAC signature, and time out after 2.5s — respond 200 fast, process async. Events:
conversation.createdpremier message d’un visiteur dans un nouveau fil, y compris les formulaires de l’onglet Accueilmessage.createdchaque message d’un visiteur, le déclencheur du bot personnaliséconversation.closedfermée par un agent ou l’APIconversation.assignedattribué à un membre de l’équipe ou retiré de son attributionconversation.handoffsignalé comme nécessitant une intervention humaineconversation.ratednote CSAT du visiteur (1–5 + commentaire)ticket.createdtoute source: formulaire du widget, IA, bots, automatisations, APIticket.updatedstatut, priorité ou agent attribué modifiécontact.createdune nouvelle personne est entrée dans le CRM, synchronisez-la avec le vôtrevisitor.identifiedune session a été identifiée par l’API, avec son état d’attestationLa livraison est au moins une fois et sans ordre garanti. Every payload carries an id (also in X-Conecto-Delivery): store it, skip ids you have already handled, and both replays and duplicates stop being your problem. When your bot answers a delivery, pass that same id as the Idempotency-Key and a redelivery can never make it speak twice.
app.post('/conecto', express.raw({ type: '*/*' }), async (req, res) => {
const sig = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body).digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.get('X-Conecto-Signature') || '')))
return res.sendStatus(401)
const { id, event, data } = JSON.parse(req.body)
res.sendStatus(200) // ack first, work after
if (await seen(id)) return // at-least-once: dedupe on the delivery id
if (event === 'message.created' && data.message.sender === 'visitor') {
await fetch(`https://conecto.chat/api/v1/conversations/${data.conversation.id}/messages/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': id, ...auth },
body: JSON.stringify({ body: await answer(data) }),
})
}
})Everything here is designed to sit under an SDK: stable envelopes, slug-addressed integrations, typed errors, cursor pagination, idempotent writes, one signature scheme in both directions, and GET /schema/ to generate from. Building something? Parlez-nous , nous voulons que les développeurs construisent sur Conecto et nous donnons la priorité à leurs demandes.