Functions de type hook

Écrivez une function qui s'exécute au sein d'une opération OmniLab et détermine si celle-ci se poursuit, puis liez-la à un point d'accroche.

8 min de lecture

Une function de type hook s'exécute pendant une opération, pas après. OmniLab suspend l'opération, appelle votre code et applique votre réponse : poursuivre, éventuellement avec des valeurs que vous fournissez, ou refuser. C'est pourquoi une function de type hook demande plus de soin qu'une function d'événement de plateforme.

Quand choisir une function de type hook

Choisissez une function de type hook quand le résultat d'une opération doit dépendre de votre propre logique. Par exemple : bloquer les inscriptions venant de domaines d'email jetables, normaliser des données avant leur stockage, ou envoyer l'email de vérification via votre propre fournisseur. Pour réagir à quelque chose qui s'est déjà produit, utilisez plutôt une function d'événement de plateforme.

Les deux points d'accroche

Une function de type hook est créée pour un seul point d'accroche et ne peut être liée qu'à celui-là.

Point d'accrocheS'exécute quandVotre exportcontinue vous permet de
contact.create.preAvant qu'un contact soit stocké, qu'il provienne d'une inscription, d'un administrateur, d'un import ou de l'APIonContactCreatePreRéécrire l'email, le numéro de téléphone et les champs personnalisés
contact.verifyUne inscription en double opt-in doit recevoir son email de vérificationonContactVerifyTransmettre l'identifiant du message attribué par votre fournisseur, ou signaler que l'adresse est déjà vérifiée

Ce que garantit un hook

Quandcontact.create.precontact.verify
Aucune liaison active ne couvre l'organisationLe contact est stocké tel que soumisL'inscription échoue, et les campagnes de cette organisation ne peuvent pas être publiées tant que le double opt-in est activé
Votre gestionnaire retourne continueLes champs retournés sont appliqués, puis le contact est stockéLe visiteur attend le lien que vous avez envoyé. Avec already_verified: true, le contact est directement créé comme vérifié
Votre gestionnaire retourne denyLe contact est refusé avec votre code, ou DENIED si le code est videL'inscription échoue
Votre gestionnaire lève une exception, dépasse une limite ou ne respecte pas le contrat, ou la function n'a jamais été compiléeLe contact est refuséL'inscription échoue
Nouvelles tentativesAucune : l'opération attend votre réponseAucune
Pour annuler un gestionnaire défaillantDésactivez la liaison : les contacts sont de nouveau stockés tels que soumisEnregistrez et compilez la dernière version fonctionnelle de votre code. Ne désactivez pas la liaison tant que le double opt-in est activé

Un refus est une exécution normale et réussie. La quatrième ligne est un échec, et en cas d'échec, un hook bloque l'opération : OmniLab la refuse plutôt que de la laisser passer sans contrôle.

Construisez votre réponse avec OmniLab.Flow.continue(result?) ou OmniLab.Flow.deny(code, message). Un simple objet de même structure convient aussi : le contrat des hooks précise ce qui est accepté.

contact.create.pre

Votre fichier exporte onContactCreatePre.

user.ts — hello world
import { OmniLab } from "./omnilab";
import type { ContactCreatePreHandler } from "./omnilab";

export const onContactCreatePre: ContactCreatePreHandler = (input) => {
  console.log("Hello world! A contact is being created: " + input.contact.email);
  return OmniLab.Flow.continue();
};

Cette version laisse passer tous les contacts sans les modifier : c'est la bonne première version. Liez-la, observez les exécutions qui apparaissent et vérifiez que les inscriptions fonctionnent toujours. Ensuite seulement, ajoutez une logique capable de refuser.

Refuser et réécrire

continue() sans argument ne change rien. Si vous passez un objet, OmniLab n'applique que les champs que vous retournez : un champ omis reste exactement tel qu'il a été soumis.

user.ts — normalise the email, block a domain
import { OmniLab } from "./omnilab";
import type { ContactCreatePreHandler } from "./omnilab";

export const onContactCreatePre: ContactCreatePreHandler = (input) => {
  const email = input.contact.email.trim().toLowerCase();

  if (email.endsWith("@blocked.example")) {
    return OmniLab.Flow.deny("blocked_domain", "Email domain is on the blocklist");
  }

  // Seul `email` est retourné : le numéro de téléphone et les champs personnalisés sont stockés tels que soumis.
  return OmniLab.Flow.continue({ email });
};

