Trackily Docs

Webhooks (entrants)

Trackily reçoit des webhooks de plusieurs sources externes : Shopify (orders, products), WooCommerce, Stripe (paiements e-commerce + paiements natifs par compte), PayPal (par compte), et les MTA (Mailgun, SES, Postmark) pour les bounces / complaints. Chaque endpoint vérifie une signature HMAC pour rejeter les requêtes non authentifiées.

Note importante : Trackily n'émet pas de webhooks sortants génériques. Si tu cherches à propager un événement Trackily (paiement reçu, conversion enregistrée, lead capturé) vers un système externe, regarde plutôt : conversion triggers email (/docs/email/automations), postbacks vers ad networks (/docs/api/postback), ou script tags injection (Shopify uniquement).

Liste des endpoints

Endpoint Source Auth
POST /webhook/shopify Shopify Admin → Notifications HMAC-SHA256 header X-Shopify-Hmac-SHA256
POST /webhook/woocommerce WooCommerce → Settings → Advanced → Webhooks HMAC-SHA256 header X-WC-Webhook-Signature
POST /webhook/stripe-ecom Stripe (legacy — e-commerce externe) Signature header Stripe-Signature
POST /webhook/stripe/:account_id Stripe (par payment_account natif) Signature header Stripe-Signature
POST /webhook/paypal/:account_id PayPal (par payment_account natif) PayPal webhook verification + secret

Les webhooks MTA (Mailgun, SES, Postmark) pour bounces / complaints sont également exposés mais documentés dans leur intégration spécifique.

Pattern global — raw body capture

Pour vérifier les signatures HMAC, on a besoin du body brut (avant JSON parsing). Express body-parser détruit cette possibilité par défaut. Trackily contourne via un middleware global :

// Capture raw body for all /webhook/* routes
app.use('/webhook', express.raw({ type: '*/*', limit: '2mb' }), (req, res, next) => {
  req.rawBody = req.body;  // Buffer
  // Best-effort JSON parse — handler choose to use parsed or raw
  try { req.body = JSON.parse(req.rawBody.toString()); } catch {}
  next();
});

Conséquence : dans chaque handler, req.rawBody est le Buffer original (utilisé pour le HMAC), req.body est le JSON parsé (pratique pour la business logic).

POST /webhook/shopify

Setup côté Shopify

  1. Shopify Admin → Settings → Notifications → Webhooks → "Create webhook"
  2. Event : choisir parmi orders/create, orders/paid, orders/cancelled, orders/fulfilled, etc.
  3. Format : JSON
  4. URL : https://trackily.online/webhook/shopify
  5. Webhook API version : v2023-10 ou plus récent

Setup côté Trackily

Settings → Integrations → Shopify webhook secret. Coller le secret HMAC fourni par Shopify (stocké chiffré).

Alternative legacy : ?key=... query param + shopify_webhook_key setting. Plus faible (leak via access logs) mais accepté pour compat.

Comportement

Le handler :

  1. Vérifie HMAC (sec.shopifyVerifySignature(secret, rawBody, sigHeader)). Si KO → 401 Unauthorized.
  2. Parse le payload (order Shopify).
  3. Extrait le click_id Trackily depuis l'une de 4 sources, dans l'ordre :
    • note_attributes (recommandé — _tly_click)
    • landing_site URL params (_tly_click, click_id, ref)
    • referring_site URL params
    • tags (regex extract)
  4. Si click_id trouvé : matching avec la clicks row, création d'une conversion row avec le revenue de l'order.
  5. Si pas de click_id : log + skip (l'order est enregistrée côté Shopify, juste pas attribuée).

Code de retour : 200 OK même si pas de matching (sinon Shopify retry 19 fois et tu pollues les logs).

Tracking côté checkout

Pour que _tly_click se retrouve dans note_attributes, injecte un script tag Shopify (cf. tool MCP create_script_tag) qui :

// /trackily-tag.js
(function() {
  var click = new URLSearchParams(location.search).get('_tly_click')
            || document.cookie.match(/_tly_click=([^;]+)/)?.[1];
  if (click && window.Shopify && window.Shopify.checkout) {
    // Set as cart attribute
    fetch('/cart/update.js', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ attributes: { _tly_click: click } })
    });
  }
})();

POST /webhook/woocommerce

Setup côté WooCommerce

  1. WP Admin → WooCommerce → Settings → Advanced → Webhooks → Add webhook
  2. Topic : Order created, Order updated, etc.
  3. Delivery URL : https://trackily.online/webhook/woocommerce
  4. Secret : générer un secret, copier dans Settings → Integrations → WooCommerce webhook secret côté Trackily

Comportement

Vérifie X-WC-Webhook-Signature (HMAC-SHA256 base64 du body brut avec le secret). Sinon idem Shopify : extract click_id depuis customer_note / meta_data, create conversion.

