Salla is the dominant e-commerce platform in Saudi Arabia and the wider Gulf region. When a client came to me needing a smart Telegram bot that could respond to customer inquiries, track orders in real time, and push personalized product recommendations — I knew I was in for an interesting build. This article walks through the architecture, the Salla API integration, and the Telegram bot logic that ties it all together.
Why Salla?
Salla provides a robust REST API and a webhook system that covers the full order lifecycle: order creation, payment confirmation, shipment updates, and delivery. It also exposes a product catalog API that you can query for recommendations. The platform is deeply embedded in Gulf e-commerce culture, and merchants there expect automation that speaks Arabic naturally.
Project Setup
The stack is intentionally minimal: Node.js 20 with TypeScript, telegraf for the Telegram bot, axios for HTTP calls to Salla, and express to receive webhooks. Here is the project skeleton:
salla-bot/
├── src/
│ ├── bot/
│ │ ├── commands.ts # /start, /order, /track
│ │ ├── scenes.ts # Conversation flows
│ │ └── middleware.ts # Auth, rate-limit
│ ├── salla/
│ │ ├── api.ts # Salla REST client
│ │ ├── webhooks.ts # Webhook handlers
│ │ └── types.ts # Salla response types
│ ├── recommendations/
│ │ └── engine.ts # Product scoring logic
│ ├── webhook-server.ts # Express app for Salla webhooks
│ └── index.ts # Entry point
├── tsconfig.json
└── package.json
Building the Salla API Client
Salla uses OAuth 2.0. Once you have your access token, every call goes through their base URL. The important thing is to cache tokens in Redis or a simple in-memory store and refresh before expiry.
// src/salla/api.ts
import axios, { AxiosInstance } from 'axios';
const BASE = 'https://api.salla.dev/admin/v2';
export class SallaClient {
private http: AxiosInstance;
constructor(private accessToken: string) {
this.http = axios.create({
baseURL: BASE,
headers: { Authorization: `Bearer ${accessToken}` },
});
}
async getOrder(orderId: string) {
const { data } = await this.http.get(`/orders/${orderId}`);
return data.data;
}
async getProducts(query?: string, limit = 10) {
const params = { per_page: limit, ...(query ? { keyword: query } : {}) };
const { data } = await this.http.get('/products', { params });
return data.data as SallaProduct[];
}
async getCustomerOrders(customerId: string) {
const { data } = await this.http.get(`/customers/${customerId}/orders`);
return data.data as SallaOrder[];
}
}
Handling Webhooks
Salla sends webhook events to a URL you register in your app dashboard. The events include order.created, order.status.updated, and order.shipped. The key rule: always respond with 200 OK within 3 seconds, then process asynchronously.
// src/webhook-server.ts
import express from 'express';
import crypto from 'crypto';
import { handleOrderCreated, handleOrderShipped } from './salla/webhooks';
const app = express();
app.use(express.json());
function verifySignature(payload: string, signature: string, secret: string) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhooks/salla', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-salla-signature'] as string;
const rawBody = req.body.toString();
if (!verifySignature(rawBody, sig, process.env.SALLA_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
const event = JSON.parse(rawBody);
setImmediate(() => routeWebhookEvent(event));
});
async function routeWebhookEvent(event: SallaWebhookEvent) {
switch (event.event) {
case 'order.created': await handleOrderCreated(event.data); break;
case 'order.shipped': await handleOrderShipped(event.data); break;
}
}
Building the Telegram Bot
I use telegraf with TypeScript. The bot uses scenes (wizard flows) to guide users through multi-step conversations, like entering an order number for tracking. The key is persisting the user's Salla customer ID alongside their Telegram chat ID so we can fetch their data without re-authentication every time.
// src/bot/commands.ts
import { Telegraf, Markup } from 'telegraf';
import { SallaClient } from '../salla/api';
import { getLinkedCustomer } from '../store';
export function registerCommands(bot: Telegraf) {
bot.command('start', ctx => {
ctx.reply(
'Welcome! I can help you track your Salla orders and discover products.\n\n' +
'Use /track to check an order or /recommend to get product suggestions.',
Markup.keyboard([['/track', '/recommend'], ['/orders', '/help']])
.resize()
.oneTime()
);
});
bot.command('track', async ctx => {
await ctx.reply('Please enter your order number:');
ctx.scene.enter('ORDER_TRACK_SCENE');
});
bot.command('orders', async ctx => {
const customer = await getLinkedCustomer(ctx.from!.id);
if (!customer) {
return ctx.reply('Link your account first with /link');
}
const salla = new SallaClient(customer.accessToken);
const orders = await salla.getCustomerOrders(customer.sallaId);
if (!orders.length) return ctx.reply('No orders found.');
const msg = orders.slice(0, 5).map(o =>
`Order #${o.reference_id} — ${o.status.name} — ${o.amounts.total.amount} ${o.currency}`
).join('\n');
ctx.reply(msg);
});
}
Real-Time Order Tracking
When Salla fires an order.status.updated webhook, the handler looks up which Telegram user is linked to that order and sends them a proactive update. This is the core loop that makes the bot feel genuinely intelligent — customers get push notifications without polling.
// src/salla/webhooks.ts
import { bot } from '../bot';
import { getLinkedChatId } from '../store';
export async function handleOrderShipped(order: SallaOrder) {
const chatId = await getLinkedChatId(order.customer.id);
if (!chatId) return;
await bot.telegram.sendMessage(chatId,
`Your order #${order.reference_id} has been shipped!\n` +
`Estimated delivery: ${order.shipments?.[0]?.estimated_delivery ?? 'N/A'}`
);
}
Product Recommendation Engine
The recommendation logic is intentionally simple — no ML model. It scores products based on three signals: category match with the customer's purchase history, price proximity to their average order value, and recency (newer products score higher). This works well for small-to-medium Salla stores and avoids the cold-start problem.
// src/recommendations/engine.ts
export function scoreProduct(product: SallaProduct, profile: CustomerProfile): number {
let score = 0;
// Category match
if (profile.topCategories.includes(product.category.id)) score += 40;
// Price proximity (within 20% of avg order value)
const priceDiff = Math.abs(product.price - profile.avgOrderValue);
if (priceDiff / profile.avgOrderValue < 0.2) score += 30;
// Recency — products added in last 30 days
const ageDays = (Date.now() - new Date(product.created_at).getTime()) / 86400000;
if (ageDays < 30) score += 20;
// Stock availability
if (product.quantity > 0) score += 10;
return score;
}
export function getTopRecommendations(
products: SallaProduct[],
profile: CustomerProfile,
limit = 5
): SallaProduct[] {
return products
.map(p => ({ product: p, score: scoreProduct(p, profile) }))
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(r => r.product);
}
Deployment Notes
I deploy this on a small VPS using PM2 with a Nginx reverse proxy. The webhook endpoint must be HTTPS, which means a valid SSL cert — I use Let's Encrypt via Certbot. One subtle issue: Salla requires your webhook URL to return HTTP 200 within 3 seconds, so never do synchronous database writes in the handler. Push everything to a queue or use setImmediate.
The bot runs 24/7 using long polling in development and webhooks in production. Telegraf handles both modes with a single config switch.
Lessons Learned
The biggest challenge was handling Arabic product names correctly. Some Salla merchants store product data with mixed LTR/RTL content, so Telegram's message rendering can look odd. The fix is to always wrap Arabic text in RTL Unicode marks (\u202B) before sending to Telegram.
Another lesson: always cache Salla API responses with a short TTL (60–120 seconds). The API has rate limits, and during a high-traffic flash sale the bot can receive dozens of tracking requests per minute from a single customer.
Overall, building on Salla's platform is a genuinely pleasant experience — their documentation is clear, the webhook events are granular, and the OAuth flow is standard. If you are building automation for Gulf e-commerce, Salla is the right starting point.