# Mailjet

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Mailjet by routing email sends through a Firebase Cloud Function that holds your Mailjet API Key and Secret Key and calls the Mailjet Send API v3.1. The key integration detail: v3.1 uses capitalized JSON keys (Messages, From, To, Subject) — wrong casing causes 400 errors. Mailjet's free tier allows 6,000 emails per month (200 per day). Your FlutterFlow app calls the proxy function with recipient, subject, and body — no credentials in the app.

## Mailjet v3.1 API and the Capitalized-Key Trap

Mailjet is one of the cleaner transactional email APIs available to FlutterFlow developers — a single `POST https://api.mailjet.com/v3.1/send` with Basic auth and a JSON body is all it takes to send an email. The free tier includes 6,000 emails per month (200 per day), which covers most early-stage app needs without any cost. For higher volumes, paid plans remove the daily cap and add sending analytics, bounce management, and A/B testing.

The most common failure for first-time Mailjet integrations is the JSON key casing. Mailjet v3.1 uses a strictly capitalized schema: `Messages` (not `messages`), `From` (not `from`), `To` (not `to`), `Subject` (not `subject`), `TextPart` (not `text` or `text_part`), and `HTMLPart` (not `html` or `html_part`). Using lowercase keys returns a `400 Bad Request` with a message like `"Error: These mandatory properties are missing: Messages"` — even though you've included them. The code example in Step 2 shows the exact v3.1 structure.

The second consideration is security. Mailjet authenticates with HTTP Basic auth using your API Key and Secret Key. These are account-level secrets — a leaked Secret Key gives full access to your Mailjet account, including sending unlimited emails (burning your quota or billing), accessing contact lists, and modifying domain settings. Because FlutterFlow compiles to a Flutter client app, any credentials added to an API Call header ship inside the app binary. A Firebase Cloud Function is the secure home for both keys.

## Before you start

- A Mailjet account at app.mailjet.com — the free tier is sufficient to start
- A verified sender email address or domain in Mailjet (Settings → Sender domains and addresses)
- A Firebase project on the Blaze (pay-as-you-go) plan with Cloud Functions enabled — required for outbound HTTP calls to Mailjet
- A FlutterFlow project with at least one screen containing a form or trigger point for email sending
- Basic Node.js familiarity to edit and deploy the Cloud Function (no advanced knowledge needed)

## Step-by-step guide

### 1. Get your Mailjet API Key and Secret Key, and verify your sender domain

Log in to your Mailjet account at app.mailjet.com. Navigate to the top-right account menu → **API Key Management** (or go to Account Settings → REST API). You'll see your **API Key** (a long alphanumeric string) and a **Secret Key** revealed when you click 'View'. Copy both into a password manager — the Secret Key is the higher-sensitivity credential.

Next, verify your sender domain so Mailjet accepts your sends and email providers trust them. In the Mailjet dashboard, go to **Account Settings** → **Sender domains and addresses** → **Add a Domain**. Enter your domain (e.g., `yourdomain.com`) and follow the DNS verification steps: Mailjet will give you SPF and DKIM DNS records to add through your domain registrar (Cloudflare, GoDaddy, Namecheap, etc.). Verification typically completes within 15-60 minutes after you save the DNS records.

If you don't have a custom domain yet, you can verify a single email address instead (Settings → Sender domains and addresses → Add a Sender Address) — Mailjet sends you a confirmation link. Verified addresses work for testing but aren't suitable for production volumes. For any real app, a verified domain is strongly recommended for deliverability.

Do not copy the API Key or Secret Key anywhere in your FlutterFlow project — they go only into your Firebase Function environment config in the next step.

**Expected result:** Your Mailjet API Key and Secret Key are saved securely, and your sender domain or email address shows 'Verified' status in Mailjet's Sender domains and addresses panel.

### 2. Deploy a Firebase Cloud Function that proxies Mailjet sends

Open the Firebase Console for your project and confirm it's on the Blaze pay-as-you-go plan (the free Spark plan blocks outbound HTTP, which is required to reach api.mailjet.com). Navigate to Functions.

