Product

How to set up double opt-in via Mailjet’s API (secure, GDPR-ready)

This blog article will show you how to create a double opt-in subscription form or link an already existing one with Mailjet via Mailjet's API.
Image for How to set up double opt-in via Mailjet’s API (secure, GDPR-ready)
June 4, 2024

This comprehensive guide will walk you through building a secure, scalable double opt-in (DOI) subscription flow using Mailjet’s Email API v3.1. 

You’ll learn to implement signed confirmation links, email templates, webhooks, and contact synchronization to create a GDPR-compliant subscription system that improves deliverability and user engagement.

What is double opt-in and why it matters

Double opt-in is a two-step subscription process where users first submit your form and then confirm their subscription via a link sent to their email inbox. Only after this confirmation are they added to your mailing list.

Key benefits of double opt-in

  • Higher deliverability: Removes typos, bots, and role accounts before they impact your sender reputation
  • Lower complaint rates: Only actively confirmed subscribers receive emails, reducing spam reports
  • Clear consent trail: Provides timestamps, IP addresses, and tokens as evidence of consent
  • Better engagement: Confirmed subscribers typically show much higher open and click rates

Single vs. double opt-in comparison

ApproachStepsSignup volumeQualityCompliance
Single Opt-InOne stepHigherLower (typos, bots)Basic
Double Opt-InTwo stepsLowerMuch higherGDPR-ready

What you’ll build

By the end of this guide, you’ll have implemented:

  • Mailjet Send API v3.1 for confirmation and welcome emails
  • Mailjet Contact and list resources for subscriber management
  • Mailjet Event API (webhooks) for real-time tracking
  • Secure token-based confirmation with replay protection

Prerequisites

Before starting, ensure you have:

  • Mailjet account with API Key and Secret for Basic Authentication
  • Verified sender/domain for your From address
  • Mailjet contacts list ID for confirmed subscribers
  • Confirmation email template (via Template API) or inline content
  • Server endpoints to create/verify tokens and handle confirmations
  • Optional: Mailjet Email Validations for form validation

Architecture overview

Here’s how the double opt-in flow works:

  1. User submits subscription form (email + optional properties)
  2. Server creates a signed, single-use token and builds confirmation URL
  3. Confirmation email sent via Send API v3.1 with the confirmation link
  4. User clicks confirmation link
  5. Server verifies token and subscribes contact to Mailjet list
  6. Optional welcome email sent and webhooks configured for ongoing tracking

Step-by-step implementation of double opt-in

Here’s a step-by-step guide using Mailjet’s email API.

Step 1: Capture the subscription

Start by validating and storing the initial subscription request:

  • Validate email format server-side using standard regex or validation libraries
  • Optional: Call Mailjet’s Email Validations API to catch obvious errors
  • Store pending record in your database with status=pending and timestamp
                            

                                // Example validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
  return { error: 'Invalid email format' };
}

// Store pending subscription
await db.subscriptions.create({
  email: email,
  status: 'pending',
  created_at: new Date(),
  ip_address: req.ip
});
                            
                        

Security is critical; never use MD5 or simple hashes. Instead, implement proper token security:

Token requirements

  • Short-lived expiration (30-60 minutes)
  • Single-use only with replay protection
  • Cryptographically signed (HMAC-SHA256 or JWT)
  • Include claims: email, issued_at, expires_at, nonce/jti

Example token creation

                            

                                const jwt = require('jsonwebtoken');
const crypto = require('crypto');

function createConfirmationToken(email) {
  const nonce = crypto.randomBytes(16).toString('hex');
  const payload = {
    email: email,
    nonce: nonce,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hour
  };

  return jwt.sign(payload, process.env.JWT_SECRET);
}

const token = createConfirmationToken('user@example.com');
const confirmUrl = `https://yourapp.com/confirm?token=${token}`;
                            
                        

Step 3: Send the confirmation email

Use Mailjet’s Send API v3.1 to deliver the confirmation email. You have two options:

Option A: Using a stored template (recommended)

First, create your template in Mailjet, then send:

                            

                                curl -s -X POST \
  --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
  https://api.mailjet.com/v3.1/send \
  -H 'Content-Type: application/json' \
  -d '{
    "Messages":[
      {
        "From":{"Email":"no-reply@yourdomain.com","Name":"Your Brand"},
        "To":[{"Email":"recipient@example.com","Name":"Recipient"}],
        "TemplateID": 123456,
        "TemplateLanguage": true,
        "Subject": "Please confirm your subscription",
        "Variables": {
          "confirm_url": "https://yourapp.com/confirm?token=...signed...",
          "first_name": "Alex"
        }
      }
    ]
  }'

                            
                        

