Email Automations
Les automations relient le reste de Trackily (landings, postbacks, orders, abandoned carts) à l'enrollment dans une liste email. C'est ici que tu transformes ton tracker en machine à drips : chaque conversion devient un trigger qui démarre une sequence sans qu'aucun humain ne touche au clavier.
Concept
Trackily offre quatre points d'enrollment automatique, complémentaires :
- Landing → liste — chaque soumission de formulaire d'une landing enroll l'email dans une liste donnée.
- Conversion trigger — chaque postback affiliate match une offre, fait remonter l'email du buyer et l'enroll dans une liste.
- Post-purchase — chaque commande native payée (paiement capturé) enroll l'acheteur dans une liste de cross-sell / thank-you.
- Abandoned cart — chaque commande qui reste en
pendingau-delà d'un seuil enroll l'acheteur dans une liste de relance.
Chaque automation alimente le même mécanisme côté liste : email_subscribers.status='subscribed' + déclenchement de la default_sequence de la liste. Pas de chemin séparé, pas de double moteur. La sequence reste l'unité atomique.
1. Landing → liste (binding direct)
Setup
{
"name": "bind_landing_to_email_list",
"arguments": {
"landing_id": 42,
"list_id": 7
}
}
Ce que fait le tool :
UPDATE landing_pages SET email_list_id = 7 WHERE id = 42- Renvoie la liste + la landing pour confirmation
- À partir de maintenant, chaque submit du form de la landing 42 (POST
/api/lead) déclenche unenrollSubscriber(list_id=7, email, name, custom_fields)après création de laleadsrow.
Pour unbind : bind_landing_to_email_list({ landing_id: 42, list_id: null }).
Custom fields passés au subscriber
Le form de la landing peut avoir des champs au-delà de email et name. Tout champ non-réservé est sérialisé dans email_subscribers.custom_fields JSONB :
// Form HTML
// <input name="email" />
// <input name="name" />
// <input name="city" />
// <input name="age_range" />
// Subscriber créé
{
"email": "marie@example.com",
"name": "Marie",
"custom_fields": {
"city": "Paris",
"age_range": "25-34"
}
}
// Dans la sequence
// Subject: "{{first_name}} de {{custom.city}}, ton offre est prête"
// → "Marie de Paris, ton offre est prête"
Cas limites
- Email déjà subscribed sur la liste → no-op (idempotent), pas de re-trigger de la sequence (évite les boucles).
- Email suppressed → silently skipped (cf. Suppression).
- Liste avec double opt-in → subscriber créé en
status='pending_confirm', email de confirmation envoyé, sequence en pause tant que pas confirmé.
2. Conversion trigger (postback → enrollment)
Concept
C'est le superpouvoir affiliate-spécifique de Trackily. Quand un postback /postback?subid=…&payout=…&offer_id=… arrive d'un réseau (Everflow, MaxBounty, custom), Trackily :
- Crée la
conversionrow (déjà fait par le handler/postback). - Cherche tous les
email_conversion_triggersactifs qui matchent l'offer_id(ou les triggers sansoffer_id= "any offer"). - Récupère l'email du buyer — trois sources, dans l'ordre :
?email=query param sur le postback (certains réseaux le passent)leads.emailjoiné parclick_id(le subscriber avait rempli un prelander avant d'acheter)clicks.lead_data->>'email'(extra field captured au lead time)
- Pour chaque trigger qui match et a un email :
enrollSubscriber(target_list_id, email, …, source='trigger:postback-offer-<id>'). - La
default_sequencede latarget_list_idse déclenche → thank-you / cross-sell drip.
Modèle de données
Schema email_conversion_triggers :
| Colonne | Type | Note |
|---|---|---|
id |
SERIAL PK | |
name |
TEXT | label |
offer_id |
INTEGER FK NULL | match cette offre, ou n'importe quelle offre si NULL |
target_list_id |
INTEGER FK NOT NULL | liste où enroller |
filters |
JSONB | { payout_min, payout_max, statuses: ["approved", "pending"] } |
is_active |
BOOLEAN | |
triggered_count |
INTEGER | nombre d'enrollments via ce trigger |
last_triggered_at |
TIMESTAMPTZ | dernier fire |
deleted_at |
TIMESTAMPTZ | soft-delete |
Setup
// Create the trigger
{
"name": "create_email_conversion_trigger",
"arguments": {
"name": "Sweepstakes FR — post-purchase",
"target_list_id": 7,
"offer_id": 12,
"filters": {
"payout_min": 5,
"statuses": ["approved"]
},
"is_active": true
}
}
// Response
{
"status": "ok",
"trigger": {
"id": 4,
"name": "Sweepstakes FR — post-purchase",
"target_list_id": 7,
"offer_id": 12,
"filters": { "payout_min": 5, "statuses": ["approved"] },
"is_active": true,
"triggered_count": 0
}
}
À partir de maintenant, chaque postback ?offer_id=12&status=approved&payout=8.50&email=… enrolle le buyer dans la liste 7. Si payout < 5 ou status != approved, le trigger ne fire pas.
Lister les triggers
{ "name": "list_email_conversion_triggers", "arguments": { "list_id": 7 } }
Désactiver / supprimer
update_email_conversion_trigger({ id: 4, is_active: false })— pause sans suppressiondelete_email_conversion_trigger({ id: 4 })— soft-delete, les enrollments passés restent
Trigger "any offer"
Omets offer_id (ou passe null) pour matcher toutes les offres. Utile pour une liste "Buyers" globale qui collecte tous tes acheteurs, peu importe le produit. À combiner avec filters.payout_min pour ne pas polluer avec les pending / faible payout.
3. Post-purchase (commerce native, Phase 6)
Concept
Pour les commandes natives (Trackily commerce, pas Shopify), tu peux brancher une liste qui se déclenche au moment où le paiement est capturé (orders.payment_status='paid'). Le buyer est enrôlé dans la liste, la default_sequence part — typiquement un thank-you Day 0, un cross-sell Day 3, un review request Day 7.
Setup — deux niveaux
Niveau global (settings) :
INSERT INTO settings (key, value)
VALUES
('commerce_default_post_purchase_list_id', '"7"'),
('commerce_default_abandoned_cart_list_id', '"8"');
Ou via l'UI Settings → Commerce → Email automations.
Niveau campagne (override par campagne) :
campaigns.post_purchase_list_id (FK → email_lists.id). Si non-NULL, écrase le default global pour les orders attribuées à cette campagne.
Logique côté server (extrait de server.js) :
const campaignCol = kind === 'post_purchase'
? 'post_purchase_list_id'
: 'abandoned_cart_list_id';
const settingKey = `commerce_default_${kind}_list_id`;
// 1. campaign-specific
// 2. global default
// 3. null → no enrollment
Idempotence
Pour l'abandoned cart, orders.abandoned_cart_emailed_at flag empêche un re-enrollment si le cron tourne plusieurs fois sur la même commande pending.
Pour le post-purchase, l'idempotence vient de email_subscribers.UNIQUE(list_id, lower(email)) — si l'email existe déjà sur la liste, l'enrollment est un no-op.
4. Abandoned cart
Concept
Un cart abandonné, c'est une order avec payment_status='pending' qui dure plus que N heures (configurable). Un cron périodique (abandonedCartCron dans server.js) :
- Parcourt les orders pending entre 1h et 48h après création (fenêtre paramétrable).
- Pour chaque order, résout
campaigns.abandoned_cart_list_id→ fallbackcommerce_default_abandoned_cart_list_idsetting. - Si liste résolue ET
orders.abandoned_cart_emailed_at IS NULLET email du buyer non-suppressed → enroll + setabandoned_cart_emailed_at = NOW().
Sequence type
Day 0 (5min après création de l'order) — "Tu as oublié ton panier"
Day 1 — "Encore disponible, mais plus pour longtemps"
Day 3 — "Dernière chance : -10% si tu finalises maintenant" (avec code discount injecté via custom_fields)
Custom fields envoyés au subscriber :
cart_total— montant du paniercart_url— lien magique pour reprendre le checkoutproduct_name— premier produit du cart (pour personnalisation)
Mettre tout ensemble
Voici un setup réel pour un dropshipper sweepstakes :
// 1. Two lists
{ "name": "create_email_list", "arguments": { "name": "Sweeps Buyers", "slug": "sweeps-buyers", "smtp_server_id": 1 } }
// → list_id = 7
{ "name": "create_email_list", "arguments": { "name": "Sweeps Cart Recovery", "slug": "sweeps-cart-recovery", "smtp_server_id": 1 } }
// → list_id = 8
// 2. Default sequences on each list (create_email_sequence + add_email_sequence_step ×3)
// ... (cf. [Sequences])
// 3. Landing → list (everyone who fills the prelander form lands in "Sweeps Buyers")
{ "name": "bind_landing_to_email_list", "arguments": { "landing_id": 42, "list_id": 7 } }
// 4. Conversion trigger (paid sweepstakes conversions → "Sweeps Buyers")
{ "name": "create_email_conversion_trigger", "arguments": {
"name": "Sweeps offers — buyers", "target_list_id": 7, "offer_id": null,
"filters": { "payout_min": 3, "statuses": ["approved"] }
}}
// 5. Settings (global defaults)
// commerce_default_post_purchase_list_id = 7
// commerce_default_abandoned_cart_list_id = 8
// 6. Per-campaign override (optional)
// UPDATE campaigns SET abandoned_cart_list_id = 8 WHERE id = 12;
Désormais, chaque lead capturé par la landing 42 entre dans "Sweeps Buyers". Chaque conversion confirmée via le postback affiliate aussi (avec dédup naturelle). Chaque cart abandonné entre dans "Sweeps Cart Recovery". Et chaque order payée native est enrollée dans "Sweeps Buyers" (qui peut être la même liste que celle des leads — la dédup fait le reste).
Erreurs courantes
- Landing→liste configuré mais aucun enrollment — vérifie que le form de la landing pose bien
name="email"(pasname="Email"ouname="user-email"). Le handler/api/leadcherchereq.body.emailstrictement. - Conversion trigger défini mais 0 triggered — l'email du buyer n'est pas remontable. Soit le postback ne passe pas
?email=, soit aucunleadsrow n'aclick_id = <subid>. Vérifie en consultantlist_clickset la jointure. - Post-purchase + landing→liste : deux mails de bienvenue — normal si la même liste sert pour les leads ET les acheteurs. Pour deux flows distincts, utilise deux listes (et donc deux sequences).
- Abandoned cart envoyé même après paiement — race condition entre le cron et le webhook Stripe. Le
abandoned_cart_emailed_atflag protège du re-send, mais pas du first-send si l'envoi a précédé le webhook. Augmente le délai du cron à 1h pour donner du slack.
Voir aussi
- Lists — où atterrissent les enrollments
- Sequences — ce qui se déclenche après
- Suppression — pourquoi un enrollment peut être silencieux
- API — Postback — détails du handler
/postbackqui fire les triggers - MCP — tools reference — toutes les ops automations en un endroit