In your local `functions/` directory, add `axios` to `package.json` dependencies: `"axios": "^1.6.0"`. Then store your Mailjet credentials as Firebase environment config:
```
firebase functions:config:set mailjet.api_key="YOUR_API_KEY" mailjet.secret_key="YOUR_SECRET_KEY" mailjet.from_email="noreply@yourdomain.com" mailjet.from_name="Your App"
```

Create the Cloud Function shown below. It builds the Basic auth string by base64-encoding `API_KEY:SECRET_KEY` (the standard format for HTTP Basic auth), then POSTs to `https://api.mailjet.com/v3.1/send` with the exact capitalized v3.1 JSON structure.

Pay close attention to the JSON body shape in the code: `Messages` is a capitalized array; each message object has `From` (object with `Email` and `Name`), `To` (array of objects with `Email` and `Name`), `Subject` (string), `TextPart` (string), and optionally `HTMLPart` (string). All keys in the v3.1 schema are capitalized — any lowercase key is silently ignored and the corresponding field is treated as missing, resulting in a 400.

After deploying with `firebase deploy --only functions`, copy the HTTPS trigger URL. Test it with Hoppscotch or Postman before wiring FlutterFlow: POST to the function URL with a JSON body `{"to":"yourtest@email.com","subject":"Test","body":"Hello from Mailjet!"}` and confirm you receive the email.

```
// functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');

admin.initializeApp();
const db = admin.firestore();

const API_KEY = functions.config().mailjet.api_key;
const SECRET_KEY = functions.config().mailjet.secret_key;
const FROM_EMAIL = functions.config().mailjet.from_email;
const FROM_NAME = functions.config().mailjet.from_name;

const MAILJET_URL = 'https://api.mailjet.com/v3.1/send';
const AUTH = Buffer.from(`${API_KEY}:${SECRET_KEY}`).toString('base64');

exports.mailjetSend = functions.https.onRequest(async (req, res) => {
  if (req.method !== 'POST') return res.status(405).send('Method Not Allowed');

  const { to, subject, body, htmlBody, toName } = req.body;
  if (!to || !subject || !body) {
    return res.status(400).json({ error: 'to, subject, and body are required' });
  }

  // CRITICAL: Mailjet v3.1 uses capitalized keys
  const payload = {
    Messages: [
      {
        From: { Email: FROM_EMAIL, Name: FROM_NAME },
        To: [{ Email: to, Name: toName || to }],
        Subject: subject,
        TextPart: body,
        HTMLPart: htmlBody || undefined
      }
    ]
  };

  try {
    const response = await axios.post(MAILJET_URL, payload, {
      headers: {
        Authorization: `Basic ${AUTH}`,
        'Content-Type': 'application/json'
      }
    });
    const sent = response.data.Messages[0];
    // Log to Firestore
    await db.collection('mailjet_sent').add({
      to, subject,
      messageId: sent.To[0].MessageID,
      status: sent.Status,
      sentAt: admin.firestore.FieldValue.serverTimestamp()
    });
    return res.status(200).json({ messageId: sent.To[0].MessageID, status: sent.Status });
  } catch (err) {
    const detail = err.response ? err.response.data : err.message;
    return res.status(500).json({ error: detail });
  }
});
```

**Expected result:** The Cloud Function deploys successfully, and a Postman/Hoppscotch test returns `{"messageId":"...","status":"success"}` with the email arriving in your inbox within seconds.

### 3. Create the FlutterFlow API Call pointing at your Cloud Function

In the FlutterFlow editor, click **API Calls** in the left navigation panel. Click **+ Add** → **Create API Group**. Name the group `MailjetProxy` and set the Base URL to the base of your Firebase Functions URL (e.g., `https://us-central1-yourproject.cloudfunctions.net`). Leave all headers empty — no auth headers belong here.

