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.
I can support you on design and development. Reply within 24 hours.
Let's talk →