Seul le code de votre refus est renvoyé à l'appelant. Le message reste dans le journal d'exécution : rédigez-le pour la personne qui lit le journal, pas pour le visiteur.

Normaliser à ce stade est payant, car le hook s'exécute avant le stockage du contact et avant la mise à jour de tout CRM connecté. L'email que vous retournez est celui qu'OmniLab stocke et transmet. Le passer en minuscules évite que Foo@example.com et foo@example.com ne donnent deux profils distincts.

Quand votre fournisseur est en panne

Si votre gestionnaire appelle un service tiers, prévoyez ce qui doit se passer quand ce service est indisponible. En cas d'échec, un hook bloque l'opération : si vous ne prévoyez rien, une panne du fournisseur bloque les inscriptions de toutes les organisations couvertes par la liaison.

Interceptez l'échec et poursuivez. Un fournisseur injoignable ne prouve pas qu'un client est un fraudeur :

user.ts — refuse bad addresses, but survive an outage
import { OmniLab } from "./omnilab";
import type { ContactCreatePreHandler } from "./omnilab";

export const onContactCreatePre: ContactCreatePreHandler = async (input) => {
  const email = input.contact.email.trim().toLowerCase();

  try {
    const response = await OmniLab.Http.get(
      "https://api.example-checker.com/v1/check?email=" + encodeURIComponent(email),
    );
    const verdict = await response.json<{ disposable: boolean }>();

    if (verdict.disposable) {
      return OmniLab.Flow.deny("disposable_email", "Checker flagged the address as disposable");
    }
  } catch (error) {
    // Le service de vérification est injoignable. On laisse passer l'inscription plutôt
    // que de bloquer un vrai client à cause de la panne d'un autre.
    console.warn("email check unavailable, allowing sign-up: " + String(error));
  }

  return OmniLab.Flow.continue({ email });
};

Ne refusez qu'en cas de réponse négative certaine. Traitez « je n'ai pas pu obtenir de réponse » comme une raison de poursuivre.

try/catch intercepte l'échec d'une requête HTTP, pas le dépassement du délai de votre gestionnaire. Réglez le délai de la function au-dessus du temps de réponse le plus lent de votre fournisseur : voir Limites d'exécution.

contact.verify

Votre fichier exporte onContactVerify. OmniLab génère le lien de double opt-in, et votre gestionnaire l'envoie via votre fournisseur d'email.

Commencez par vérifier le branchement sans fournisseur. Le lien arrive dans input.verification_url : écrivez-le dans le journal.

user.ts — hello world, for setup only
import { OmniLab } from "./omnilab";
import type { ContactVerifyHandler } from "./omnilab";

export const onContactVerify: ContactVerifyHandler = (input) => {
  console.log("Hello world! Verification link for " + input.contact.email);
  console.log(input.verification_url);
  return OmniLab.Flow.continue();
};

Inscrivez un contact de test, ouvrez le journal d'exécution et copiez le lien depuis le panneau Journaux pour terminer la vérification à la main.

À remplacer avant la mise en production

Poursuivre sans rien envoyer indique à OmniLab que l'email est parti alors que ce n'est pas le cas : les vrais contacts ne reçoivent jamais de lien. N'utilisez cette version que pendant la mise en place de la function.

Envoyer via votre fournisseur

OmniLab ne réessaie jamais un appel de hook. Chaque envoi, y compris un renvoi demandé par le contact, comporte une nouvelle input.context.idempotency_key. Transmettez-la si votre fournisseur accepte une clé d'idempotence ; sinon, vous pouvez l'ignorer.

Le contexte indique aussi d'où vient l'inscription. context.interaction_id et context.interaction_public_key identifient la campagne, et context.group_id l'organisation pour laquelle la liaison a été résolue. Une même function peut s'en servir pour adapter son email à chaque campagne : inutile de maintenir une function par campagne.

Construisez la salutation avec contactDisplayName plutôt qu'avec input.contact.firstname. Un contact comporte soit firstname + lastname, soit un seul fullname, selon la façon dont le formulaire d'inscription de la campagne recueille les noms. Un gestionnaire qui ne lit que firstname envoie « Bonjour , » à tous les contacts d'une campagne qui recueille un nom complet.