Inside the group, click **+ Add API Call** and name it `SendEmail`. Set the method to `POST` and the endpoint to `/mailjetSend` (matching the function export name exactly — case-sensitive). Click the **Variables** tab and add four variables:
- `to` (String) — recipient email address
- `subject` (String) — email subject line
- `body` (String) — plain text message
- `htmlBody` (String, optional, default `""`)

In the **Body** section, select **JSON** as the content type and enter:
```json
{"to":"{{to}}","subject":"{{subject}}","body":"{{body}}","htmlBody":"{{htmlBody}}"}
```

Switch to the **Response & Test** tab and paste a sample success response:
```json
{"messageId":"1234567890","status":"success"}
```
Click **Generate JSON Paths**. FlutterFlow will create `$.messageId` and `$.status` paths that you can bind in action chains to show confirmation messaging.

Click **Test API Call**: fill in a real recipient address for `to`, write a test subject and body, leave `htmlBody` empty, then click **Test**. If your Cloud Function is running correctly, you'll see a `200` response with a messageId and receive the email within seconds. If you get a `400`, inspect the error — the most common cause at this stage is a wrong function name in the endpoint path.

```
// FlutterFlow API Call configuration
{
  "group_name": "MailjetProxy",
  "base_url": "https://us-central1-yourproject.cloudfunctions.net",
  "calls": [
    {
      "name": "SendEmail",
      "method": "POST",
      "endpoint": "/mailjetSend",
      "headers": {},
      "body_type": "JSON",
      "body": {
        "to": "{{to}}",
        "subject": "{{subject}}",
        "body": "{{body}}",
        "htmlBody": "{{htmlBody}}"
      },
      "variables": [
        { "name": "to", "type": "String" },
        { "name": "subject", "type": "String" },
        { "name": "body", "type": "String" },
        { "name": "htmlBody", "type": "String", "default": "" }
      ]
    }
  ]
}
```

**Expected result:** The FlutterFlow API Call test returns status 200 with a messageId, and the test email arrives in your inbox — the proxy pipeline is confirmed end-to-end.

### 4. Wire the send action to a form button and add success/error feedback

With the API Call configured, connect it to your app's UI. Open the screen where email sending should trigger — a contact form, an order confirmation, a support request screen, or any form that collects recipient and content data.

Ensure you have these widgets: a `TextField` for the recipient's email (optional if you're always sending to the app owner), a `TextField` for the subject, a multi-line `TextField` for the message body, and a `Button` labeled 'Send Email'.

Select the **Button**, open the **Actions** panel, and click **+ Add Action**. Choose **Backend/API Call** → **MailjetProxy → SendEmail**. In the variable bindings:
- `to`: bind to the recipient TextField's current value, or hard-code the app owner's email if this is a contact-to-owner form
- `subject`: bind to the subject TextField's current value, or use a static string like `"New contact form submission"`
- `body`: bind to the message body TextField's current value
- `htmlBody`: bind to an empty string (or skip this field for plain-text-only forms)

Add a second **chained action** immediately after the API Call: **Show Snack Bar** with conditional text. In the Condition field, check if the API response `$.status` equals `"success"` — if true, show 'Email sent successfully!'; if false, show 'Something went wrong — please try again.'

Add an optional third action after a successful send: **Clear TextField** (reset all form fields to empty) so users can see the form is ready for a new submission. This small UX detail significantly reduces 'did it send?' confusion.

For the free Mailjet tier with a 200 emails/day cap, consider adding a Firestore write before the API Call that increments a daily send counter, and a Conditional Action that checks the counter before allowing the send.

**Expected result:** Tapping the Send button calls the Cloud Function, shows a success Snack Bar, clears the form fields, and the email appears in the recipient's inbox within seconds.

### 5. Add CORS support and handle Mailjet event webhooks for delivery tracking

If your FlutterFlow app publishes as a web app (or you're testing in FlutterFlow's browser-based Run mode), the Cloud Function needs CORS headers. Browser-based requests include an `Origin` header, and the browser blocks responses without `Access-Control-Allow-Origin`. Native iOS and Android builds do not require this.

