Engineering Conversions: Building a Curated WhatsApp Recommendation Engine for Boutique Sellers
Email marketing is dead for local boutique stores; open rates are under 15%. WhatsApp, on the other hand, has open rates exceeding 90%. But there is a catch — if you blast generic catalog links to all your contacts, users will block you instantly. At BoutiqueAI, we built a fully automated, personalized WhatsApp campaign engine that intelligently matches new arrivals to customers' individual style preferences, applies a 5-factor relevance scoring algorithm, generates personalized product collages, and dispatches each message via the Meta WhatsApp Business Cloud API. This is a deep dive into exactly how we engineered it.
The Problem: Spray-and-Pray vs. Curated Reach
Our boutique sellers were already using WhatsApp manually — forwarding the same product image to every contact, getting blocked, and watching their reach erode. The core issue was zero personalization. A customer who buys budget cotton casuals has no interest in heavy luxury embroidery. A customer who last bought festive lehengas will ignore linen office wear. We needed a system that could automatically decide which products are right for which customers, assemble a personalized product collage, and fire a templated WhatsApp message — all at scale, without the seller lifting a finger after clicking "Schedule Campaign".
Step 1: The Customer Preference Profile
Every time a customer makes a purchase in a boutique, our system enriches their profile using the AI-extracted tags of the product they bought. We maintain these fields on each customer record:
- color_family_pref: Dominant color family (Warm, Cool, Neutral, Jewel) inferred from past purchase palette data.
- style_centroid: A 512-dimension CLIP vector — a rolling average across all their purchased products' embeddings — encoding their visual taste as a point in embedding space.
- embroidery_density_pref: Whether they lean towards plain, medium, or heavily embroidered items.
- embroidery_type_pref: An array of specific embroidery names (e.g., "zari", "mirror work", "block print") they've bought.
- occasion_affinity: A JSON map of occasion names to affinity scores, e.g.,
{"wedding": 0.8, "casual": 0.2}, updated after every sale. - price_band: Classified as budget, mid, premium, or luxury based on their spending history.
Step 2: The 5-Factor Composite Relevance Score
When a boutique owner selects a product pool for a campaign, our previewCampaignAudience endpoint scores every customer against every boutique-wide product. Each gets a composite score from 0 to 1 built from five weighted signals:
| Factor | Weight | Signal Used |
|---|---|---|
| Color Family Match | 35% | Compatibility matrix lookup (e.g. Warm-Neutral: 0.6, Warm-Cool: 0.1) against product dominant color family |
| CLIP Style Similarity | 25% | Cosine similarity between customer's 512-dim style centroid and product's CLIP embedding |
| Embroidery Preference | 20% | Density match (+0.5) + specific embroidery type name match (+0.5) |
| Occasion Affinity | 12% | Per-occasion score from the customer's affinity map |
| Price Band Fit | Hard Gate | Customer must be within ±1 price band of the product — prevents luxury spam to budget customers |
const colorScore = (COLOR_FAMILY_SCORING[customer.color_family_pref] || {})[product.dominant_color_family] || 0.1;
score += 0.35 * colorScore;
const clipSim = calculateCosineSimilarity(customer.style_centroid, product.clip_embedding);
score += 0.25 * clipSim;
let embroideryMatch = 0;
if (customer.embroidery_density_pref === product.embroidery_density) embroideryMatch += 0.5;
if (productEmbroideryName && customer.embroidery_type_pref?.some(p => p === productEmbroideryName)) embroideryMatch += 0.5;
score += 0.20 * embroideryMatch;
const affinity = (customer.occasion_affinity || {})[productOccasionName] || 0.1;
score += 0.12 * affinity;
// Hard gate: customers scoring below 0.40 are excluded from any messaging
if (bestScore < 0.40) { exclusions.lowScore++; continue; }
Step 3: Hybrid Product Slot Assignment
Every customer who clears the threshold gets a custom-ordered product list — not just the single top product. We fill three distinct slots:
- Slots 1–3 (Preference Matches): Strictly from the boutique owner's selected campaign pool, sorted by AI score descending, minimum score 0.40.
- Slots 4–5 (Trending): Boutique-wide products not yet picked, sorted by
(isInPool ? 1_000_000 : 0) + sales_count— so pool items always outrank generic trending items. - Slot 6 (Fresh Arrival): The newest product not yet assigned, pool-prioritized via
(isInPool ? 1e16 : 0) + createdAttimestamp.
This ordered list is persisted as product_ids_ordered in a catalogLinks table — one unique row per customer per campaign, each with a cryptographically generated 12-character hex slug for their personalized shop URL.
Step 4: BullMQ Scheduling with Redis
Sellers can send immediately or schedule for any future time. After saving the campaign and catalog links in a single database transaction, we push a delayed job to a BullMQ queue backed by Redis:
const delay = Math.max(0, new Date(scheduled_at) - new Date());
await campaignQueue.add("process-campaign", { campaignId: result.id }, {
delay,
jobId: `campaign-${result.id}` // Idempotent key — prevents duplicate dispatches
});
Step 5: Personalized Collage + Meta Cloud API Dispatch
When the BullMQ worker fires, it reads each customer's product_ids_ordered list, fetches image URLs, and calls a collage generation service that composites the images into a grid and uploads it to a cloud storage service. The resulting CDN URL becomes the header image in the WhatsApp template.
Messages are sent via Meta WhatsApp Cloud API v23.0 using the pre-approved customer_purchase_notification template with three dynamic body variables — customer_name, boutique_name, and price (pulled from the customer's last recorded purchase) — plus a URL button pointing to the boutique's storefront slug:
{
template: {
name: "customer_purchase_notification",
language: { code: "en" },
components: [
{ type: "header",
parameters: [{ type: "image", image: { link: combinedImageUrl } }] },
{ type: "body",
parameters: [
{ type: "text", parameter_name: "customer_name", text: customerName },
{ type: "text", parameter_name: "boutique_name", text: boutiqueName },
{ type: "text", parameter_name: "price", text: lastPurchaseAmount }
]
},
{ type: "button", sub_type: "url", index: "0",
parameters: [{ type: "text", text: boutiqueSlug }] }
]
}
}
Each boutique can register their own WhatsApp Business Account (WABA). At dispatch time, we dynamically resolve the per-boutique phoneNumberId and encrypted accessToken from a registered credentials store, falling back to our platform's shared credentials when none is registered. This multi-tenant model lets a single campaign worker serve hundreds of boutiques, each sending messages from their own verified business number.
Step 6: Wallet Tokens, Cooldowns & Delivery Tracking
- Pre-flight balance check: Campaign creation is rejected immediately if the boutique's message token balance is less than the number of selected recipients.
- Provisional wallet deduction: Each successful dispatch deducts one message token from the boutique's balance, linked to its message ID and campaign ID for full auditability.
- 5-day cooldown gate: Customers contacted within the last 5 days are automatically excluded at scoring time, preventing message fatigue.
- Chunked parallel dispatch: Recipients are processed 3 at a time concurrently, balancing throughput without triggering Meta rate limits.
- Webhook delivery tracking: Meta pushes status callbacks (sent → delivered → read) to our webhook endpoint. We update
whatsappLogsrecords in real time so sellers can monitor per-recipient delivery, open, and failure counts live on their dashboard.
Results: Engineering That Converts
By replacing generic blasts with a five-signal AI scoring pipeline, personalized product collage headers, and multi-tenant Meta API dispatch, BoutiqueAI turned WhatsApp from a chat tool into a precision marketing channel. Sellers previously seeing 3–4% click-through on bulk forwards now see CTRs above 25%. The right customers see the right products — and that is the only kind of marketing that actually works.
Thanks for reading.