Skip to main content

Mailjet Integration with OmniCRM

OmniCRM integrates with Mailjet to manage all email communication with customers and staff, ensuring professional, branded, and reliable email delivery for both transactional emails and marketing campaigns.

Overview

The Mailjet integration provides:

  • Automated Transactional Emails - Password resets, invoices, welcome emails, notifications
  • Marketing Consent (double opt-in) - Auditable opt-in/opt-out for marketing email, including confirmation and re-permission flows
  • Email Templates - 10+ pre-configured email types with customizable Mailjet templates
  • Marketing Campaigns - Segmented email campaigns based on customer data (segments can be built from CRM data or CSV upload)
  • Reliable Delivery - Professional email infrastructure with tracking and analytics

OmniCRM does not maintain a live Mailjet contact-list sync. Use Customers → All Contacts in the admin UI (or GET /crm/contact/export) to export contacts for external list management when needed.

Configuration

Mailjet is configured in OmniCRM-API/crm_config.yaml under the mailjet section.

Basic Configuration

mailjet:
api_key: your_mailjet_api_key
api_secret: your_mailjet_api_secret

Obtaining API Credentials:

  1. Create account at <https://www.mailjet.com>
  2. Navigate to Account Settings → API Keys
  3. Copy API Key and Secret Key
  4. Paste into crm_config.yaml

Email Template Configuration

OmniCRM uses 10 distinct email template types for automated communications. Each template is configured with:

  • from_email - Sender email address
  • from_name - Sender display name
  • template_id - Mailjet template ID (numeric)
  • subject - Email subject line

Template Types and Configuration

Customer Welcome Email

Sent when a new customer account is created.

api_crmCommunicationCustomerWelcome:
from_email: "support@yourcompany.com"
from_name: "Your Company Support"
template_id: 5977509
subject: "Welcome to Your Company"

When Sent:

  • New customer signs up via Self-Care portal
  • Staff creates new customer account
  • Customer activates service for first time

Template Variables Available:

  • {{ var:customer_name }} - Customer's full name
  • {{ var:email }} - Customer's email address
  • {{ var:company_name }} - Your company name
  • {{ var:login_url }} - Link to Self-Care portal
  • {{ var:support_url }} - Link to support page

Customer Invoice Email

Sent when an invoice is generated and ready for payment.

api_crmCommunicationCustomerInvoice:
from_email: "billing@yourcompany.com"
from_name: "Your Company Billing"
template_id: 6759851
subject: "Your Invoice - "

When Sent:

  • Invoice automatically generated for billing period
  • Manual invoice created by staff
  • Customer requests invoice copy

Template Variables Available:

  • {{ var:customer_name }} - Customer's full name
  • {{ var:invoice_number }} - Invoice ID/number
  • {{ var:invoice_date }} - Invoice issue date
  • {{ var:due_date }} - Payment due date
  • {{ var:total_amount }} - Total amount due
  • {{ var:invoice_url }} - Link to view/download invoice PDF
  • {{ var:pay_url }} - Link to pay invoice online

Invoice Attachment:

The invoice PDF is automatically attached to the email.

Customer Invoice Reminder

Sent to remind customers of overdue invoices.

api_crmCommunicationCustomerInvoiceReminder:
from_email: "billing@yourcompany.com"
from_name: "Your Company Billing"
template_id: 6759852
subject: "Payment Reminder - Invoice Overdue"

When Sent:

  • Invoice is X days past due (configurable)
  • Manual reminder triggered by staff
  • Automated reminder workflow (if configured)

Template Variables Available:

  • {{ var:customer_name }}
  • {{ var:invoice_number }}
  • {{ var:due_date }}
  • {{ var:days_overdue }}
  • {{ var:total_amount }}
  • {{ var:pay_url }}

Staff User Welcome Email

Sent when a new staff user account is created.

api_crmCommunicationUserWelcome:
from_email: "admin@yourcompany.com"
from_name: "Your Company Admin"
template_id: 5977510
subject: "Welcome to the Team"

When Sent:

  • Admin creates new staff user
  • "Send Welcome Email" button clicked in user management

Template Variables Available:

  • {{ var:user_name }} - Staff user's full name
  • {{ var:email }} - Staff user's email
  • {{ var:role }} - Assigned role(s)
  • {{ var:login_url }} - Link to admin portal login
  • {{ var:temp_password }} - Temporary password (if applicable)
  • {{ var:support_email }} - IT support contact

User Password Reset

Sent when a user requests to reset their password.

api_crmCommunicationUserPasswordReset:
from_email: "noreply@yourcompany.com"
from_name: "Your Company Security"
template_id: 5977511
subject: "Password Reset Request"

When Sent:

  • User clicks "Forgot Password" on login page
  • User submits password reset request