Update `functions/package.json` to add `"cors": "^2.8.5"` as a dependency, then wrap the function handler as shown in the code below. After redeploying, the FlutterFlow web preview will work without CORS errors.

For delivery tracking — opens, clicks, bounces, spam complaints — Mailjet can push event notifications to a webhook endpoint. This is optional but valuable for production apps. In the Mailjet dashboard, go to **Account Settings** → **Event Tracking (Triggers)** → **Add a new event URL**. Enter your Cloud Function webhook handler URL (a second function export that receives event data and writes to Firestore). Your FlutterFlow app can then display delivery status alongside sent messages using a real-time Firestore query.

The free Mailjet tier tracks opens and clicks, and sends event webhooks at no extra cost. The Mailjet `MessageID` stored in Firestore after each send is the correlation key — use it to match event notifications to the original sent-message document.

If the Cloud Function setup feels complex, RapidDev's team can handle the full Mailjet + FlutterFlow integration, including webhook-based delivery tracking — book a free scoping call at rapidevelopers.com/contact.

```
// Updated index.js with CORS + optional webhook handler
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');
const cors = require('cors')({ origin: true });

admin.initializeApp();
const db = admin.firestore();

const API_KEY = functions.config().mailjet.api_key;
const SECRET_KEY = functions.config().mailjet.secret_key;
const FROM_EMAIL = functions.config().mailjet.from_email;
const FROM_NAME = functions.config().mailjet.from_name;
const AUTH = Buffer.from(`${API_KEY}:${SECRET_KEY}`).toString('base64');

exports.mailjetSend = functions.https.onRequest((req, res) => {
  cors(req, res, async () => {
    if (req.method !== 'POST') return res.status(405).send('Method Not Allowed');
    const { to, subject, body, htmlBody, toName } = req.body;
    if (!to || !subject || !body) {
      return res.status(400).json({ error: 'to, subject, and body are required' });
    }
    const payload = {
      Messages: [{
        From: { Email: FROM_EMAIL, Name: FROM_NAME },
        To: [{ Email: to, Name: toName || to }],
        Subject: subject,
        TextPart: body,
        HTMLPart: htmlBody || undefined
      }]
    };
    try {
      const response = await axios.post('https://api.mailjet.com/v3.1/send', payload, {
        headers: { Authorization: `Basic ${AUTH}`, 'Content-Type': 'application/json' }
      });
      const sent = response.data.Messages[0];
      await db.collection('mailjet_sent').add({
        to, subject,
        messageId: sent.To[0].MessageID,
        status: sent.Status,
        sentAt: admin.firestore.FieldValue.serverTimestamp()
      });
      return res.status(200).json({ messageId: sent.To[0].MessageID, status: sent.Status });
    } catch (err) {
      const detail = err.response ? err.response.data : err.message;
      return res.status(500).json({ error: detail });
    }
  });
});

// Optional: Mailjet event webhook handler
exports.mailjetEvents = functions.https.onRequest(async (req, res) => {
  const events = Array.isArray(req.body) ? req.body : [req.body];
  for (const event of events) {
    const { MessageID, event: eventType } = event;
    if (MessageID) {
      const snap = await db.collection('mailjet_sent')
        .where('messageId', '==', String(MessageID)).limit(1).get();
      if (!snap.empty) {
        await snap.docs[0].ref.update({ deliveryStatus: eventType });
      }
    }
  }
  res.status(200).send('ok');
});
```

**Expected result:** FlutterFlow web preview works without CORS errors; Mailjet event webhooks update the `deliveryStatus` field in Firestore documents, enabling real-time delivery status display in your app.

## Best practices

