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:
- Create account at <https://www.mailjet.com>
- Navigate to Account Settings → API Keys
- Copy API Key and Secret Key
- 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
- Login to Mailjet dashboard
- Navigate to Email Templates → Transactional Templates
- Click Create a New Template
- 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>

<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
- Save template in Mailjet
- Note the Template ID (numeric, e.g.,
5977509) - Copy this ID to
crm_config.yaml
Step 4: Test Template
- In Mailjet, use Test Email feature
- Provide sample values for all variables
- Send test email to yourself
- 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.
| Method | Path | Purpose |
|---|---|---|
GET | /crm/contact/search | Paginated contact list with search, page, per_page, sort, order, filters (JSON) |
GET | /crm/contact/export | CSV 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).
Marketing Consent (double opt-in)
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) andcontact.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:
routes/auth.py /self-signup-partialextractscustomer.subscribe_to_newsletterfrom the request body.- After the contact is created,
marketing_consent_service.request_consentis called withsource='signup', the request IP, and User-Agent. - A
marketing_consentaudit row is written withgranted=True,confirmed_at=NULL, and a signed URL-safe token. The contact'smarketing_consent_statusis set topending. - A transactional Mailjet email is sent containing
${public_url}${confirmation_url_path}?token=.... - When the user clicks the link,
GET /crm/marketing-consent/confirmredeems the token:confirmed_atis stamped on the audit row andmarketing_consent_statusflips togranted.
Flow B: Self-care (logged-in users)
Self-care surfaces a "Preferences" page
(/self-care/preferences) which calls:
| Method | Path | Purpose |
|---|---|---|
GET | /crm/marketing-consent/me | Current status + full audit history |
POST | /crm/marketing-consent/me/subscribe | (Re)start the double-opt-in flow |
POST | /crm/marketing-consent/me/unsubscribe | Revoke 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:
| Method | Path | Purpose |
|---|---|---|
GET | /crm/oam/marketing-consent/stats | Per-status counts + candidate list + samples |
POST | /crm/oam/marketing-consent/re-permission?dry_run=true | Plan (no email sent) |
POST | /crm/oam/marketing-consent/re-permission?dry_run=false | Execute. 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.
Recommended first-time rollout
- Confirm the database migration for
marketing_consenthas run. - Visit Email Marketing → Marketing Consent in the admin UI
(
/email-marketing/marketing-consent). Review the per-status breakdown — most legacy contacts will benone. - Run Dry-run re-permission and inspect the counts.
- Send confirmation emails with the
limitquery parameter set to a small batch (e.g. 50) to validate deliverability and support inbox volume. - Run again without
limitto cover the rest of the base. The job is idempotent: contacts with a recent pending request are skipped. - 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
| Metric | Labels | Notes |
|---|---|---|
omnicrm_marketing_consent_events_total | event | confirmation_sent, confirmation_failed, granted, revoked, token_redeemed, token_invalid, token_expired |
omnicrm_marketing_consent_status_total | status | Gauge 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_keyandapi_secretincrm_config.yaml - Check Mailjet account status and billing
- Verify template ID exists in Mailjet
- Check API logs for errors
- Verify
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 (
grantedstatus) - 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
- Collect opt-ins via the marketing consent flow (
Related Documentation
administration_configuration- Complete Mailjet configuration referencepayments_invoices- Invoice generation and email deliveryauthentication_flows- Password reset and verification emailscustomer_care- Self-Care portal welcome emails- Monitoring & Metrics - Mailjet API metrics, email delivery tracking, and performance monitoring
Further Reading
- Mailjet Documentation: <https://dev.mailjet.com/>
- Mailjet API Reference: <https://dev.mailjet.com/email/reference/>