Retour aux InsightsBack to Insights

Synchroniser les consentements clients vers Imagino via une API Python Django Syncing customer consents to Imagino via a Python Django API

Pierre Frin Mai 2026May 2026 9 min de lecture9 min read
Client Django (hébergé client) Interface préférences ☑ opt-in email ☑ opt-in SMS ConsentLog email · nom · prénom optin_email · optin_sms post_save signal API Python POST /consents imagino Tables standard optin_email ✓ optin_sms ✓ Table custom errorLogConsentAPI error_code · error_msg resolved · created_at si erreur → HÉBERGÉ CHEZ LE CLIENT IMAGINO (SaaS)

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.

Le contexte et le besoin

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.

Le modèle de données Django

La table ConsentLog stockait chaque modification de consentement avec ses métadonnées :

models.py
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 Django — déclenchement temps réel

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.

signals.py
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.

Le client API Imagino

La logique d'appel à l'API Imagino est isolée dans un module dédié. Ça facilite les tests unitaires et la maintenance.

imagino_client.py
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),
        )

La gestion des erreurs — errorLogConsentAPI

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.

models.py — table de log d'erreurs
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.

Les cas d'erreur rencontrés

Profil introuvable dans 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.

Doublon détecté

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.

Timeout API

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.

Configuration et sécurité

Les paramètres d'API ne sont jamais dans le code — ils sont dans les variables d'environnement :

settings.py
import os

IMAGINO_API_URL = os.environ.get("IMAGINO_API_URL")
IMAGINO_API_KEY = os.environ.get("IMAGINO_API_KEY")

Ce qu'on aurait fait différemment

Conclusion

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.

Context and requirement

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 Django data model

The ConsentLog table stored each consent change with its metadata:

models.py
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.

The Django signal — real-time triggering

Django's post_save signal is triggered automatically after each model instance save. This is the entry point for synchronisation.

signals.py
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 client

The Imagino API call logic is isolated in a dedicated module, making unit testing and maintenance easier.

imagino_client.py
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),
        )

Error handling — errorLogConsentAPI

Rather than raising an exception that would block the user flow, API call errors are caught and stored in a dedicated ErrorLogConsentAPI table.

models.py — error log 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.

Error cases encountered

Profile not found in Imagino

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.

Duplicate detected

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.

API timeout

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.

What we'd do differently

Conclusion

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.

Pierre Frin
Fondateur Grokium — Développeur Python Django · Expert intégration CRMFounder Grokium — Python Django Developer · CRM Integration Expert

Un projet d'intégration Python / Imagino ?A Python / Imagino integration project?

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 →