# Mailgun API

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

## TL;DR

Connect FlutterFlow to Mailgun by deploying a Firebase Cloud Function that holds your private Mailgun API key and exposes a thin /send-email endpoint. FlutterFlow calls your Cloud Function — never Mailgun directly — so the API key never ships inside your compiled app. The actual Mailgun call is a form-encoded POST to api.mailgun.net/v3/{domain}/messages using HTTP Basic auth.

## Transactional Email in FlutterFlow with Mailgun

Mailgun is one of the most popular transactional email APIs for developers — it's fast to stand up, has excellent deliverability, and the sending call itself is refreshingly simple: a single POST with a handful of form fields. Many FlutterFlow builders reach for Mailgun when they need to send welcome emails, order confirmations, password resets, or notifications from their app.

The one complication is security. Mailgun authenticates via HTTP Basic with your private API key (username `api`, password = your key). This key grants full sending rights on your domain — anyone who gets it can send email as you, rack up bills, and damage your sender reputation. Since FlutterFlow compiles your app to a Flutter binary running on users' devices, any string you put in an API Call header is decompilable. The correct architecture is a Firebase Cloud Function (or Supabase Edge Function) that holds the key in environment variables, accepts a simple JSON payload from FlutterFlow (`{to, subject, html}`), and makes the actual Mailgun call server-side.

One more detail trips up almost every builder: Mailgun's API body is **form-encoded fields**, not a JSON object. In FlutterFlow's API Call editor, set the Body type to 'Form Fields' (not Raw/JSON) and add each field (`from`, `to`, `subject`, `html` or `text`) individually. Sending JSON to Mailgun's messages endpoint will produce a silent 400 or 'no recipient' error. Also note that Mailgun operates on two separate base URLs — US accounts use `api.mailgun.net` and EU accounts use `api.eu.mailgun.net`. Using the wrong one for your account region always returns a 401.

## Before you start

- A Mailgun account (mailgun.com) — a free trial is available that lets you send to authorized addresses only until you verify a domain
- A domain you own with DNS access to add SPF, DKIM, and MX records (Mailgun walks you through this in the dashboard)
- A Firebase project with Cloud Functions enabled on the Blaze pay-as-you-go plan (required for external HTTP calls to Mailgun), OR a Supabase project with Edge Functions
- A FlutterFlow project — API Calls are available on all paid FlutterFlow plans
- Basic familiarity with FlutterFlow's API Calls panel and Action Flow Editor

## Step-by-step guide

### 1. Verify your sending domain in Mailgun and add DNS records

