Skip to content

Setup guide · Telegram

Connect Telegram to Truplexy

Create a Telegram bot, point its webhook at your server, and add a small handler that asks Truplexy and replies in the chat.

Before you start

  • A Truplexy account on any plan.
  • A Telegram account to create the bot with @BotFather.
  • A server reachable over https, where the chat key and the code below run.

What you’ll connect:

Secrets the code reads from the environment, besides TRUPLEXY_CHAT_KEY:

VariableWhat it is
TELEGRAM_TOKENThe bot token from @BotFather.

1. Issue a chat key

  1. In the dashboard, open Integrations → Add a channel → Telegram.
  2. Add a Telegram channel, so tickets show where they came from, and issue its chat key.
  3. Store it on your server as TRUPLEXY_CHAT_KEY, in .env or your host’s settings. Never put it in a website, mobile app or theme file: anyone could copy it.

2. Set up Telegram

  1. Message @BotFather in Telegram and run /newbot. Keep the token it gives you as a server secret.
  2. Point Telegram at your server by opening https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://your-server.com/telegram in a browser, with your token and your server’s address.

3. Add the server code

This handler keeps one Truplexy conversation per customer thread, asks Truplexy for the reply and sends it back on Telegram. The second file calls the chat API; it is the same on every channel. The sample keeps threads in memory: use your database in production.

server.js
app.post("/telegram", express.json(), async (req, res) => {
  res.sendStatus(200);
  const msg = req.body.message;
  if (!msg?.text || !msg.from || msg.sender_chat) return;
  const { reply } = await ask(`tg:${msg.chat.id}:${msg.from.id}`, msg.text, { contacts: [{ type: "telegram", value: String(msg.from.id) }] });
  if (reply) await sendTelegram(msg.chat.id, reply);
});

const sendTelegram = (chat_id, text) =>
  fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id, text }),
  });
truplexy.js
// truplexy.js — runs on your server only.
// TRUPLEXY_CHAT_KEY lives in your secret manager, never in a browser or app.
const API = "https://api.truplexy.com/v2";