POST /webhook/stripe-ecom

Webhook Stripe global pour le e-commerce externe (legacy — quand Trackily orchestrait des paiements Stripe sans système payment_accounts par compte). Vérifie Stripe-Signature avec stripe_ecom_webhook_secret.

Évite ce path — préfère /webhook/stripe/:account_id pour le commerce natif.

POST /webhook/stripe/:account_id

Webhook Stripe par payment_account. Chaque payment_account natif a son propre webhook secret (stocké dans payment_accounts.webhook_secret, chiffré). L'URL inclut l'account_id pour identifier quel secret utiliser.

Setup

  1. Création du payment_account dans Trackily (Settings → Commerce → Payment methods). Récupérer son account_id.
  2. Stripe Dashboard → Developers → Webhooks → Add endpoint
  3. URL : https://trackily.online/webhook/stripe/<account_id>
  4. Events : payment_intent.succeeded, payment_intent.payment_failed, charge.refunded, customer.subscription.created, etc.
  5. Copier le whsec_... signing secret dans Trackily UI (champ webhook_secret du payment_account).

Comportement

  1. Lit le payment_account par account_id (déchiffre webhook_secret automatiquement via CIPHER_TABLES).
  2. Vérifie signature Stripe-Signature via Stripe.webhooks.constructEvent(rawBody, sig, webhook_secret).
  3. Switch sur event.type :
    • payment_intent.succeeded → mark order paid, déclenche post-purchase email automation
    • payment_intent.payment_failed → mark order failed
    • charge.refunded → record refund
    • customer.subscription.created/updated/deleted → update native_subscriptions row
  4. Code de retour : 200 OK (Stripe retry sinon).

Auto-création de webhook au moment du checkout

Quand un buyer paye via Stripe pour la première fois sur une instance fresh, Trackily peut auto-créer le webhook (extrait de server.js) :

const webhookUrl = `${req.protocol}://${req.get('host')}/webhook/stripe/native-pending`;
const webhookCreate = await fetch('https://api.stripe.com/v1/webhook_endpoints', { ... });

C'est best-effort — si tu préfères le contrôle explicite, désactive et configure manuellement.

POST /webhook/paypal/:account_id

Webhook PayPal par payment_account. URL : https://trackily.online/webhook/paypal/<account_id>.

Setup

PayPal Developer Dashboard → My Apps & Credentials → ton App → Add webhook :

  • URL : https://trackily.online/webhook/paypal/<account_id>
  • Events : PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED, BILLING.SUBSCRIPTION.CREATED, etc.

Le webhook_id retourné par PayPal va dans payment_accounts.webhook_secret (overloaded : pour PayPal, ce champ stocke le webhook_id qui sert à la vérification).

Vérification

Pas une simple HMAC : PayPal demande de POST une request de vérification vers leur API avec les headers reçus + le body. Plus lourd, mais c'est leur convention. Implémenté via paypal-helpers.js.

Pattern de retry

Source Retry policy
Shopify 19 retries sur 48h, exponential backoff
WooCommerce configurable côté WP, défaut ~3 retries
Stripe 3 jours, ~10 retries, exponential backoff
PayPal 25 retries sur 3 jours

Si ton handler crash ou retourne 5xx, attends-toi à recevoir le même payload N fois. L'idempotence est de ta responsabilité :

  • Côté Trackily, on dédup via la combo (external_event_id, source) quand disponible.
  • Pour les events sans ID stable (rare), on dédup par (order_external_id, event_type) en assumant qu'un même event ne fire qu'une fois dans une fenêtre courte.

Debugging

Réception sans matching

Si un webhook arrive mais aucune conversion n'est créée :

  1. Vérifie côté logs [Shopify] Webhook received qu'il est bien reçu.
  2. Si pas reçu : firewall / DNS / signature secret erroné.
  3. Si reçu mais "No click_id found" : ton tracking côté checkout n'injecte pas _tly_click dans les note_attributes / landing_site. Vérifie le script tag.

Signature failure

[Shopify] HMAC verification failed

Causes habituelles :

  • Secret recopié avec espaces ou retour à la ligne en trop
  • Tu utilises l'API version où Shopify a changé le format de signature (rare, mais arrive)
  • Un proxy en amont (Nginx, Cloudflare) modifie le body en transit (mauvaise config buffer)

Mitigations : strict timing-safe compare déjà en place, copier-coller le secret depuis Shopify directement (pas via un mot de passe manager qui ajoute des chars invisibles).

Volume

Si tu reçois >100 webhooks/seconde (gros store Shopify avec orders/* + products/* + customers/*), le rate limit de Node Express peut saturer. Mitigations :

  • Désactive les events que tu n'utilises pas (côté Shopify)
  • Augmente le rate limit nginx en amont
  • Considère un worker queue dédié (out of scope pour Trackily 1.0)

Voir aussi