Trackily Docs

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 :

  1. Landing → liste — chaque soumission de formulaire d'une landing enroll l'email dans une liste donnée.
  2. Conversion trigger — chaque postback affiliate match une offre, fait remonter l'email du buyer et l'enroll dans une liste.
  3. Post-purchase — chaque commande native payée (paiement capturé) enroll l'acheteur dans une liste de cross-sell / thank-you.
  4. Abandoned cart — chaque commande qui reste en pending au-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 :

  1. UPDATE landing_pages SET email_list_id = 7 WHERE id = 42
  2. Renvoie la liste + la landing pour confirmation
  3. À partir de maintenant, chaque submit du form de la landing 42 (POST /api/lead) déclenche un enrollSubscriber(list_id=7, email, name, custom_fields) après création de la leads row.

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 :

  1. Crée la conversion row (déjà fait par le handler /postback).
  2. Cherche tous les email_conversion_triggers actifs qui matchent l'offer_id (ou les triggers sans offer_id = "any offer").
  3. Récupère l'email du buyer — trois sources, dans l'ordre :
    • ?email= query param sur le postback (certains réseaux le passent)
    • leads.email joiné par click_id (le subscriber avait rempli un prelander avant d'acheter)
    • clicks.lead_data->>'email' (extra field captured au lead time)
  4. Pour chaque trigger qui match et a un email : enrollSubscriber(target_list_id, email, …, source='trigger:postback-offer-<id>').
  5. La default_sequence de la target_list_id se 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 suppression
  • delete_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) :

  1. Parcourt les orders pending entre 1h et 48h après création (fenêtre paramétrable).
  2. Pour chaque order, résout campaigns.abandoned_cart_list_id → fallback commerce_default_abandoned_cart_list_id setting.
  3. Si liste résolue ET orders.abandoned_cart_emailed_at IS NULL ET email du buyer non-suppressed → enroll + set abandoned_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 panier
  • cart_url — lien magique pour reprendre le checkout
  • product_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" (pas name="Email" ou name="user-email"). Le handler /api/lead cherche req.body.email strictement.
  • Conversion trigger défini mais 0 triggered — l'email du buyer n'est pas remontable. Soit le postback ne passe pas ?email=, soit aucun leads row n'a click_id = <subid>. Vérifie en consultant list_clicks et 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_at flag 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