user.ts — send with Brevo
import { OmniLab, contactDisplayName } from "./omnilab";
import type { ContactVerifyHandler } from "./omnilab";

export const onContactVerify: ContactVerifyHandler = async (input) => {
  // Lit fullname, ou firstname + lastname, selon ce que comporte le contact.
  const name = contactDisplayName(input.contact);

  // Les valeurs de la configuration du plugin sont des chaînes, et Brevo attend un nombre.
  const templateId = Number(OmniLab.Config.get("brevo_template_id"));
  if (!Number.isInteger(templateId) || templateId < 1) {
    throw new Error("Set brevo_template_id in Plugin config to a Brevo template id");
  }

  const response = await OmniLab.Http.post("https://api.brevo.com/v3/smtp/email", {
    headers: {
      "content-type": "application/json",
      "api-key": OmniLab.Config.getSecret("brevo.API_KEY"),
    },
    body: JSON.stringify({
      to: [{ email: input.contact.email, name }],
      templateId,
      params: { NAME: name, VERIFICATION_URL: input.verification_url },
    }),
  });

  if (!response.ok) {
    return OmniLab.Flow.deny("dispatch_failed", "Brevo refused the send: HTTP " + response.status);
  }

  const sent = await response.json<{ messageId?: string }>();
  return OmniLab.Flow.continue({ provider_message_id: sent.messageId ?? "" });
};

Ce code nécessite api.brevo.com dans les hôtes autorisés, une clé brevo_template_id dans la carte Configuration du plugin, et un lot de secrets brevo associé, contenant API_KEY. Ces trois éléments se configurent comme indiqué dans Configurer l'exécution et les secrets. Sans identifiant de modèle valide, le gestionnaire lève une exception : l'inscription échoue et le journal d'exécution indique pourquoi.

Poursuivre signifie expédié, pas délivré. continue indique à OmniLab que votre fournisseur a accepté le message, pas qu'il est arrivé. Ne refusez que si l'expédition elle-même a échoué, car un refus fait échouer l'inscription.

Ne pas envoyer l'email si le contact est déjà vérifié

Vos propres données contiennent peut-être déjà un opt-in confirmé pour cette adresse, issu par exemple d'une campagne précédente. Retournez alors already_verified: true au lieu d'envoyer l'email : OmniLab crée le contact comme vérifié sur la foi de votre preuve. Ne le faites qu'à partir d'un double opt-in confirmé, jamais d'une simple présence dans une liste importée : vous vous portez garant du consentement.

user.ts — skip the email when your records prove consent
import { OmniLab, contactDisplayName } from "./omnilab";
import type { ContactVerifyHandler } from "./omnilab";

// Demande à votre propre système si cette adresse a déjà terminé le double opt-in.
// Tout échec compte comme "non prouvé" : l'email est alors envoyé normalement.
async function hasConfirmedOptIn(email: string): Promise<boolean> {
  try {
    const response = await OmniLab.Http.get(
      "https://crm.lindenhall.example/v1/optins?email=" + encodeURIComponent(email),
      { headers: { authorization: "Bearer " + OmniLab.Config.getSecret("crm.API_TOKEN") } },
    );
    if (!response.ok) {
      return false;
    }
    const record = await response.json<{ confirmed?: boolean }>();
    return record.confirmed === true;
  } catch (error) {
    console.warn("opt-in lookup failed, sending the email: " + String(error));
    return false;
  }
}

export const onContactVerify: ContactVerifyHandler = async (input) => {
  if (await hasConfirmedOptIn(input.contact.email)) {
    return OmniLab.Flow.continue({ already_verified: true });
  }

  const name = contactDisplayName(input.contact);
  const templateId = Number(OmniLab.Config.get("brevo_template_id"));
  if (!Number.isInteger(templateId) || templateId < 1) {
    throw new Error("Set brevo_template_id in Plugin config to a Brevo template id");
  }

  const response = await OmniLab.Http.post("https://api.brevo.com/v3/smtp/email", {
    headers: {
      "content-type": "application/json",
      "api-key": OmniLab.Config.getSecret("brevo.API_KEY"),
    },
    body: JSON.stringify({
      to: [{ email: input.contact.email, name }],
      templateId,
      params: { NAME: name, VERIFICATION_URL: input.verification_url },
    }),
  });

  if (!response.ok) {
    return OmniLab.Flow.deny("dispatch_failed", "Brevo refused the send: HTTP " + response.status);
  }

  const sent = await response.json<{ messageId?: string }>();
  return OmniLab.Flow.continue({ provider_message_id: sent.messageId ?? "" });
};