Template Variables Available:

  • {{ var:user_name }}
  • {{ var:reset_url }} - Time-limited password reset link (typically 1 hour)
  • {{ var:expiry_time }} - When reset link expires

Security Note:

Reset links expire after configured time period (default 1 hour).

User Password Reset Success

Sent to confirm password was successfully changed.

api_crmCommunicationUserPasswordResetSuccess:
from_email: "noreply@yourcompany.com"
from_name: "Your Company Security"
template_id: 5977512
subject: "Password Changed Successfully"

When Sent:

  • User successfully completes password reset
  • Immediately after new password is set

Template Variables Available:

  • {{ var:user_name }}
  • {{ var:change_date }} - Date/time password was changed
  • {{ var:ip_address }} - IP address of change (optional)
  • {{ var:support_email }} - Contact if change was unauthorized

User Password Change

Sent when a user changes their password from settings.

api_crmCommunicationUserPasswordChange:
from_email: "noreply@yourcompany.com"
from_name: "Your Company Security"
template_id: 5977513
subject: "Password Change Notification"

When Sent:

  • User changes password from profile/settings
  • Admin resets user password

Template Variables Available:

  • {{ var:user_name }}
  • {{ var:change_date }}
  • {{ var:changed_by }} - "Self" or admin name
  • {{ var:support_email }}

Email Verification

Sent to verify a user's email address.

api_crmCommunicationEmailVerification:
from_email: "noreply@yourcompany.com"
from_name: "Your Company"
template_id: 5977514
subject: "Verify Your Email Address"

When Sent:

  • New account created (customer or staff)
  • User changes email address
  • Email verification required for security

Template Variables Available:

  • {{ var:user_name }}
  • {{ var:verification_url }} - Link to verify email
  • {{ var:verification_code }} - Code to enter manually (alternative to link)

Balance Expired Notification

Sent when a customer's service balance/allowance has expired.

api_crmCommunicationsBalanceExpired:
from_email: "support@yourcompany.com"
from_name: "Your Company Support"
template_id: 5977515
subject: "Your Service Balance Has Expired"

When Sent:

  • Prepaid balance expires
  • Monthly allowance renewal date passed
  • Service expiry date reached

Template Variables Available:

  • {{ var:customer_name }}
  • {{ var:service_name }} - Name of expired service
  • {{ var:expiry_date }}
  • {{ var:balance_type }} - "Data", "Voice", "Monetary", etc.
  • {{ var:renewal_url }} - Link to renew/top-up

Low Balance Alert

Sent when a customer's balance falls below configured threshold.

api_crmCommunicationsBalanceLow:
from_email: "support@yourcompany.com"
from_name: "Your Company Support"
template_id: 5977516
subject: "Low Balance Alert"

When Sent:

  • Balance drops below threshold (e.g., 20% remaining)
  • Configured in service plan or OCS
  • Real-time monitoring triggers alert

Template Variables Available:

  • {{ var:customer_name }}
  • {{ var:service_name }}
  • {{ var:current_balance }}
  • {{ var:threshold }}
  • {{ var:balance_type }}
  • {{ var:topup_url }} - Link to add balance

eSIM Delivery Email

Sent when an eSIM is provisioned and ready for activation.

api_crmCommunicationEsimDelivery:
from_email: "support@yourcompany.com"
from_name: "Your Company Support"
template_id: 0
subject: "Your eSIM is Ready"

When Sent:

  • eSIM provisioned during self-signup
  • eSIM assigned to a customer service

Template Variables Available:

  • {{ var:customer_name }} - Customer's full name
  • {{ var:iccid }} - eSIM ICCID
  • {{ var:activation_code }} - LPA activation code string
  • {{ var:has_qr_code }} - Boolean, true if QR code image is attached
  • {{ var:has_activation_code }} - Boolean, true if activation code is available

Built-in Fallback:

When template_id is 0 (or the template key is missing from config), OmniCRM uses a built-in branded HTML email with the eSIM QR code rendered inline. The colours and brand name are controlled by the branding section in crm_config.yaml — see Configuration Guide for details.

Creating Mailjet Email Templates

For each email type, you need to create a corresponding template in Mailjet.

Step 1: Create Template in Mailjet

  1. Login to Mailjet dashboard
  2. Navigate to Email Templates → Transactional Templates
  3. Click Create a New Template
  4. Choose Code your own template (for advanced users) or Use template builder

Step 2: Design Template

Use Mailjet's drag-and-drop builder or HTML editor to design your email.

Essential Elements:

  • Header - Company logo and branding
  • Greeting - Personalized with {{ var:customer_name }} or {{ var:user_name }}
  • Content - Main message body
  • Variables - Insert template variables from list above
  • Call to Action - Buttons/links for user actions
  • Footer - Unsubscribe link, company address, support contact