- Never place your Mailjet API Key or Secret Key in a FlutterFlow API Call header — both are account-level secrets that compile into the app binary. Always keep them in Firebase Cloud Function environment config.
- Use Mailjet v3.1 capitalized JSON keys exactly: Messages, From, To, Subject, TextPart, HTMLPart. Lowercase variants are silently ignored and result in 400 errors that look like missing fields.
- Verify your sender domain (SPF, DKIM, DMARC) before sending any production email. Unverified domains are deliverability risks — emails may go to spam or be rejected silently.
- Log the Mailjet `MessageID` to Firestore after every successful send. This is your correlation key for delivery receipts, bounce events, and spam complaints from Mailjet's event webhooks.
- Monitor the free tier's 200 emails/day cap in production. Add a Firestore counter that tracks daily sends and shows a warning (or blocks the form) when approaching the limit.
- Set `HTMLPart` to `undefined` (not an empty string) when sending plain-text-only emails. An empty `HTMLPart` string triggers Mailjet validation errors; omitting it entirely sends cleanly.
- Add CORS support to the Cloud Function if you're building a web app or testing in FlutterFlow's web preview. Browser contexts enforce CORS; native iOS/Android builds do not.
- For transactional emails (order confirmations, receipts), always include a plain-text `TextPart` alongside `HTMLPart`. Some email clients render only plain text, and having both improves deliverability scores.

## Use cases

### User sign-up welcome email

When a new user registers in a FlutterFlow app backed by Firebase Auth, a triggered Cloud Function sends a personalized Mailjet welcome email with the user's name, a getting-started guide link, and any onboarding steps. The email is sent from the product's verified domain address rather than a generic service address.

Prompt example:

```
After a user completes registration, send a welcome email with their first name, the app name, and a button linking to the getting-started guide. Use the company's verified Mailjet sender domain.
```

### Contact form delivery to the app owner

A service or portfolio app built in FlutterFlow includes a contact form. On submission, the form data (name, email, message) is sent via the Mailjet Cloud Function to the owner's email address. A confirmation email is also sent back to the user's provided address — both in a single Mailjet API call using the `Messages` array.

Prompt example:

```
Build a contact form with Name, Email, and Message fields. On submit, send the message to the owner's address and a confirmation to the user's email — both via Mailjet in a single API call.
```

### Order confirmation for an e-commerce app

After a successful Stripe payment in a FlutterFlow shopping app, a Cloud Function sends a Mailjet order confirmation email with the order ID, itemised list, total, and estimated delivery date. The email uses an HTMLPart template stored in the Cloud Function for a polished brand experience.

Prompt example:

```
When a Stripe payment_intent.succeeded webhook fires, trigger a Mailjet email to the customer with their order summary, total amount, and estimated delivery date formatted in a clean HTML layout.
```

## Troubleshooting

### Mailjet API returns 400 Bad Request with 'These mandatory properties are missing: Messages'

Cause: The JSON payload uses lowercase keys (e.g., `messages`, `from`, `to`, `subject`) instead of the capitalized keys required by Mailjet v3.1 (`Messages`, `From`, `To`, `Subject`, `TextPart`).

Solution: Check your Cloud Function's payload object and ensure every key exactly matches the v3.1 schema: `Messages` (array), `From` (object), `To` (array of objects), `Subject` (string), `TextPart` (string), `HTMLPart` (string, optional). JavaScript object keys are case-sensitive — copy the structure from the code in Step 2 exactly.

```
// WRONG (lowercase keys)
{ "messages": [{ "from": { "email": "..." }, "to": [...], "subject": "..." }] }

// CORRECT (v3.1 capitalized keys)
{ "Messages": [{ "From": { "Email": "..." }, "To": [...], "Subject": "..." }] }
```

### Cloud Function returns 401 Unauthorized when calling Mailjet

Cause: The Basic auth credentials are incorrect — either the API Key or Secret Key in Firebase config is wrong, or the base64 encoding was done incorrectly.

Solution: Verify the values in Firebase config: in the Firebase Console, go to Functions → Configuration and check `mailjet.api_key` and `mailjet.secret_key`. Compare them to the values in Mailjet's API Key Management page (account menu → API Key Management). If incorrect, update with `firebase functions:config:set mailjet.api_key="..." mailjet.secret_key="..."` and redeploy. Also confirm the auth string is built as `Buffer.from(API_KEY + ':' + SECRET_KEY).toString('base64')` — the colon separator between the two values is required.

### Emails are sent (200 response) but never arrive in the recipient's inbox