En plus de la configuration Brevo, ce code nécessite crm.lindenhall.example dans les hôtes autorisés et un lot de secrets crm contenant API_TOKEN.

Lier la function au point d'accroche

Choisissez d'abord la portée :

PortéeQuand l'utiliserComment la définir
GlobalLa function doit s'exécuter pour toutes les organisationsL'option Global (tous les groupes) activée
Groupes précisSeules certaines organisations en ont besoinLe champ Groupes cibles

Une liaison est active dès sa création

Il n'existe ni liaison en brouillon, ni liaison en mode journal seul : en quelques secondes, les opérations couvertes commencent à attendre votre function. Testez-la d'abord, puis liez une seule organisation à faible trafic avant de créer une liaison globale.

Dans l'éditeur de la function :

  1. Sélectionnez l'onglet Hooks.

    Lindenhall double opt-in avec l'onglet Hooks mis en évidence

  2. Sélectionnez Ajouter une liaison.

    Onglet Hooks avec le bouton Ajouter une liaison mis en évidence

    Le champ Point d'accroche est fixé par la function et s'affiche en lecture seule.

  3. Sous Groupes cibles, sélectionnez Lindenhall Shopping Centre.

    Formulaire d'ajout de liaison avec Lindenhall Shopping Centre dans Groupes cibles, mis en évidence

  4. Sélectionnez Créer.

    Formulaire d'ajout de liaison avec le bouton Créer mis en évidence

La liaison est créée au statut Actif. Pour couvrir toutes les organisations, activez plutôt l'option Global (tous les groupes) à l'étape 3.

Le formulaire applique les règles de portée :

  • Au maximum une liaison globale active par point d'accroche. S'il en existe une, l'interrupteur est désactivé et indique pourquoi.
  • Un groupe appartient à au maximum une liaison active par point d'accroche. Les groupes déjà couverts sont grisés.
  • Une liaison propre à des groupes l'emporte sur la liaison globale pour ces groupes. Utilisez une liaison globale pour le comportement par défaut, et des liaisons par groupe pour les exceptions.

Une liaison ne se modifie pas : pour changer ses organisations, supprimez-la et créez-en une nouvelle. Pour contact.verify, créez la liaison avant qu'un administrateur active le double opt-in : voir Vérification email en double opt-in.

Retirer une liaison du parcours en production

L'effet dépend du point d'accroche : voir la dernière ligne de Ce que garantit un hook. Pour contact.verify, désactiver la liaison fait échouer toutes les inscriptions en double opt-in.

Dans l'éditeur de la function :

  1. Sélectionnez l'onglet Hooks.
  2. Sur la ligne de la liaison, sélectionnez Désactiver.

Le bouton Activer la rétablit. Le bouton Supprimer retire la liaison mais conserve la function. La carte de la function affiche non lié quand une function de type hook n'a aucune liaison active.

En cas de blocage

  • L'onglet Hooks est absent : il n'apparaît que sur les functions de type hook. Vérifiez le badge de type en haut de l'éditeur.
  • L'interrupteur global est désactivé : une autre liaison globale active couvre ce point d'accroche. Désactivez-la, ou ciblez plutôt des organisations précises.
  • Un groupe est grisé dans Groupes cibles : une autre liaison active sur ce point d'accroche le couvre déjà.
  • Les inscriptions échouent depuis que vous avez créé une liaison : revenez en arrière comme l'indique le tableau des garanties pour votre point d'accroche, puis lisez le journal d'exécution.
  • Les exécutions semblent réussies mais les opérations sont refusées, ou un test affiche Violation de contrat : la réponse de votre gestionnaire ne respecte pas le contrat. Voir Lire une violation de contrat de hook.
  • L'email de vérification n'arrive jamais : ouvrez le détail de l'exécution. EGRESS_BLOCKED signifie que l'hôte du fournisseur n'est pas autorisé. PERMISSION_DENIED ou NOT_FOUND renvoyé par getSecret signale un problème de lot de secrets ou de clé : voir Configurer l'exécution et les secrets.

Étapes suivantes

Sur cette page