Example Template (Password Reset):

<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.button { background-color: #4CAF50; color: white; padding: 14px 28px; }
</style>
</head>
<body>
![Logo](https://yourcompany.com/logo.png)

<h2>Password Reset Request</h2>

<p>Hello {{ var:user_name }},</p>

<p>We received a request to reset your password. Click the button below to create a new password:</p>

<a href="{{ var:reset_url }}" class="button">Reset Password</a>

<p>This link expires in {{ var:expiry_time }}.</p>

<p>If you didn't request this, please ignore this email.</p>

<hr>
<p style="font-size: 12px; color: #666;">
Your Company | support@yourcompany.com<br/>
123 Business St, City, Country
</p>
</body>
</html>

Step 3: Get Template ID

  1. Save template in Mailjet
  2. Note the Template ID (numeric, e.g., 5977509)
  3. Copy this ID to crm_config.yaml

Step 4: Test Template

  1. In Mailjet, use Test Email feature
  2. Provide sample values for all variables
  3. Send test email to yourself
  4. Verify formatting, links, and branding

Step 5: Configure in OmniCRM

Add template configuration to crm_config.yaml:

mailjet:
api_key: your_api_key
api_secret: your_secret

api_crmCommunicationUserPasswordReset:
from_email: "noreply@yourcompany.com"
from_name: "Your Company Security"
template_id: 5977511
subject: "Password Reset Request"

Restart OmniCRM API for changes to take effect:

cd OmniCRM-API
sudo systemctl restart omnicrm-api

Contacts export (CRM)

For reporting, compliance, or manual import into Mailjet (or another ESP), admins can browse and export contacts from the CRM without a live Mailjet list sync.

Admin UI

Customers → All Contacts (/contacts) provides a paginated table with search, filters (marketing_consent_status, customer_status), and Export CSV. The export uses the same filters and search as the table.

API

Both endpoints require VIEW_CUSTOMER_CONTACT permission.

MethodPathPurpose
GET/crm/contact/searchPaginated contact list with search, page, per_page, sort, order, filters (JSON)
GET/crm/contact/exportCSV download with the same query parameters (max 50,000 rows)

CSV columns: contact_id, customer_id, customer_name, customer_status, name, email, phone, job title, contact_types, marketing_consent_status, marketing_consent_granted_at, created, last_modified.

Filter on marketing_consent_status=granted when preparing a list for marketing use. Import the CSV into Mailjet (or your ESP) using their contact-import workflow.

Implementation: OmniCRM-API/services/contact_service.py (search_contacts, export_contacts_csv).

Mailjet refuses to approve a contact-list import unless the sender can demonstrate that each recipient explicitly consented to receive marketing email. To meet that requirement, OmniCRM stores per-contact marketing consent and exposes a double-opt-in ("confirmation email") flow that produces an auditable consent trail.

The implementation lives in OmniCRM-API/services/marketing_consent_service.py. Only contacts with marketing_consent_status == 'granted' should be used for marketing sends or external list imports.

Data model

Two tables are involved:

  • contact.marketing_consent_status (Enum: none | pending | granted | revoked) and contact.marketing_consent_granted_at - the denormalised current state used for fast filtering in the contacts table and export.
  • marketing_consent - an append-only audit log. Every state transition (signup tick, confirmation click, self-care toggle, webhook-driven revoke, admin re-permission) writes a new row capturing the contact ID, email at the time of the event, source, consent text version, IP address, user-agent, signed token, and confirmation / revocation timestamps. The log is the source of truth if Mailjet ever asks for proof of consent for a given recipient.

The "consent text version" is configured under marketing_consent.consent_text_version in crm_config.yaml and is stamped on every audit row, so changing the wording of the consent copy in the future is non-destructive: existing rows continue to point at the version the user actually saw.

Configuration

Add the marketing_consent block to crm_config.yaml:

marketing_consent:
consent_text_version: "v1-2026-05"
confirmation_url_path: "/marketing/confirm"
token_expiry_days: 30
re_permission_batch_size: 200
re_permission_rate_per_second: 5
re_permission_skip_pending_within_hours: 72

Optionally also configure a Mailjet template for the confirmation email (otherwise a hard-coded HTML/text body is used):

mailjet:
api_crmCommunicationMarketingConfirm:
from_email: "support@example.com"
from_name: "Norfone Support"
template_id: 0 # 0 = use built-in HTML body
subject: "Please confirm your subscription"

Mailjet template variables exposed to a custom template: firstname, confirmation_link, consent_text_version.

Flow A: New signups

The self-signup form renders a "Sign up to Norfone e-News for the latest news, events & promotions" checkbox. When ticked:

  1. routes/auth.py /self-signup-partial extracts customer.subscribe_to_newsletter from the request body.
  2. After the contact is created, marketing_consent_service.request_consent is called with source='signup', the request IP, and User-Agent.
  3. A marketing_consent audit row is written with granted=True, confirmed_at=NULL, and a signed URL-safe token. The contact's marketing_consent_status is set to pending.
  4. A transactional Mailjet email is sent containing ${public_url}${confirmation_url_path}?token=....
  5. When the user clicks the link, GET /crm/marketing-consent/confirm redeems the token: confirmed_at is stamped on the audit row and marketing_consent_status flips to granted.

Flow B: Self-care (logged-in users)

Self-care surfaces a "Preferences" page (/self-care/preferences) which calls:

MethodPathPurpose
GET/crm/marketing-consent/meCurrent status + full audit history
POST/crm/marketing-consent/me/subscribe(Re)start the double-opt-in flow
POST/crm/marketing-consent/me/unsubscribeRevoke marketing consent immediately

Revocation writes a granted=False audit row and sets the status to revoked. The contact should be removed from any external marketing lists maintained outside OmniCRM.

Flow C: Existing base re-permission (admin)

Legacy contacts created before this feature shipped do not have a consent record and are therefore in marketing_consent_status='none'. Admins can ask them to opt in via the re-permission campaign:

MethodPathPurpose
GET/crm/oam/marketing-consent/statsPer-status counts + candidate list + samples
POST/crm/oam/marketing-consent/re-permission?dry_run=truePlan (no email sent)
POST/crm/oam/marketing-consent/re-permission?dry_run=falseExecute. Paced and bounded by config.

The job scans every contact whose status is none, revoked, or pending older than re_permission_skip_pending_within_hours, has a valid email, is not unsubscribed, and whose customer status is not Closed. For each candidate it calls the same request_consent used by the signup flow, so the audit row format and token semantics are identical regardless of origin.

The send is rate-limited via a token-bucket sized from re_permission_rate_per_second, keeping us comfortably under Mailjet's transactional throughput cap. An optional limit query parameter caps the per-run send count so the first execution can be a small smoke test.

  1. Confirm the database migration for marketing_consent has run.
  2. Visit Email Marketing → Marketing Consent in the admin UI (/email-marketing/marketing-consent). Review the per-status breakdown — most legacy contacts will be none.
  3. Run Dry-run re-permission and inspect the counts.
  4. Send confirmation emails with the limit query parameter set to a small batch (e.g. 50) to validate deliverability and support inbox volume.
  5. Run again without limit to cover the rest of the base. The job is idempotent: contacts with a recent pending request are skipped.
  6. After opt-ins plateau, use Customers → All Contacts, filter marketing_consent_status = granted, and Export CSV for import into Mailjet (or your ESP) if you maintain a marketing list there.

Prometheus metrics

MetricLabelsNotes
omnicrm_marketing_consent_events_totaleventconfirmation_sent, confirmation_failed, granted, revoked, token_redeemed, token_invalid, token_expired
omnicrm_marketing_consent_status_totalstatusGauge per `none

Transactional Mailjet API metrics (omnicrm_mailjet_api_*, omnicrm_mailjet_emails_sent_*) are documented in Monitoring & Metrics.

Troubleshooting

Email not sending

  • Cause: Invalid API credentials, Mailjet account suspended, or template ID wrong
  • Fix:
    • Verify api_key and api_secret in crm_config.yaml
    • Check Mailjet account status and billing
    • Verify template ID exists in Mailjet
    • Check API logs for errors

Template variables not substituting

  • Cause: Variable name mismatch or missing data in OmniCRM
  • Fix:
    • Verify variable names match exactly (case-sensitive)
    • Use {{ var:variable_name }} format
    • Check OmniCRM is passing variable data in API call
    • Test with sample data in Mailjet

Invoice PDF not attaching

  • Cause: PDF generation failed or file size too large
  • Fix:
    • Check invoice generation logs
    • Verify invoice template renders correctly
    • Ensure PDF under 15MB (Mailjet limit)
    • Test invoice PDF generation separately

Need a marketing contact list in Mailjet

  • Cause: OmniCRM no longer pushes contacts to Mailjet automatically
  • Fix:
    • Collect opt-ins via the marketing consent flow (granted status)
    • Export from Customers → All Contacts with consent filter, or GET /crm/contact/export?filters={"marketing_consent_status":["granted"]}
    • Import the CSV into Mailjet using their contact-import UI
  • administration_configuration - Complete Mailjet configuration reference
  • payments_invoices - Invoice generation and email delivery
  • authentication_flows - Password reset and verification emails
  • customer_care - Self-Care portal welcome emails
  • Monitoring & Metrics - Mailjet API metrics, email delivery tracking, and performance monitoring

Further Reading