export class TruplexyApiError extends Error {
  constructor(status, error, requestId) {
    super(error.message ?? "Truplexy request failed");
    this.status = status; this.code = error.code ?? "UPSTREAM_ERROR";
    this.requestId = error.request_id ?? requestId; this.details = error;
  }
}
export async function call(path, body, method = "POST") {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${process.env.TRUPLEXY_CHAT_KEY}`, "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new TruplexyApiError(res.status, data.error ?? {}, res.headers.get("X-Request-ID"));
  return data;
}

export async function upsertCustomer(customer) {
  try { return await call("/customer", customer, "PUT"); }
  catch (e) {
    if (e.status !== 409 || e.code !== "CUSTOMER_EXISTS") throw e;
    return call("/customer", customer, "PUT"); // one retry only
  }
}
export async function createConversation(customer, channelId) {
  const known = customer == null ? null : await upsertCustomer(customer);
  if (customer != null && (typeof known?.id !== "string" || !known.id)) throw new TruplexyApiError(502, { code: "UPSTREAM_ERROR", message: "Missing customer ID" });
  return call("/conversations", { ...(known && { customer_id: known.id }), ...(channelId && { channel_id: channelId }) });
}

// One Truplexy conversation per customer thread. Use your database in production.
const threads = new Map();
export const threadFor = (conversationId) => [...threads].find(([, c]) => c === conversationId)?.[0];

export async function ask(thread, text, customer, channelId) {
  let id = threads.get(thread);
  if (!id) threads.set(thread, (id = (await createConversation(customer, channelId)).id));
  const r = await call(`/conversations/${id}/messages`, { message: text.slice(0, 4000) });
  // "escalated" (a person has it) and "context" have no reply: send nothing.
  return { reply: r.reply, status: r.status, conversationId: id };
}

export const handoff = (id) => call(`/conversations/${id}/handoff`);
app.py
import os
import httpx
from fastapi import BackgroundTasks, FastAPI, Request, Response
from truplexy import ask

app = FastAPI()
TELEGRAM = f"https://api.telegram.org/bot{os.environ['TELEGRAM_TOKEN']}"


@app.post("/telegram")
async def telegram(request: Request, background: BackgroundTasks):
    background.add_task(handle_update, await request.json())
    return Response(status_code=200)


async def handle_update(update: dict):
    msg = update.get("message") or {}
    if not msg.get("text") or not msg.get("from") or msg.get("sender_chat"):
        return
    r = await ask(f"tg:{msg['chat']['id']}:{msg['from']['id']}", msg["text"], {"contacts": [{"type": "telegram", "value": str(msg["from"]["id"])}]})
    if r["reply"]:
        await send_telegram(msg["chat"]["id"], r["reply"])


async def send_telegram(chat_id, text: str):
    async with httpx.AsyncClient() as http:
        await http.post(f"{TELEGRAM}/sendMessage", json={"chat_id": chat_id, "text": text})
truplexy.py
# truplexy.py — runs on your server only.
# TRUPLEXY_CHAT_KEY lives in your secret manager, never in a browser or app.
import os
import httpx

API = "https://api.truplexy.com/v2"
_http = httpx.AsyncClient(
    base_url=API,
    headers={"Authorization": f"Bearer {os.environ['TRUPLEXY_CHAT_KEY']}"},
    timeout=60,
)


class TruplexyApiError(Exception):
    def __init__(self, response, error):
        super().__init__(error.get("message", "Truplexy request failed"))
        self.status, self.code = response.status_code, error.get("code", "UPSTREAM_ERROR")
        self.request_id = error.get("request_id") or response.headers.get("x-request-id")
        self.details = error


async def call(path: str, body: dict | None = None, method: str = "POST") -> dict:
    res = await _http.request(method, path, **({"json": body} if body is not None else {}))
    try:
        data = res.json()
    except ValueError:
        data = {}
    if res.is_error:
        raise TruplexyApiError(res, data.get("error", {}))
    return data


async def upsert_customer(customer: dict) -> dict:
    try:
        return await call("/customer", customer, "PUT")
    except TruplexyApiError as e:
        if e.status != 409 or e.code != "CUSTOMER_EXISTS":
            raise
        return await call("/customer", customer, "PUT")


async def create_conversation(customer=None, channel_id=None) -> dict:
    opening = {"channel_id": channel_id} if channel_id else {}
    if customer is not None:
        opening["customer_id"] = (await upsert_customer(customer))["id"]
    return await call("/conversations", opening)


# One Truplexy conversation per customer thread. Use your database in production.
threads: dict[str, str] = {}


def thread_for(conversation_id: str) -> str | None:
    return next((t for t, c in threads.items() if c == conversation_id), None)


async def ask(thread: str, text: str, customer=None, channel_id=None) -> dict:
    conv = threads.get(thread)
    if not conv:
        conv = threads[thread] = (await create_conversation(customer, channel_id))["id"]
    r = await call(f"/conversations/{conv}/messages", {"message": text[:4000]})
    # "escalated" (a person has it) and "context" have no reply: send nothing.
    return {"reply": r.get("reply"), "status": r["status"], "conversation_id": conv}


async def handoff(conversation_id: str) -> dict:
    return await call(f"/conversations/{conversation_id}/handoff")
telegram.php
<?php // telegram.php — the URL you gave setWebhook
require __DIR__ . '/truplexy.php';

$update = json_decode(file_get_contents('php://input'), true);
http_response_code(200);
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();   // answer at once, keep working

$msg = $update['message'] ?? null;
if (!empty($msg['text']) && !empty($msg['from']) && empty($msg['sender_chat'])) {
    $r = ask('tg:' . $msg['chat']['id'] . ':' . $msg['from']['id'], $msg['text'], ['contacts' => [['type' => 'telegram', 'value' => (string) $msg['from']['id']]]]);
    if ($r['reply']) {
        post_json('https://api.telegram.org/bot' . getenv('TELEGRAM_TOKEN') . '/sendMessage',
            ['chat_id' => $msg['chat']['id'], 'text' => $r['reply']]);
    }
}
truplexy.php
<?php // truplexy.php — runs on your server only.
// TRUPLEXY_CHAT_KEY lives in your secret manager, never in a browser or app.
const TRUPLEXY_API = 'https://api.truplexy.com/v2';

class TruplexyApiError extends RuntimeException {
    public function __construct(public int $status, public array $details, public string $requestId = '') {
        parent::__construct($details['message'] ?? 'Truplexy request failed');
    }
    public function apiCode(): string { return $this->details['code'] ?? 'UPSTREAM_ERROR'; }
}

/** Calls an API with a JSON body and returns the decoded answer. */
function post_json(string $url, ?array $body = null, array $headers = [], string $method = 'POST'): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json'], $headers),
    ]);
    if ($method !== 'GET') curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body ?? new stdClass()));
    $requestId = '';
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $line) use (&$requestId) {
        if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13));
        return strlen($line);
    });
    $res = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $data = json_decode((string) $res, true) ?? [];
    if ($res === false || $status >= 300) throw new TruplexyApiError($status ?: 502, $data['error'] ?? [], $data['error']['request_id'] ?? $requestId);
    return $data;
}

function truplexy(string $path, ?array $body = null, string $method = 'POST'): array {
    return post_json(TRUPLEXY_API . $path, $body, ['Authorization: Bearer ' . getenv('TRUPLEXY_CHAT_KEY')], $method);
}

function upsert_customer(array $customer): array {
    try { return truplexy('/customer', $customer, 'PUT'); }
    catch (TruplexyApiError $e) {
        if ($e->status !== 409 || $e->apiCode() !== 'CUSTOMER_EXISTS') throw $e;
        return truplexy('/customer', $customer, 'PUT');
    }
}
function create_conversation(?array $customer = null, ?string $channelId = null): array {
    $opening = $channelId ? ['channel_id' => $channelId] : [];
    if ($customer !== null) {
        $known = upsert_customer($customer);
        if (empty($known['id'])) throw new TruplexyApiError(502, ['code' => 'UPSTREAM_ERROR', 'message' => 'Missing customer ID']);
        $opening['customer_id'] = $known['id'];
    }
    return truplexy('/conversations', $opening ?: null);
}

// One Truplexy conversation per customer thread. Use your database in production.
function threads(?array $save = null): array {
    $file = __DIR__ . '/threads.json';
    if ($save !== null) file_put_contents($file, json_encode($save), LOCK_EX);
    return $save ?? (json_decode(@file_get_contents($file) ?: '{}', true));
}

function thread_for(string $conversationId): ?string {
    return array_search($conversationId, threads(), true) ?: null;
}

function ask(string $thread, string $text, ?array $customer = null, ?string $channelId = null): array {
    $threads = threads();
    $id = $threads[$thread] ??= create_conversation($customer, $channelId)['id'];
    threads($threads);
    $r = truplexy("/conversations/$id/messages", ['message' => mb_substr($text, 0, 4000)]);
    // "escalated" (a person has it) and "context" have no reply: send nothing.
    return ['reply' => $r['reply'], 'status' => $r['status'], 'conversationId' => $id];
}

function handoff(string $conversationId): array {
    return truplexy("/conversations/$conversationId/handoff");
}
main.go
http.HandleFunc("POST /telegram", func(w http.ResponseWriter, r *http.Request) {
	var update struct {
		Message struct {
			Text string
			Chat struct{ ID int64 }
            From struct{ ID int64 }
            SenderChat any `json:"sender_chat"`
		}
	}
	json.NewDecoder(r.Body).Decode(&update)
	w.WriteHeader(http.StatusOK)
	m := update.Message
	if m.Text == "" || m.From.ID == 0 || m.SenderChat != nil {
		return
	}
	go func() {
		chat := strconv.FormatInt(m.Chat.ID, 10)
		if a, err := ask("tg:"+chat+":"+strconv.FormatInt(m.From.ID, 10), m.Text, identify("telegram", strconv.FormatInt(m.From.ID, 10))); err == nil && a.Reply != "" {
			sendTelegram(chat, a.Reply)
		}
	}()
})

func sendTelegram(chatID, text string) error {
	return sendJSON("POST", "https://api.telegram.org/bot"+os.Getenv("TELEGRAM_TOKEN")+"/sendMessage",
		map[string]string{"chat_id": chatID, "text": text}, nil)
}
truplexy.go
// truplexy.go — runs on your server only.
// TRUPLEXY_CHAT_KEY lives in your secret manager, never in a browser or app.
package main

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"sync"
	"time"
)

const truplexyAPI = "https://api.truplexy.com/v2"

var httpClient = &http.Client{Timeout: 60 * time.Second}

type APIError struct {
    Status int
    Code, Message, RequestID string
    Details map[string]any
}
func (e *APIError) Error() string { return e.Code + ": " + e.Message }

// sendJSON calls an API with a JSON body, decoding the answer into out when given.
func sendJSON(method, url string, body any, headers map[string]string, out ...any) error {
	var payload io.Reader = http.NoBody
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return err
		}
		payload = bytes.NewReader(b)
	}
	req, err := http.NewRequest(method, url, payload)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	res, err := httpClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode >= 300 {
		msg, _ := io.ReadAll(res.Body)
        var data struct { Error map[string]any }
        json.Unmarshal(msg, &data)
        code, _ := data.Error["code"].(string)
        message, _ := data.Error["message"].(string)
        requestID, _ := data.Error["request_id"].(string)
        if requestID == "" { requestID = res.Header.Get("X-Request-ID") }
        return &APIError{res.StatusCode, code, message, requestID, data.Error}
	}
	if len(out) == 0 {
		return nil
	}
	return json.NewDecoder(res.Body).Decode(out[0])
}

func call(path string, body, out any) error { return callMethod("POST", path, body, out) }
func callMethod(method, path string, body, out any) error {
	auth := map[string]string{"Authorization": "Bearer " + os.Getenv("TRUPLEXY_CHAT_KEY")}
	if out == nil {
		return sendJSON(method, truplexyAPI+path, body, auth)
	}
	return sendJSON(method, truplexyAPI+path, body, auth, out)
}

func upsertCustomer(customer map[string]any) (string, error) {
    var result struct { ID string }
    err := callMethod("PUT", "/customer", customer, &result)
    if e, ok := err.(*APIError); ok && e.Status == 409 && e.Code == "CUSTOMER_EXISTS" {
        err = callMethod("PUT", "/customer", customer, &result)
    }
    if err == nil && result.ID == "" { err = &APIError{Status: 502, Code: "UPSTREAM_ERROR", Message: "Missing customer ID"} }
    return result.ID, err
}
type ConversationOptions struct { Customer map[string]any; ChannelID string }
func identify(kind, value string) ConversationOptions {
    return ConversationOptions{Customer: map[string]any{"contacts": []map[string]string{{"type": kind, "value": value}}}}
}
func createConversation(options ConversationOptions) (string, error) {
    opening := map[string]string{}
    if options.ChannelID != "" { opening["channel_id"] = options.ChannelID }
    if options.Customer != nil {
        id, err := upsertCustomer(options.Customer)
        if err != nil { return "", err }
        opening["customer_id"] = id
    }
    var result struct { ID string }
    err := call("/conversations", opening, &result)
    return result.ID, err
}

// One Truplexy conversation per customer thread. Use your database in production.
var (
	mu      sync.Mutex
	threads = map[string]string{}
)

func threadFor(conversationID string) string {
	mu.Lock()
	defer mu.Unlock()
	for t, c := range threads {
		if c == conversationID {
			return t
		}
	}
	return ""
}

type Answer struct {
	Reply          string `json:"reply"` // empty for "escalated" and "context": send nothing
	Status         string `json:"status"`
	ConversationID string `json:"conversationId"`
}

func ask(thread, text string, options ...ConversationOptions) (Answer, error) {
	mu.Lock()
	id, ok := threads[thread]
	mu.Unlock()
	if !ok {
        option := ConversationOptions{}
        if len(options) > 0 { option = options[0] }
        var err error
        id, err = createConversation(option)
        if err != nil { return Answer{}, err }
		mu.Lock()
		threads[thread] = id
		mu.Unlock()
	}
	if r := []rune(text); len(r) > 4000 {
		text = string(r[:4000])
	}
	var a Answer
	err := call("/conversations/"+id+"/messages", map[string]string{"message": text}, &a)
	a.ConversationID = id
	return a, err
}

func handoff(conversationID string) error {
	return call("/conversations/"+conversationID+"/handoff", map[string]any{}, nil)
}

Register the http.HandleFunc routes inside main(); the functions sit beside it in package main.

4. Send team replies back

When someone replies from Tickets, Truplexy calls your webhook with message.created. Verify the signature, then deliver the message on Telegram.

Set the webhook up under Integrations → Webhook and keep its signing secret as TRUPLEXY_WEBHOOK_SECRET; the webhooks guide has the full contract.

webhook.js
// Your team acted in Truplexy → act on it here.
app.post("/truplexy-webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("X-Truplexy-Signature") ?? "", process.env.TRUPLEXY_WEBHOOK_SECRET)) return res.sendStatus(401);
  res.sendStatus(200);                                   // answer within 2.5 seconds
  const { type, data } = JSON.parse(raw);
  const conversationId = data.conversation_id;
  if (type === "message.created") {                      // your team replied
    const text = data.message.content;
    const agent = data.message.agent ?? "Support";
    const thread = threadFor(conversationId);
    await sendTelegram(thread.split(":")[1], `${agent}: ${text}`);
  }
});
verify.js
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
webhook.py
import json
import os
from fastapi import BackgroundTasks, Request, Response
from truplexy import thread_for
from verify import verify


# Your team acted in Truplexy → act on it here.
@app.post("/truplexy-webhook")
async def truplexy_webhook(request: Request, background: BackgroundTasks):
    raw = await request.body()
    if not verify(raw, request.headers.get("X-Truplexy-Signature", ""), os.environ["TRUPLEXY_WEBHOOK_SECRET"]):
        return Response(status_code=401)
    background.add_task(on_event, json.loads(raw))   # answer within 2.5 seconds
    return Response(status_code=200)


async def on_event(event: dict):
    data = event["data"]
    conversation_id = data.get("conversation_id")
    if event["type"] == "message.created":   # your team replied
        text = data["message"]["content"]
        agent = data["message"].get("agent") or "Support"
        thread = thread_for(conversation_id)
        await send_telegram(thread.split(":")[1], f"{agent}: {text}")
verify.py
import hashlib
import hmac
import time


def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    fresh = t.isdigit() and abs(time.time() - int(t)) < 300
    return fresh and hmac.compare_digest(v1, expected)
webhook.php
<?php // webhook.php — your team acted in Truplexy → act on it here.
require __DIR__ . '/truplexy.php';
require __DIR__ . '/verify.php';

$raw = file_get_contents('php://input');
if (!verify_truplexy($raw, $_SERVER['HTTP_X_TRUPLEXY_SIGNATURE'] ?? '', getenv('TRUPLEXY_WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit;
}
http_response_code(200);
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();   // answer at once, keep working

$event = json_decode($raw, true);
$data = $event['data'];
$conversationId = $data['conversation_id'] ?? null;
if ($event['type'] === 'message.created') {   // your team replied
    $text = $data['message']['content'];
    $agent = $data['message']['agent'] ?? 'Support';
    $thread = thread_for($conversationId);
    post_json('https://api.telegram.org/bot' . getenv('TELEGRAM_TOKEN') . '/sendMessage',
        ['chat_id' => explode(':', $thread)[1], 'text' => "$agent: $text"]);
}
verify.php
<?php // verify.php
function verify_truplexy(string $rawBody, string $header, string $secret): bool {
    $parts = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = explode('=', $part, 2) + [1 => ''];
        $parts[$k] = $v;
    }
    $t = $parts['t'] ?? '';
    $expected = hash_hmac('sha256', "$t.$rawBody", $secret);
    return ctype_digit($t) && abs(time() - (int) $t) < 300 && hash_equals($expected, $parts['v1'] ?? '');
}
webhook.go
// Your team replied in Truplexy → deliver it on the platform.
http.HandleFunc("POST /truplexy-webhook", func(w http.ResponseWriter, r *http.Request) {
	raw, _ := io.ReadAll(r.Body)
	if !verify(raw, r.Header.Get("X-Truplexy-Signature"), os.Getenv("TRUPLEXY_WEBHOOK_SECRET")) {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	w.WriteHeader(http.StatusOK)   // answer within 2.5 seconds
	var event struct {
		Type string `json:"type"`
		Data struct {
			ConversationID string `json:"conversation_id"`
			Message        struct{ Content, Agent string }
		}
	}
	if json.Unmarshal(raw, &event) != nil || event.Type != "message.created" {
		return
	}
	agent := event.Data.Message.Agent
	if agent == "" {
		agent = "Support"
	}
	go deliver(threadFor(event.Data.ConversationID), event.Data.Message.Content, agent)
})

func deliver(thread, text, agent string) {
	sendTelegram(strings.Split(thread, ":")[1], agent+": "+text)
}
verify.go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strconv"
	"strings"
	"time"
)

func verify(rawBody []byte, header, secret string) bool {
	var t, v1 string
	for _, part := range strings.Split(header, ",") {
		k, v, _ := strings.Cut(part, "=")
		switch k {
		case "t":
			t = v
		case "v1":
			v1 = v
		}
	}
	ts, err := strconv.ParseInt(t, 10, 64)
	if err != nil || max(time.Now().Unix()-ts, ts-time.Now().Unix()) >= 300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(t + "."))
	mac.Write(rawBody)
	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(v1))
}

Customer identity

Truplexy matches people by their contacts, so one person across channels is one customer. On Telegram, send the person as a telegram contact; add any others you know, such as an email address. Create the conversation with the returned customer_id, and its tickets inherit the customer and the channel.

  • Send 1–20 contacts as {type, value, label?, primary?}, with optional name and metadata. Names allow 120 characters; metadata must be a JSON object up to 4 KB.
  • Contact types are 2–32 lowercase letters, digits, hyphens or underscores, starting with a letter. Values allow 1–254 characters; labels allow 60.
  • Emails are lowercased. Phone numbers lose spaces, dashes, dots and parentheses and must contain 5–20 digits, optionally beginning with +. Other values keep their case.
  • Each contact belongs to one customer within one bot. Mark at most one primary contact per type; otherwise the first becomes primary. Existing primary flags survive an upsert.
  • An upsert finds any matching contact, adds missing contacts, updates supplied name and metadata, and marks the customer seen. Omit optional fields to preserve them.
  • CUSTOMER_CONFLICT means the contacts belong to different customers: nothing is changed. Resolve it in Customers. CUSTOMER_EXISTS is a race: the SDK and helpers retry once.
  • Pass the returned id as customer_id when starting a conversation. Tickets inherit that customer and the conversation’s channel. Existing conversations are not relinked.

Test it

  • Send a message on Telegram. The reply should arrive within a few seconds.
  • For an identified customer, open Customers and check the contact and conversation link. Any tickets should carry that customer too.
  • Ask for a person. The conversation shows up under Tickets → Handoffs.
  • Take it over and reply from the ticket. Your reply should appear on Telegram.

Troubleshooting

ProblemFix
The bot never answersCheck that setWebhook succeeded and that its URL is the public https address of your /telegram route. Telegram only delivers to a webhook that answers.
It answers privately but not in a groupThe handler ignores messages without text and messages sent as a channel. Check how the bot is added to the group.
CHANNEL_DISABLEDThe channel the key is bound to is switched off. Turn it on under Integrations → Your channels.
PLAN_LIMIT_REACHEDThis month’s AI replies or tokens are used up. Upgrade or add tokens under Settings → Plan.
CUSTOMER_CONFLICTThe contacts you sent belong to different customers. Correct them in Customers; nothing is changed until you do.
reply is nullA person has taken over the conversation (status escalated), or the message only carried context. Send nothing; the team’s reply arrives through the webhook.
Nothing arrives after your team repliesCheck the webhook under Integrations → Webhook: it must be https, answer 2xx within 2.5 seconds, and verify X-Truplexy-Signature against the raw body.

Still stuck? Email hello@truplexy.com with the request_id from the error.

Next steps