Log in to your Mailgun account at app.mailgun.com. Click 'Sending' → 'Domains' in the left nav, then click 'Add New Domain'. Enter your domain (e.g., `mail.yourapp.com` — Mailgun recommends a subdomain to avoid affecting your main domain's SPF/DKIM). Choose the region: US (api.mailgun.net) or EU (api.eu.mailgun.net). Note this choice — you must use the matching base URL in all subsequent API calls, and the US/EU endpoints are not interchangeable.

After adding the domain, Mailgun displays the DNS records you need to add: two TXT records (SPF and DKIM), and optionally a CNAME for tracking. Go to your DNS provider (Cloudflare, Namecheap, etc.) and add these records exactly as shown. DNS propagation typically takes 5–30 minutes. Return to Mailgun, click 'Verify DNS Settings', and wait for all records to show green checkmarks.

Once verified, find your Mailgun API key: click your account avatar → 'API Keys' → copy the 'Private API key' (starts with `key-`). Also note your 'Sending API Key' — this is a more restricted key you can use if you only need to send. Store this key somewhere secure (like a password manager) — you'll paste it into your Cloud Function's environment variables in the next step, never into FlutterFlow.

**Expected result:** Your domain shows as 'Active' in Mailgun's Sending → Domains panel with green checkmarks on all DNS records. You have your private API key copied.

### 2. Deploy a Cloud Function as your Mailgun proxy

Open your Firebase project, go to Functions, and create a new HTTP Cloud Function named `sendEmail`. This function will receive a JSON POST from FlutterFlow containing `{to, subject, html}`, add the Mailgun credentials, convert the data to form fields, and POST to Mailgun on your behalf. The Mailgun API key is stored in Firebase environment config, not hardcoded.

The function uses `form-data` npm package to build the multipart/form-data body that Mailgun requires. Install it as a dependency by adding it to your functions/package.json (`"form-data": "^4.0.0"`). Use `axios` for the HTTP call.

After writing the function code, deploy it with `firebase deploy --only functions:sendEmail`. Copy the deployed function URL (visible in the Firebase console under Functions, e.g., `https://us-central1-your-project.cloudfunctions.net/sendEmail`). You'll paste this URL as the base URL in your FlutterFlow API Group in Step 3.

Set the Firebase environment config before deploying:
`firebase functions:config:set mailgun.api_key='key-yourprivatekey' mailgun.domain='mail.yourapp.com' mailgun.from='YourApp <hello@yourapp.com>' mailgun.region='us'`

For EU accounts, change the region to 'eu' — the function code will use `api.eu.mailgun.net` when region is 'eu'.

```
// index.js (Firebase Cloud Function)
const functions = require('firebase-functions');
const axios = require('axios');
const FormData = require('form-data');

const MAILGUN_API_KEY = functions.config().mailgun.api_key;
const MAILGUN_DOMAIN = functions.config().mailgun.domain;
const MAILGUN_FROM = functions.config().mailgun.from;
const MAILGUN_REGION = functions.config().mailgun.region || 'us';
const BASE_URL = MAILGUN_REGION === 'eu'
  ? 'https://api.eu.mailgun.net/v3'
  : 'https://api.mailgun.net/v3';

exports.sendEmail = functions.https.onRequest(async (req, res) => {
  // Allow CORS for FlutterFlow web builds
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') {
    res.set('Access-Control-Allow-Methods', 'POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
    return res.status(204).send('');
  }

  const { to, subject, html, text } = req.body;
  if (!to || !subject || (!html && !text)) {
    return res.status(400).json({ error: 'Missing required fields: to, subject, html/text' });
  }

  const form = new FormData();
  form.append('from', MAILGUN_FROM);
  form.append('to', to);
  form.append('subject', subject);
  if (html) form.append('html', html);
  if (text) form.append('text', text);

  try {
    const response = await axios.post(
      `${BASE_URL}/${MAILGUN_DOMAIN}/messages`,
      form,
      {
        auth: { username: 'api', password: MAILGUN_API_KEY },
        headers: form.getHeaders()
      }
    );
    return res.status(200).json({ id: response.data.id, message: response.data.message });
  } catch (err) {
    const status = err.response?.status || 500;
    return res.status(status).json({ error: err.response?.data || err.message });
  }
});
```

**Expected result:** Your Cloud Function is deployed and its URL is live. Test it with a curl command or Postman — send a POST with JSON body {to, subject, html} and confirm you receive an email and a 200 response with a Mailgun message ID.

### 3. Create a FlutterFlow API Group pointing at your Cloud Function

In FlutterFlow, click **API Calls** in the left nav, then **+ Add** → **Create API Group**. Name it 'Mailgun Proxy'. Set the Base URL to your Cloud Function URL from Step 2 (e.g., `https://us-central1-your-project.cloudfunctions.net`). You don't need shared headers on the API Group level since your Cloud Function doesn't require authentication — the Mailgun key is handled server-side.

Click **+ Add API Call** inside the group. Name it 'Send Email'. Set method to **POST**, path to `/sendEmail`. In the Body section, set the body type to **JSON** (since you're calling your Cloud Function, not Mailgun directly — your function accepts JSON and translates to form-data internally). Add body fields:
- `to` (String variable)
- `subject` (String variable)
- `html` (String variable)

Click the **Variables** tab and add three variables: `toEmail`, `subject`, `htmlBody` — all String type. Map them in the body: `"to": "{{toEmail}}"`, `"subject": "{{subject}}"`, `"html": "{{htmlBody}}"`.

Click **Test** and paste sample values for each variable. You should get a 200 response with a Mailgun message ID in the body. If you get an error, check the Cloud Function logs in the Firebase console for the exact Mailgun error message — it's much more informative than the generic error FlutterFlow shows.

```
{
  "group": "Mailgun Proxy",
  "baseUrl": "https://us-central1-YOUR-PROJECT.cloudfunctions.net",
  "calls": [
    {
      "name": "Send Email",
      "method": "POST",
      "path": "/sendEmail",
      "bodyType": "JSON",
      "body": {
        "to": "{{toEmail}}",
        "subject": "{{subject}}",
        "html": "{{htmlBody}}"
      },
      "variables": [
        { "name": "toEmail", "type": "String" },
        { "name": "subject", "type": "String" },
        { "name": "htmlBody", "type": "String" }
      ]
    }
  ]
}
```

**Expected result:** The 'Send Email' API Call appears in your Mailgun Proxy group. Testing it in FlutterFlow returns a 200 with a JSON body containing `id` (the Mailgun message queue ID) and `message: 'Queued. Thank you.'`.

### 4. Wire the API Call to a compose form in your app

Add a compose screen to your FlutterFlow app with the following widgets in a Column: a **TextField** for the recipient email (label 'To'), a **TextField** for the subject line, a **TextField** for the message body (set Multiline to true, Min Lines to 5), and a **Button** labeled 'Send Email'.

Select the Send Email button and open the **Action Flow Editor** (right-click → Actions, or the Actions panel on the right). Click **+ Add Action** → **Backend/API Call**. Select your 'Mailgun Proxy' API Group, then 'Send Email' API Call. In the variable mapping:
- `toEmail` → TextField value for the To field
- `subject` → TextField value for the Subject field
- `htmlBody` → TextField value for the Body field (wrap in `<p>` tags if you want HTML formatting)

After the API Call action, add a **Conditional Action**: check `response.statusCode == 200`. If true: show a SnackBar with 'Email sent!', then clear all three TextFields (Set Value → empty string). If false: show a SnackBar with 'Failed to send — please try again' and optionally log the error to your Supabase/Firestore error log.

If this integration is going into a customer-facing product and you want it built and tested properly, RapidDev's team handles FlutterFlow email integrations every week — free scoping call at rapidevelopers.com/contact.

**Expected result:** The compose screen sends an email when the button is tapped. A success SnackBar appears and the form clears. The email arrives in the recipient's inbox within seconds.

### 5. Parse the Mailgun response and handle errors

Mailgun's API returns a small JSON object on success: `{"id": "<message-id@domain>", "message": "Queued. Thank you."}`. In FlutterFlow's API Call response section, click 'Generate JSON Paths' after a successful test and create paths for `$.id` and `$.message`. You can use the `id` path to store the Mailgun message ID in a local state variable, which is useful for debugging (you can look up that ID in Mailgun's Logs dashboard to see delivery status).

For error handling, the most common errors are:
- **401 Unauthorized**: wrong API key or wrong region base URL (US key hitting EU endpoint). Your Cloud Function will surface this as a 401 from Mailgun.
- **400 Bad Request / 'no recipient'**: usually means the body format was wrong (JSON instead of form-data). Since your Cloud Function handles form-data internally, this should not happen — but double-check the function code uses `FormData`, not raw JSON.
- **'Domain not found'** or **'Sandbox restrictions'**: your domain is still unverified, or you're using the sandbox domain and the recipient isn't on the authorized list.

In FlutterFlow, add a second conditional branch in your Action Flow Editor: if `response.statusCode` is not 200, parse the error body's `$.error` field and show it in a Dialog or SnackBar so the user (or you as the developer) can understand what went wrong. During development, print the full error to FlutterFlow's Debug console via a Debug Message action.

**Expected result:** Successful sends show the Mailgun queue ID in a Debug Message. Failed sends surface a readable error message via a SnackBar. You can look up any message ID in Mailgun's Logs panel to check its delivery status.

## Best practices

- Never put your Mailgun private API key in a FlutterFlow API Call header or Dart code — it grants full send rights on your domain and ships decompilable inside the app binary; always keep it in a Cloud Function environment variable
- Verify your own domain (with SPF and DKIM) before going live — Mailgun's sandbox domain is restricted to authorized recipients only and unsuitable for production
- Always use the correct regional base URL for your Mailgun account (api.mailgun.net for US, api.eu.mailgun.net for EU) — cross-region calls always return 401
- Use Mailgun's Logs panel to debug delivery issues — it shows whether an email was queued, delivered, bounced, or marked as spam, far more detail than the API response
- Add input validation in FlutterFlow before the API Call — check that the email field contains '@' and the subject is non-empty, to avoid wasting Mailgun quota on invalid sends
- Store the Mailgun message ID (`$.id` from the response) in your Firestore or Supabase database alongside the record that triggered the send — useful for debugging and auditing
- Set up Mailgun webhooks (via your Cloud Function endpoint) to track delivery events like bounces and spam complaints — these are critical for maintaining your domain's sender reputation

## Use cases

### Welcome email after user signs up in a FlutterFlow app

A FlutterFlow app using Supabase Auth sends a branded welcome email immediately after a new user registers. The sign-up action triggers a Call Cloud Function action that passes the user's email and first name to the Cloud Function, which assembles a personalized HTML email and sends it via Mailgun.

Prompt example:

```
After a user completes registration, send them a welcome email from hello@yourapp.com with their first name and a link to get started.
```

### Order confirmation email for an e-commerce FlutterFlow app

A FlutterFlow shopping app sends a transactional order confirmation whenever a purchase is completed. The checkout screen triggers the Cloud Function with order details (items, total, delivery address), which generates an HTML receipt and sends it via Mailgun. Mailgun's logs panel shows delivery status for each email.

Prompt example:

```
When an order is placed, send the customer an order confirmation email listing their items and total price.
```

### Contact form submissions routed to the business owner

A FlutterFlow lead-gen app has a contact form. On submission, a Cloud Function sends the form data to the business owner's email via Mailgun and sends a confirmation receipt to the user. Both sends are triggered from a single FlutterFlow API Call action on the submit button.

Prompt example:

```
When a user submits the contact form, email the form contents to admin@business.com and send the user a 'we'll be in touch' confirmation.
```

## Troubleshooting

### Cloud Function call returns 401 from Mailgun even though the API key is correct

Cause: Most likely a region mismatch — the domain is registered in Mailgun's EU region but the Cloud Function is hitting the US base URL (api.mailgun.net) or vice versa.

Solution: Log in to Mailgun, go to Sending → Domains, and check which region your domain is in (shown at the top of the domain detail page). If it says 'EU', your Cloud Function must use `api.eu.mailgun.net/v3` as the base URL. Update your Firebase config (`mailgun.region = 'eu'`) and redeploy. A US API key used against the EU endpoint always returns 401 regardless of key validity.

### Email appears queued in Mailgun logs but never arrives in the inbox

Cause: The sending domain's DNS records (SPF, DKIM) are not fully verified or propagated, causing receiving mail servers to reject or quarantine the message. Also check spam folders.

Solution: Return to Mailgun → Sending → Domains → click your domain → 'Domain Verification & DNS'. Click 'Verify DNS Settings'. All records should show green. If any are red, add the missing records to your DNS provider and wait for propagation (up to 48 hours for some providers). Also check the recipient's spam folder — Mailgun messages from new domains often land there until the domain builds a sending reputation.

### FlutterFlow returns 'XMLHttpRequest error' when testing the API Call in web Run mode

Cause: If you're calling your Cloud Function from FlutterFlow's web Run mode, the browser enforces CORS and your Cloud Function may not be returning the correct CORS headers.

Solution: Ensure your Cloud Function includes CORS headers in every response: `res.set('Access-Control-Allow-Origin', '*')` (for development) or restrict to your actual domain for production. The Cloud Function code example in Step 2 already includes a CORS preflight handler for OPTIONS requests. If still failing, test the Cloud Function URL directly in the browser or Postman to confirm it's live and returning CORS headers.

### Mailgun returns 400 'to parameter is missing' even though the 'to' field looks correct

Cause: If you were calling Mailgun's API directly (bypassing the Cloud Function pattern), this usually means the body was sent as JSON instead of form-encoded. Mailgun's /messages endpoint requires multipart/form-data or application/x-www-form-urlencoded, not application/json.

Solution: If you're using the Cloud Function approach from this tutorial, the function handles form-data internally — check that the function is using the FormData package and that form.getHeaders() is included in the axios call. If you're testing Mailgun directly (not recommended), set the FlutterFlow API Call body type to 'Form Fields', not 'JSON', and add each field (from, to, subject, html) individually as separate form entries.

## Frequently asked questions

### Can I call Mailgun's API directly from FlutterFlow without a Cloud Function?

Technically yes, but it's strongly inadvisable for production. The Mailgun private API key would be stored in your FlutterFlow API Call header, which ships inside the compiled app binary. Anyone who decompiles your APK or IPA can extract the key and send unlimited emails from your domain. For development testing only, you can put the key directly and treat it as disposable — but rotate it immediately and switch to the Cloud Function pattern before going live.

### Mailgun says my message is 'Queued' but I never receive it — what's wrong?

A 'Queued' status means Mailgun accepted the message. The most common reasons for non-delivery are: (1) the message went to the spam folder — check there first; (2) the recipient domain rejected the message due to SPF/DKIM issues — check Mailgun's Logs for a 'Rejected' or 'Failed' status update; (3) you're using the sandbox domain and the recipient isn't on Mailgun's authorized recipients list. Verify your sending domain's DNS records are all green in Mailgun's dashboard.

### How do I send to multiple recipients at once?

Mailgun supports multiple recipients by passing a comma-separated list in the `to` field (e.g., `alice@example.com, bob@example.com`). For bulk sends to many recipients, use Mailgun's Batch Sending feature — send one API call with a `recipient-variables` JSON object containing per-recipient substitution variables. This is more efficient than making one API call per recipient and avoids exposing recipients' addresses to each other.

### Does Mailgun support HTML emails with images?

Yes — pass your full HTML in the `html` field and inline-reference your image URLs. For images hosted externally (your S3 bucket, CDN, etc.), use standard `<img src='https://...'>` tags. Mailgun also supports inline attachments via the `inline` field for images that should display inline in the email. Keep image sizes small for better deliverability — large inline images increase the chance of spam filtering.

### What is Mailgun's pricing in 2026?

Mailgun's pricing changes periodically — check the current plans at mailgun.com/pricing. As of early 2026, Mailgun offered a Foundation plan starting around $35/month for roughly 50,000 emails, with higher volume plans available. A free trial tier lets you test with limited sends to authorized recipients. Always verify current pricing on Mailgun's official site, as rates and plan names are updated regularly.

---

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