> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Send email with Mailgun through Courier

> Connect Mailgun with an API key and domain, track delivery with webhooks, and override fields.

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

## Prerequisites

* [A Mailgun account with a verified sending domain](https://www.mailgun.com/)
* Your Mailgun API key and domain name

## Setup

<Steps>
  <Step title="Create a Mailgun API key">
    In Mailgun, open [API security](https://app.mailgun.com/settings/api_security) and copy a sending key. Your domain is on the Domains page.
  </Step>

  <Step title="Configure in Courier">
    Open the <AppLink href="https://app.courier.com/integrations/catalog/mailgun">Mailgun integration</AppLink> in Courier, enter your API key, domain, and From Address, then save.
  </Step>
</Steps>

<Info>
  EU-region accounts need the host set to `api.eu.mailgun.net` in a per-send config override. The integration settings have no host field.
</Info>

Addressing a recipient and sending are the same on every email provider, so they are documented once: <Doc href="/docs/integrations/email/overview#profile-requirements">profile requirements</Doc> and <Doc href="/docs/integrations/email/overview#send-to-a-recipient">send to a recipient</Doc>.

## Overrides

<Doc href="/docs/send/overrides#how-overrides-work">How overrides work</Doc> covers the two levels and which one wins. <Doc href="/docs/integrations/email/overview#channel-overrides">Email channel overrides</Doc> lists the fields every email provider takes.

Use the `override` object to change the payload Courier sends to Mailgun’s Messages API. This example adds a Mailgun tag.

<CodeGroup>
  ```javascript Node.js highlight={8-19} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        email: "sarah@acme-corp.com",
      },
      providers: {
        mailgun: {
          override: {
            body: {
              "o:tag": "notifications",
            },
            config: {
              apiKey: "<your API Key>",
              domain: "<domain>",
              host: "<host>",
            },
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={8-19} theme={null}
  response = client.send.message(
      message={
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "to": {
              "email": "sarah@acme-corp.com",
          },
          "providers": {
              "mailgun": {
                  "override": {
                      "body": {
                          "o:tag": "notifications",
                      },
                      "config": {
                          "apiKey": "<your API Key>",
                          "domain": "<domain>",
                          "host": "<host>",
                      },
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={11-22} wrap theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
        "to": {
          "email": "sarah@acme-corp.com"
        },
        "providers": {
          "mailgun": {
            "override": {
              "body": {
                "o:tag": "notifications"
              },
              "config": {
                "apiKey": "<your API Key>",
                "domain": "<domain>",
                "host": "<host>"
              }
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={8-19} theme={null}
  response = courier.send_.message(
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        email: "sarah@acme-corp.com"
      },
      providers: {
        mailgun: {
          override: {
            body: {
              "o:tag": "notifications"
            },
            config: {
              apiKey: "<your API Key>",
              domain: "<domain>",
              host: "<host>"
            }
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={10-21} theme={null}
  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				Email: courier.String("sarah@acme-corp.com"),
  			},
  		},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  		Providers: shared.MessageProvidersParam{
  			"mailgun": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"body": map[string]any{
  						"o:tag": "notifications",
  					},
  					"config": map[string]any{
  						"apiKey": "<your API Key>",
  						"domain": "<domain>",
  						"host": "<host>",
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={6-15} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().email("sarah@acme-corp.com").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .providers(MessageProviders.builder()
              .putAdditionalProperty("mailgun", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "body", java.util.Map.of(
                          "o:tag", "notifications"
                      ),
                      "config", java.util.Map.of(
                          "apiKey", "<your API Key>",
                          "domain", "<domain>",
                          "host", "<host>"
                      )
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={8-19} theme={null}
  $response = $client->send->message(
    message: [
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'to' => [
        'email' => 'sarah@acme-corp.com',
      ],
      'providers' => [
        'mailgun' => [
          'override' => [
            'body' => [
              'o:tag' => 'notifications',
            ],
            'config' => [
              'apiKey' => '<your API Key>',
              'domain' => '<domain>',
              'host' => '<host>',
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={9-28} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { Email = "sarah@acme-corp.com" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "mailgun",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "body": {
                              "o:tag": "notifications"
                            },
                            "config": {
                              "apiKey": "<your API Key>",
                              "domain": "<domain>",
                              "host": "<host>"
                            }
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={5} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"email": "sarah@acme-corp.com"}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
    --message.providers '{"mailgun": {"override": {"body": {"o:tag": "notifications"}, "config": {"apiKey": "<your API Key>", "domain": "<domain>", "host": "<host>"}}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my template to sarah@acme-corp.com and add a Mailgun tag.
  ```
</CodeGroup>

Courier replaces the full request body with the contents of `override`.

Set `fromAddress` and other Mailgun config options under `override.config`.

Refer to the [Mailgun API docs](https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/) for supported parameters.

<Info title="EU host">
  To send through Mailgun’s EU region, set `host` to `api.eu.mailgun.net`:

  ```json theme={null}
  "config": {
    "apiKey": "<your API Key>",
    "domain": "<domain>",
    "host": "api.eu.mailgun.net"
  }
  ```
</Info>

## Attachments

Add an `attachments` array to the override. File content must be base64-encoded.

<CodeGroup>
  ```javascript Node.js highlight={13-19} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        email: "sarah@acme-corp.com",
      },
      data: {
        hello: "world",
      },
      providers: {
        mailgun: {
          override: {
            attachments: [
              {
                filename: "billing.pdf",
                contentType: "application/pdf",
                data: "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh",
              },
            ],
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={13-19} theme={null}
  response = client.send.message(
      message={
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "to": {
              "email": "sarah@acme-corp.com",
          },
          "data": {
              "hello": "world",
          },
          "providers": {
              "mailgun": {
                  "override": {
                      "attachments": [
                          {
                              "filename": "billing.pdf",
                              "contentType": "application/pdf",
                              "data": "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh",
                          },
                      ],
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={16-22} wrap theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
        "to": {
          "email": "sarah@acme-corp.com"
        },
        "data": {
          "hello": "world"
        },
        "providers": {
          "mailgun": {
            "override": {
              "attachments": [
                {
                  "filename": "billing.pdf",
                  "contentType": "application/pdf",
                  "data": "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh"
                }
              ]
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={13-19} theme={null}
  response = courier.send_.message(
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        email: "sarah@acme-corp.com"
      },
      data: {
        hello: "world"
      },
      providers: {
        mailgun: {
          override: {
            attachments: [
              {
                filename: "billing.pdf",
                contentType: "application/pdf",
                data: "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh"
              }
            ]
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={15-21} theme={null}
  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				Email: courier.String("sarah@acme-corp.com"),
  			},
  		},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  		Data: map[string]any{
  			"hello": "world",
  		},
  		Providers: shared.MessageProvidersParam{
  			"mailgun": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"attachments": []any{
  						map[string]any{
  							"filename": "billing.pdf",
  							"contentType": "application/pdf",
  							"data": "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh",
  						},
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={10-16} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().email("sarah@acme-corp.com").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .data(JsonValue.from(java.util.Map.of(
              "hello", "world"
          )))
          .providers(MessageProviders.builder()
              .putAdditionalProperty("mailgun", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "attachments", java.util.List.of(
                          java.util.Map.of(
                              "filename", "billing.pdf",
                              "contentType", "application/pdf",
                              "data", "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh"
                          )
                      )
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={13-19} theme={null}
  $response = $client->send->message(
    message: [
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'to' => [
        'email' => 'sarah@acme-corp.com',
      ],
      'data' => [
        'hello' => 'world',
      ],
      'providers' => [
        'mailgun' => [
          'override' => [
            'attachments' => [
              [
                'filename' => 'billing.pdf',
                'contentType' => 'application/pdf',
                'data' => 'Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh',
              ],
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={20-26} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { Email = "sarah@acme-corp.com" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Data = new Dictionary<string, JsonElement>()
          {
              { "hello", JsonSerializer.SerializeToElement("world") },
          },
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "mailgun",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "attachments": [
                              {
                                "filename": "billing.pdf",
                                "contentType": "application/pdf",
                                "data": "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh"
                              }
                            ]
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={6} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"email": "sarah@acme-corp.com"}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
    --message.data '{"hello": "world"}' \
    --message.providers '{"mailgun": {"override": {"attachments": [{"filename": "billing.pdf", "contentType": "application/pdf", "data": "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh"}]}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my template to sarah@acme-corp.com with an attachment through Mailgun.
  ```
</CodeGroup>

## IP allowlisting

Mailgun supports IP allowlists for API access. Courier runs on AWS and doesn’t use fixed outbound IPs.

Subscribe to the [`AmazonIpSpaceChanged`](https://docs.aws.amazon.com/general/latest/gr/aws-ip-ranges.html#subscribe-notifications) SNS topic. AWS notifies you whenever its IP ranges change, so you can update your allowlist.

## Delivery tracking

Courier does not poll Mailgun unless you turn on **Enable polling for status updates** in the integration settings. Without polling or a webhook, messages stay at `SENT` instead of advancing to `DELIVERED`. Webhooks are better, because Mailgun reports status to Courier in real time.

<Steps>
  <Step title="Copy the Courier webhook URL">
    In Courier, go to the <AppLink href="https://app.courier.com/integrations/catalog/mailgun">Mailgun configuration page</AppLink>. Copy the generated Webhook URL.

    <Frame>
      <img src="https://mintcdn.com/courier-4f1f25dc/xDJPBS8EQ58X_0-v/assets/mailgun-webhook.webp?fit=max&auto=format&n=xDJPBS8EQ58X_0-v&q=85&s=f4f8f9638f1c1004e26b38ae275d0ce7" width="1322" height="1466" data-path="assets/mailgun-webhook.webp" />
    </Frame>
  </Step>

  <Step title="Configure webhooks in Mailgun">
    In Mailgun, go to **Sending → Webhooks**.\
    Add a webhook for **Delivered Messages** and paste the URL.\
    Repeat for **Permanent Failure**.

    <Frame>
      <img src="https://mintcdn.com/courier-4f1f25dc/xDJPBS8EQ58X_0-v/assets/mailgun-ui.webp?fit=max&auto=format&n=xDJPBS8EQ58X_0-v&q=85&s=5dbd7fe8d6adb1c1c8b0fb6092096853" width="2131" height="670" data-path="assets/mailgun-ui.webp" />
    </Frame>

    <Frame>
      <img src="https://mintcdn.com/courier-4f1f25dc/xDJPBS8EQ58X_0-v/assets/mailgun-form.webp?fit=max&auto=format&n=xDJPBS8EQ58X_0-v&q=85&s=a3a810891716c6f9b7cbf28eb78be46d" width="1200" height="782" data-path="assets/mailgun-form.webp" />
    </Frame>
  </Step>

  <Step title="Match the domain">
    In Mailgun, make sure the selected domain matches the one in Courier.
  </Step>

  <Step title="Disable polling (optional)">
    Wait \~1 hour so in-flight updates land. Then switch off **Enable polling for status updates** in Courier and click **Save**.
  </Step>
</Steps>

## Troubleshooting

<Accordion title="550 error, missing MX record">
  <Warning>
    Mailgun returns a 550 error when the sending domain has no MX record.
  </Warning>

  **Fix:**\
  Add an MX record to your domain’s DNS. Wait \~2 hours for propagation.
</Accordion>

<Accordion title="Account throttling or probation">
  <Warning>
    Mailgun may throttle or suspend delivery for accounts with high bounce/spam rates or traffic spikes.
  </Warning>

  **Fix:**

  1. Complete Mailgun’s Business Verification.
  2. Remove addresses that bounce consistently. Avoid bulk sends to unverified users.
</Accordion>

## Provider details

```text theme={null}
mailgun
```

Courier recommends routing to the channel. Naming this key in `routing.channels` instead is supported, and sends through just this provider.

<Card title="Send to a specific provider" icon="bullseye-arrow" href="/docs/send/send-to-a-provider" horizontal arrow="true">
  When that is worth doing, and what you give up: failover, channel priority, and providers you add later.
</Card>
