Dans le cadre d'un projet Imagino, j'ai eu à concevoir et implémenter une brique de synchronisation des consentements clients en temps réel. L'objectif : chaque modification de consentement opt-in email ou SMS dans notre application Django devait être immédiatement répercutée dans les tables standard d'Imagino, sans intervention manuelle, sans batch.
Cet article détaille l'architecture mise en place, les choix techniques et les points de vigilance rencontrés.
L'application Django hébergée chez le client proposait une interface utilisateur dédiée à la gestion des consentements — un formulaire de préférences de communication accessible aux clients finaux, directement dans leur espace personnel. C'est via cette interface que les utilisateurs activaient ou désactivaient leurs opt-ins email et SMS.
Chaque soumission du formulaire créait une nouvelle entrée dans la table ConsentLog, avec un historique complet des modifications. Imagino, de son côté, attendait que ses tables standard de gestion des opt-ins soient à jour pour alimenter les campagnes email et SMS.
Le besoin était clair : à chaque nouvelle entrée dans ConsentLog — déclenchée par une action utilisateur sur l'interface — pousser les informations vers l'API Imagino pour mettre à jour le profil client correspondant en temps réel.
💡 Choix d'architecture : on a opté pour du temps réel via signal Django plutôt qu'un batch planifié. La raison : un consentement retiré doit être pris en compte immédiatement pour des raisons RGPD. Un délai de plusieurs heures n'était pas acceptable.
La table ConsentLog stockait chaque modification de consentement avec ses métadonnées :
from django.db import models from django.utils import timezone class ConsentLog(models.Model): # Clés d'identification — matching avec profil Imagino email = models.EmailField(db_index=True) nom = models.CharField(max_length=100) prenom = models.CharField(max_length=100) # Consentements optin_email = models.BooleanField(default=False) optin_sms = models.BooleanField(default=False) # Métadonnées created_at = models.DateTimeField(default=timezone.now) source = models.CharField(max_length=50) # web, app, crm... class Meta: # Pas de doublon : un seul enregistrement actif par email unique_together = ['email', 'nom', 'prenom']
La contrainte unique_together sur le triplet email + nom + prénom est importante : elle garantit qu'on ne crée pas de doublon côté Django, et c'est aussi la clé de matching utilisée pour retrouver le profil dans Imagino.
Le signal post_save de Django est déclenché automatiquement après chaque sauvegarde d'une instance de modèle. C'est le point d'entrée de la synchronisation.
from django.db.models.signals import post_save from django.dispatch import receiver from .models import ConsentLog from .imagino_client import push_consent_to_imagino @receiver(post_save, sender=ConsentLog) def sync_consent_to_imagino(sender, instance, created, **kwargs): # On ne synchronise que les nouvelles entrées # Pas d'update — chaque changement crée un nouvel enregistrement if not created: return push_consent_to_imagino(instance)
💡 Décision importante : on ne gère que les créations (created=True), pas les mises à jour. Chaque changement de consentement crée une nouvelle ligne dans ConsentLog — c'est un log immuable, pas une table d'état. Ça simplifie considérablement la logique et garantit un historique complet.
La logique d'appel à l'API Imagino est isolée dans un module dédié. Ça facilite les tests unitaires et la maintenance.
import requests from django.conf import settings from .models import ErrorLogConsentAPI IMAGINO_API_URL = settings.IMAGINO_API_URL IMAGINO_API_KEY = settings.IMAGINO_API_KEY def push_consent_to_imagino(consent: ConsentLog) -> None: payload = { "email": consent.email, "nom": consent.nom, "prenom": consent.prenom, "optin_email": consent.optin_email, "optin_sms": consent.optin_sms, } try: response = requests.post( f"{IMAGINO_API_URL}/consents", json=payload, headers={ "Authorization": f"Bearer {IMAGINO_API_KEY}", "Content-Type": "application/json", }, timeout=5, ) response.raise_for_status() except requests.RequestException as exc: # Log de l'erreur pour traitement ultérieur ErrorLogConsentAPI.objects.create( consent_log = consent, error_code = getattr(exc.response, "status_code", 0), error_msg = str(exc), )
Plutôt que de lever une exception qui bloquerait le flux utilisateur, les erreurs d'appel API sont capturées et stockées dans une table dédiée ErrorLogConsentAPI.
class ErrorLogConsentAPI(models.Model): consent_log = models.ForeignKey( ConsentLog, on_delete=models.CASCADE, related_name='errors' ) error_code = models.IntegerField() error_msg = models.TextField() created_at = models.DateTimeField(auto_now_add=True) resolved = models.BooleanField(default=False) class Meta: ordering = ['-created_at']
Cette table est stockée directement dans Imagino — c'est une table custom créée spécifiquement pour ce projet, en dehors des tables standard de la plateforme. Elle permet à l'équipe de voir en un coup d'œil les synchronisations qui ont échoué, d'en connaître la cause (timeout, profil introuvable, doublon…) et de rejouer manuellement ou automatiquement les entrées en erreur, sans quitter l'environnement Imagino.
L'API Imagino retourne une erreur quand le triplet email + nom + prénom ne correspond à aucun profil existant. C'est le cas le plus fréquent — un utilisateur s'inscrit et donne son consentement avant que son profil ait été créé dans Imagino (délai d'ingestion).
La solution : l'erreur est loggée avec le code approprié, et un job de réconciliation nocturne rejoue les entrées en erreur une fois les profils créés.
Quand l'API Imagino détecte plusieurs profils correspondant aux mêmes clés d'identification (email + nom + prénom non unique dans Imagino), elle retourne une erreur de lecture client. Ce cas révèle un problème de qualité de données en amont — des profils en doublon dans Imagino qu'il faut fusionner.
Un timeout de 5 secondes est configuré sur chaque appel. En cas de dépassement, l'erreur est loggée et le consentement sera rejoué lors de la réconciliation. L'utilisateur n'est jamais bloqué par un problème de disponibilité de l'API Imagino.
⚠️ Point de vigilance : le signal post_save s'exécute dans le même thread que la requête HTTP Django. Un appel API synchrone de 5 secondes maximum peut allonger le temps de réponse perçu par l'utilisateur. Si la latence est critique, envisagez de déléguer l'appel API à une tâche Celery asynchrone.
Les paramètres d'API ne sont jamais dans le code — ils sont dans les variables d'environnement :
import os IMAGINO_API_URL = os.environ.get("IMAGINO_API_URL") IMAGINO_API_KEY = os.environ.get("IMAGINO_API_KEY")
Cette architecture de synchronisation temps réel est simple, robuste et maintenable. Le signal Django garantit qu'aucun consentement n'est perdu, la table d'erreurs permet de tracer et rejouer les échecs, et la logique d'insert-only simplifie considérablement la gestion des états.
Elle fonctionne bien pour des volumes modérés (quelques dizaines de modifications par minute). Au-delà, le passage à Celery s'impose pour éviter toute dégradation des temps de réponse.
As part of an Imagino project, I designed and implemented a real-time customer consent synchronisation component. The goal: every opt-in email or SMS consent change in our Django application had to be immediately reflected in Imagino's standard tables, with no manual intervention and no batch processing.
This article details the architecture, technical decisions and watch points encountered.
The Django application hosted at the client's site provided a dedicated user interface for consent management — a communication preference form accessible to end customers directly from their personal account. This is where users activated or deactivated their email and SMS opt-ins.
Each form submission created a new entry in the ConsentLog table, with a complete change history. Imagino, in turn, needed its standard opt-in management tables to be up to date to feed email and SMS campaigns.
The requirement was clear: every new entry in ConsentLog — triggered by a user action on the interface — should push information to the Imagino API to update the corresponding customer profile in real time.
💡 Architecture decision: we chose real-time via Django signal rather than a scheduled batch. The reason: a withdrawn consent must be acted on immediately for GDPR compliance. A delay of several hours was not acceptable.
The ConsentLog table stored each consent change with its metadata:
from django.db import models from django.utils import timezone class ConsentLog(models.Model): # Identification keys — matching with Imagino profile email = models.EmailField(db_index=True) nom = models.CharField(max_length=100) prenom = models.CharField(max_length=100) # Consents optin_email = models.BooleanField(default=False) optin_sms = models.BooleanField(default=False) # Metadata created_at = models.DateTimeField(default=timezone.now) source = models.CharField(max_length=50) # web, app, crm... class Meta: # No duplicate: one active record per email unique_together = ['email', 'nom', 'prenom']
The unique_together constraint on the email + last name + first name triplet is important: it prevents duplicates on the Django side, and is also the matching key used to find the profile in Imagino.
Django's post_save signal is triggered automatically after each model instance save. This is the entry point for synchronisation.
from django.db.models.signals import post_save from django.dispatch import receiver from .models import ConsentLog from .imagino_client import push_consent_to_imagino @receiver(post_save, sender=ConsentLog) def sync_consent_to_imagino(sender, instance, created, **kwargs): # Only sync new entries # No update — each change creates a new record if not created: return push_consent_to_imagino(instance)
💡 Key decision: we only handle creations (created=True), not updates. Each consent change creates a new row in ConsentLog — it's an immutable log, not a state table. This considerably simplifies the logic and guarantees a complete history.
The Imagino API call logic is isolated in a dedicated module, making unit testing and maintenance easier.
import requests from django.conf import settings from .models import ErrorLogConsentAPI IMAGINO_API_URL = settings.IMAGINO_API_URL IMAGINO_API_KEY = settings.IMAGINO_API_KEY def push_consent_to_imagino(consent: ConsentLog) -> None: payload = { "email": consent.email, "nom": consent.nom, "prenom": consent.prenom, "optin_email": consent.optin_email, "optin_sms": consent.optin_sms, } try: response = requests.post( f"{IMAGINO_API_URL}/consents", json=payload, headers={ "Authorization": f"Bearer {IMAGINO_API_KEY}", "Content-Type": "application/json", }, timeout=5, ) response.raise_for_status() except requests.RequestException as exc: # Log error for later processing ErrorLogConsentAPI.objects.create( consent_log = consent, error_code = getattr(exc.response, "status_code", 0), error_msg = str(exc), )
Rather than raising an exception that would block the user flow, API call errors are caught and stored in a dedicated ErrorLogConsentAPI table.
class ErrorLogConsentAPI(models.Model): consent_log = models.ForeignKey( ConsentLog, on_delete=models.CASCADE, related_name='errors' ) error_code = models.IntegerField() error_msg = models.TextField() created_at = models.DateTimeField(auto_now_add=True) resolved = models.BooleanField(default=False) class Meta: ordering = ['-created_at']
This table is stored directly in Imagino — it's a custom table created specifically for this project, outside the platform's standard tables. It lets the team see failed synchronisations at a glance, understand the cause (timeout, profile not found, duplicate…) and manually or automatically replay failed entries, without leaving the Imagino environment.
The Imagino API returns an error when the email + last name + first name triplet matches no existing profile. This is the most frequent case — a user signs up and gives consent before their profile has been created in Imagino (ingestion delay).
The solution: the error is logged with the appropriate code, and a nightly reconciliation job replays failed entries once profiles are created.
When the Imagino API detects multiple profiles matching the same identification keys (non-unique email + name triplet in Imagino), it returns a client read error. This case reveals an upstream data quality issue — duplicate profiles in Imagino that need to be merged.
A 5-second timeout is configured on each call. In case of timeout, the error is logged and the consent will be replayed during reconciliation. The user is never blocked by an Imagino API availability issue.
⚠️ Watch point: the post_save signal runs in the same thread as the Django HTTP request. A synchronous API call of up to 5 seconds can extend the response time perceived by the user. If latency is critical, consider delegating the API call to an asynchronous Celery task.
This real-time synchronisation architecture is simple, robust and maintainable. The Django signal ensures no consent is lost, the error table enables tracing and replaying failures, and the insert-only logic considerably simplifies state management.
It works well for moderate volumes (a few dozen changes per minute). Beyond that, moving to Celery is necessary to prevent response time degradation.
Je peux vous accompagner sur la conception et le développement. Réponse sous 24h.I can support you on design and development. Reply within 24 hours.
Parlons de votre projet →Let's talk →