Cause: The sender address or domain is not verified in Mailjet, so sends are being silently dropped or rejected by receiving mail servers. Mailjet may still report status 'success' while the email never leaves their system.

Solution: In the Mailjet dashboard, go to Settings → Sender domains and addresses and confirm your sender address/domain shows 'Verified'. If it shows 'Not verified' or 'Pending', follow the DNS verification steps (add SPF and DKIM records through your domain registrar). Also check the Mailjet event log (Dashboard → Transactional → Email → Logs) for bounced or blocked messages with error details.

### XMLHttpRequest error in FlutterFlow's web preview or Run mode

Cause: The Firebase Cloud Function is not returning CORS headers. FlutterFlow's web-based preview sends HTTP requests from a browser context, which requires the server to include `Access-Control-Allow-Origin` in the response.

Solution: Add the `cors` npm package to `functions/package.json` and wrap the function handler with `cors({ origin: true })` middleware as shown in Step 5. Redeploy. This issue only affects web builds — native iOS and Android FlutterFlow apps do not enforce CORS.

```
const cors = require('cors')({ origin: true });
exports.mailjetSend = functions.https.onRequest((req, res) => {
  cors(req, res, async () => {
    // ... existing handler ...
  });
});
```

## Frequently asked questions

### Does the free Mailjet tier work for a production FlutterFlow app?

Yes, for low-volume use cases. The free tier allows 6,000 emails per month with a cap of 200 per day. For a contact form, onboarding flow, or order confirmation system that doesn't send hundreds of emails per day, the free tier is sufficient. When you exceed 200 emails in a day, Mailjet queues or rejects the overflow — monitor your send logs to know when to upgrade.

### Why is my Mailjet send returning 400 even though I included all the fields?

Almost certainly a casing issue. Mailjet v3.1's JSON schema uses capitalized keys: `Messages`, `From`, `To`, `Subject`, `TextPart`, `HTMLPart`. If your payload uses `messages`, `from`, `to`, or `subject` (lowercase), Mailjet ignores those fields entirely and reports them as missing. Check the exact payload your Cloud Function is building using `console.log(JSON.stringify(payload))` in the function code, then compare key-by-key against the v3.1 schema.

### Can I send to multiple recipients in one Mailjet API call?

Yes. The `To` field in each Messages object accepts an array of recipient objects: `[{"Email":"alice@example.com","Name":"Alice"},{"Email":"bob@example.com","Name":"Bob"}]`. You can also send individual messages to different recipients in one request by adding multiple objects to the `Messages` array — each with its own `To`, `Subject`, and body. This is more efficient than making one Cloud Function call per recipient.

### Will my sender address show in the recipient's inbox as spam?

Not if your domain's DNS records are properly configured. Add the SPF and DKIM records that Mailjet provides in Settings → Sender domains and addresses, and optionally a DMARC policy. These records cryptographically verify that Mailjet is authorized to send on behalf of your domain, which is how receiving mail servers distinguish legitimate email from spoofed spam. Without them, even perfectly formatted emails may land in spam.

### Can I track whether recipients opened or clicked links in emails sent through Mailjet?

Yes. Mailjet automatically adds open-tracking pixels and click-tracking redirects when you send HTML emails. You can view aggregate stats in the Mailjet dashboard (Transactional → Statistics) or receive real-time event notifications via Mailjet webhooks. Configure an event webhook URL (your `mailjetEvents` Cloud Function) in Account Settings → Event Tracking — Mailjet will POST open, click, bounce, and spam events to your function, which can update Firestore documents for in-app display.

### Can FlutterFlow call Mailjet's API directly without a Cloud Function?

Technically yes, but you should not do this. Placing your API Key and Secret Key in a FlutterFlow API Call header means they are compiled into the app binary, where they can be extracted from any downloaded app. With your keys, someone can send unlimited emails from your Mailjet account, access your contact lists, and exhaust your quota. The Cloud Function proxy adds about 30 minutes of setup and eliminates this risk entirely.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/mailjet
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/mailjet