Option B: Inline content (No template)

                            

                                curl -s -X POST \
  --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
  https://api.mailjet.com/v3.1/send \
  -H 'Content-Type: application/json' \
  -d '{
    "Messages":[
      {
        "From":{"Email":"no-reply@yourdomain.com","Name":"Your Brand"},
        "To":[{"Email":"recipient@example.com"}],
        "Subject":"Confirm your subscription",
        "TextPart":"Tap to confirm: {{var:confirm_url}}",
        "HTMLPart":"<p>Almost there! <a href=\"{{var:confirm_url}}\">Confirm your subscription</a>.</p>",
        "TemplateLanguage": true,
        "Variables":{"confirm_url":"https://yourapp.com/confirm?token=..."}
      }
    ]
  }'
                            
                        

Step 4: Handle confirmation clicks

Your confirmation endpoint must securely verify tokens:

                            

                                app.get('/confirm', async (req, res) => {
  const { token } = req.query;

  try {
    // Verify token signature and expiration
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Check if token was already used
    const existingUse = await db.used_tokens.findOne({ 
      token_hash: crypto.createHash('sha256').update(token).digest('hex') 
    });

    if (existingUse) {
      return res.status(400).send('Token already used');
    }

    // Mark token as used
    await db.used_tokens.create({
      token_hash: crypto.createHash('sha256').update(token).digest('hex'),
      used_at: new Date()
    });

    // Proceed with subscription
    await subscribeToMailjet(decoded.email);

    res.send('Subscription confirmed successfully!');

  } catch (error) {
    res.status(400).send('Invalid or expired confirmation link');
  }
});
                            
                        

Step 5: Subscribe to Mailjet list

Once the token is verified, add the contact to your Mailjet list:

Create or Update Contact

                            

                                curl -s -X POST \
  --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
  https://api.mailjet.com/v3/REST/contact \
  -H 'Content-Type: application/json' \
  -d '{
    "Email":"recipient@example.com",
    "Name":"Recipient Name"
  }'

                            
                        

Add to List

                            

                                curl -s -X POST \
  --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
  https://api.mailjet.com/v3/REST/listrecipient \
  -H 'Content-Type: application/json' \
  -d '{
    "ContactAlt":"recipient@example.com",
    "ListID": 123456
  }'

                            
                        

Save Contact Properties

                            

                                curl -s -X PUT \
  --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
  https://api.mailjet.com/v3/REST/contactdata/recipient@example.com \
  -H 'Content-Type: application/json' \
  -d '{
    "Data":[
      {"Name":"first_name","Value":"Alex"},
      {"Name":"city","Value":"Austin"}
    ]
  }'
                            
                        

Note: Define custom contact properties first using the contactmetadata endpoint if they don’t already exist.

Step 6: Send welcome email (Optional)

After successful subscription, send a welcome email message using the same Send API v3.1 pattern from Step 3.

Step 7: Configure webhooks for event tracking

Set up webhooks to receive real-time events for ongoing list hygiene:

                            

                                curl -s -X POST \
  --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
  https://api.mailjet.com/v3/REST/eventcallbackurl \
  -H 'Content-Type: application/json' \
  -d '{
    "EventType": "sent",
    "Url": "https://yourapp.com/webhooks/mailjet"
  }'
                            
                        

Track these events:

  • sent, open, click for engagement metrics
  • bounce, blocked, spam for deliverability issues
  • unsub for compliance and list maintenance

Security and compliance best practices

Here are some of the most common best practices.

Data logging for compliance

Store the following information for GDPR compliance:

  • Form submission timestamp and IP address
  • User agent string
  • Email address entered
  • Confirmation timestamp and IP address
  • Token ID used (never the raw token)

Security recommendations

  • Never use MD5 – Use HMAC-SHA256 or JWT with proper expiration
  • Implement rate limiting to prevent automated signups
  • Add CAPTCHA for additional bot protection
  • Verify your domain with SPF/DKIM records for better deliverability
  • Store token hashes only – never log raw tokens

Troubleshooting and FAQ

Got more questions about this process? Check out our FAQ.

Can I resend confirmation emails?

Yes – create a “resend” endpoint that invalidates the old token and issues a new one with fresh expiration.

What if the contact already exists?

You can re-send confirmations. On click, use the listrecipient endpoint to ensure they’re on the correct list.

Where should I put the token – query string or path?

Query string is typical and easier to parse. Never log raw tokens in analytics.

Should I use Mailjet’s Form Builder instead?

Form Builder is excellent for quick launches. Use this API approach for customized experiences at scale.

Additional resources

With this implementation, you’ll have a robust, secure, and compliant double opt-in system that protects your sender reputation while building a high-quality subscriber base.

D