April 29, 20268 min read

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 Match35%Compatibility matrix lookup (e.g. Warm-Neutral: 0.6, Warm-Cool: 0.1) against product dominant color family
CLIP Style Similarity25%Cosine similarity between customer's 512-dim style centroid and product's CLIP embedding
Embroidery Preference20%Density match (+0.5) + specific embroidery type name match (+0.5)
Occasion Affinity12%Per-occasion score from the customer's affinity map
Price Band FitHard GateCustomer must be within ±1 price band of the product — prevents luxury spam to budget customers
campaign.controller.js — Composite Score Calculation
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) + createdAt timestamp.

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:

campaign.controller.js — Scheduled Queue Dispatch
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:

whatsappDirectService.js — Meta Cloud API Template Payload
{
  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 whatsappLogs records 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.

Share this story