Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsFlutterFlow API Call

Mailgun API

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.

What you'll learn

  • Why Mailgun's private API key cannot be placed in FlutterFlow API Call headers and how to route it safely through a Cloud Function
  • How to verify a sending domain in Mailgun and add the required DNS records (SPF/DKIM)
  • How to deploy a Firebase Cloud Function that holds the Mailgun key and exposes a simple /send-email endpoint
  • How to configure a FlutterFlow API Call with form-encoded body fields (not JSON) to hit your Cloud Function
  • How to handle Mailgun's common errors — wrong region, unverified domain, and 400 bad request — with proper user feedback
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read45 minutesCommunicationLast updated July 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolMailgun API
CategoryCommunication
MethodFlutterFlow API Call
DifficultyIntermediate
Time required45 minutes
Last updatedJuly 2026

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.

Integration method

FlutterFlow API Call

FlutterFlow sends email by making an API Call to a Firebase Cloud Function (or Supabase Edge Function) that you control. The Cloud Function holds the Mailgun private API key securely and forwards the request to Mailgun's REST API using HTTP Basic authentication with form-encoded fields — not JSON. FlutterFlow never communicates with Mailgun directly, keeping the sending credential out of the compiled app binary.

Prerequisites

  • 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.

Pro tip: Mailgun's sandbox domain (which is automatically created when you sign up) lets you send emails immediately, but only to email addresses you've added as 'Authorized Recipients' in the Mailgun dashboard. It's useful for initial testing but you must verify your own domain before sending to real users.

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
1// index.js (Firebase Cloud Function)
2const functions = require('firebase-functions');
3const axios = require('axios');
4const FormData = require('form-data');
5
6const MAILGUN_API_KEY = functions.config().mailgun.api_key;
7const MAILGUN_DOMAIN = functions.config().mailgun.domain;
8const MAILGUN_FROM = functions.config().mailgun.from;
9const MAILGUN_REGION = functions.config().mailgun.region || 'us';
10const BASE_URL = MAILGUN_REGION === 'eu'
11 ? 'https://api.eu.mailgun.net/v3'
12 : 'https://api.mailgun.net/v3';
13
14exports.sendEmail = functions.https.onRequest(async (req, res) => {
15 // Allow CORS for FlutterFlow web builds
16 res.set('Access-Control-Allow-Origin', '*');
17 if (req.method === 'OPTIONS') {
18 res.set('Access-Control-Allow-Methods', 'POST');
19 res.set('Access-Control-Allow-Headers', 'Content-Type');
20 return res.status(204).send('');
21 }
22
23 const { to, subject, html, text } = req.body;
24 if (!to || !subject || (!html && !text)) {
25 return res.status(400).json({ error: 'Missing required fields: to, subject, html/text' });
26 }
27
28 const form = new FormData();
29 form.append('from', MAILGUN_FROM);
30 form.append('to', to);
31 form.append('subject', subject);
32 if (html) form.append('html', html);
33 if (text) form.append('text', text);
34
35 try {
36 const response = await axios.post(
37 `${BASE_URL}/${MAILGUN_DOMAIN}/messages`,
38 form,
39 {
40 auth: { username: 'api', password: MAILGUN_API_KEY },
41 headers: form.getHeaders()
42 }
43 );
44 return res.status(200).json({ id: response.data.id, message: response.data.message });
45 } catch (err) {
46 const status = err.response?.status || 500;
47 return res.status(status).json({ error: err.response?.data || err.message });
48 }
49});

Pro tip: If you prefer Supabase over Firebase, the same logic works as a Deno Edge Function — replace FormData with the built-in URLSearchParams (for form-encoded) and use Deno.env.get('MAILGUN_API_KEY'). Deno's fetch handles Basic auth via the Authorization header: btoa('api:' + key).

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.

api_group_config.json
1{
2 "group": "Mailgun Proxy",
3 "baseUrl": "https://us-central1-YOUR-PROJECT.cloudfunctions.net",
4 "calls": [
5 {
6 "name": "Send Email",
7 "method": "POST",
8 "path": "/sendEmail",
9 "bodyType": "JSON",
10 "body": {
11 "to": "{{toEmail}}",
12 "subject": "{{subject}}",
13 "html": "{{htmlBody}}"
14 },
15 "variables": [
16 { "name": "toEmail", "type": "String" },
17 { "name": "subject", "type": "String" },
18 { "name": "htmlBody", "type": "String" }
19 ]
20 }
21 ]
22}

Pro tip: If your Cloud Function is behind a custom domain or you've added Firebase App Check, you may need to add an Authorization header to your FlutterFlow API Call. For most setups without App Check, no FlutterFlow auth header is needed — the security comes from the Mailgun key being server-side only.

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.

Pro tip: Add basic input validation before the API Call action: check that the email field is not empty and contains an '@' character using a Conditional Action with a String contains check. Showing an inline error message is better UX than letting Mailgun return a 400.

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.

Pro tip: Mailgun's Logs section (app.mailgun.com → Sending → Logs) shows the full delivery history for every message, including whether it was queued, delivered, bounced, or marked as spam. Check here first when a test email doesn't arrive — it's far more informative than the API response alone.

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.

Common 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.

FlutterFlow Prompt

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

Copy this prompt to try it in FlutterFlow

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.

FlutterFlow Prompt

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

Copy this prompt to try it in FlutterFlow

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.

FlutterFlow Prompt

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.

Copy this prompt to try it in FlutterFlow

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.

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

Alternatives

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire FlutterFlow integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.