Library / SDK
mailgun/mailgun-php avatar
mailgun/mailgun-php

mailgun-php is a PSR-18 wrapper, and the HTTP client is yours to choose

Mailgun's Official SDK for PHP

1,136 stars315 forksPHPMIT

At a glance

What is it?
The Mailgun PHP SDK is the official client for the Mailgun API, built as a thin wrapper over a PSR-18 HTTP client and a PSR-7/PSR-17 implementation, with resource-oriented accessors for messages, IPs, pools, analytics and subaccounts. It fits a PHP application that sends transactional mail and manages its sending infrastructure, and it does not fit a team that wants no Mailgun-specific code in their codebase.
Who is it for?
mailgun-php fits a PHP application that sends transactional email through Mailgun and wants a typed, PSR-compliant client rather than hand-rolled HTTP calls, and it does not fit a project that needs to stay provider-neutral, since the API surface is Mailgun-specific even though the transport is not.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 51 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The SDK is a wrapper, and the transport is a separate decision

The Mailgun PHP SDK is described as a clean, PSR-18 HTTP client wrapper around the Mailgun API. That single phrase describes the whole architecture, and the requirements section makes it explicit.

You need PHP 7.4 or higher, a PSR-18 HTTP client such as symfony/http-client or guzzlehttp/guzzle, and a PSR-7 and PSR-17 implementation such as nyholm/psr7. The SDK is not coupled to any specific HTTP library, and the instruction is to bring your own PSR-18 client.

That separation is the design decision worth understanding. The SDK does not own a transport, a retry policy, a connection pool or a logging stack. It builds PSR-7 requests, hands them to whatever client you injected, and parses PSR-7 responses into value objects. A team that already has a configured HTTP client with its own timeouts, middleware and instrumentation keeps all of that, and the SDK adds only the Mailgun domain knowledge on top.

The cost of that choice is that the SDK's own error handling and retry behaviour are not in your hands either, so you have to decide separately what happens when a send fails. There is a Response Handling section and a Debugging section in the table of contents, which is where that is covered.

The project is MIT licensed, the last push was on 2026-09-02 with v4.5.2 released the same day, after v4.5.1 in June and v4.5.0 in April 2026.

Region is fixed when the client is built, not per request

The quick start is short, and one detail in it is a real operational decision.

php
require 'vendor/autoload.php';

use Mailgun\Mailgun;

// US servers (default)
$mg = Mailgun::create('your-api-key');

// EU servers
$mg = Mailgun::create('your-api-key', 'https://api.eu.mailgun.net');

The EU endpoint is a second argument to Mailgun::create, and the installation section repeats the point with a note for EU region users. That means region is a property of the client instance, not a per-call parameter, so a single application that needs to talk to both regions needs two client objects, and any code that builds the client from configuration has to decide once which region it is bound to.

There is a second constraint on every call, and it is a common source of confusion for new users. The $domain you pass to any API call must match a domain configured in app.mailgun.com. The SDK does not create the sending domain for you, and it does not validate the domain against your account before making the call, so a mismatch shows up as an API error rather than a configuration error.

Installation is a single Composer command that pulls in the SDK and a working HTTP stack:

bash
composer require mailgun/mailgun-php symfony/http-client nyholm/psr7

The two extra packages are what make the SDK runnable out of the box, since the SDK itself provides neither.

Messages are sent as an options array, including attachments and tracking

The sending API is the part every integration uses, and the shape is an options array keyed by Mailgun's own field names.

A simple message is a from, a to, a subject, a text body and an html body:

php
$mg->messages()->send('example.com', [
    'from'    => 'Alice <[email protected]>',
    'to'      => '[email protected]',
    'subject' => 'Hello from Mailgun!',
    'text'    => 'This is a plain-text body.',
    'html'    => '<p>This is an <strong>HTML</strong> body.</p>',
]);

The array keys are the API's field names rather than PHP property names, and the ones prefixed with o: are Mailgun's options namespace. The extended example shows how they combine:

php
$mg->messages()->send('example.com', [
    'from'       => '[email protected]',
    'to'         => ['[email protected]', '[email protected]'],
    'subject'    => 'Monthly report',
    'text'       => 'Please find the report attached.',
    'attachment' => [['filePath' => '/tmp/report.pdf', 'filename' => 'report.pdf']],
    'o:tracking' => 'yes',
    'o:tag'      => ['monthly', 'report'],
]);

The recipient is an array in the second example, so multiple recipients are one call rather than a loop. Attachments are arrays of arrays with a filePath and a filename, and the filename is what the recipient sees rather than the path on your server, which matters when your temp files have names that would confuse a reader.

Scheduled delivery uses the same call with o:deliverytime, whose value in the example is 'tomorrow 9am UTC'. The natural-language form is convenient, and it is also a value the SDK hands straight to the API, so the timezone in that string is significant.

This is worth noting as a design consequence: because the transport is generic and the payload is Mailgun's own field vocabulary, the SDK does not normalise Mailgun's options into a neutral abstraction. The o: prefixed keys are visible in your application code, which keeps the SDK thin and honest at the cost of a provider-specific shape in your codebase.

IP management and dynamic pools are first-class resources

The largest part of the SDK's surface is IP management, which tells you what kind of application this is aimed at. This is not a transactional email wrapper, it is a client for managing a sending infrastructure.

Listing account IPs is a single call, and the result is iterable:

php
$response = $mg->ips()->index();

foreach ($response->getItems() as $ip) {
    echo $ip . PHP_EOL;
}

The same method takes a dedicated flag, and there is a second, more detailed variant that returns pool assignments, subaccount ownership and timestamps, filterable by pool_id and subaccount_id with a limit. The detailed items are arrays, so the field access is different from the flat list, which is a small inconsistency worth knowing about.

Single IP inspection returns an object with named getters, getIp, getRdns and getDedicated, and there are domain-scoped operations: assign an IP to a domain, unassign it, and list the IPs currently assigned to a domain. Then there are the account-wide asynchronous operations, assignIpToAllDomains and removeIpFromAllDomains, the first of which returns a reference you track with getReferenceId, the second of which takes an alternative IP to swap in.

Dynamic IP Pools, abbreviated DIPP, are the reason much of this exists. They let you group dedicated IPs and link them to domains so sending traffic is spread across all IPs in the pool automatically. Pools are created with a name and description, and a single updateIpPool call can modify metadata, add and remove IPs and link and unlink domains at once. The README notes that when a pool is linked to domains, adding or removing IPs propagates to all linked domains asynchronously after the API responds, which is the kind of eventual consistency that a send path has to tolerate.

The remaining API surface, and the framework integration section

The table of contents lists the rest of the client's domains: Analytics, Subaccounts, Response Handling, Debugging and Framework Integration, plus the sending and IP sections already covered.

Analytics and Subaccounts are the two that a growth-stage team tends to need next. Subaccounts in particular matter for anyone serving multiple customers from one Mailgun account, and the fact that the IP methods take a subaccount_id filter shows that the two features are related rather than separate.

Response Handling and Debugging are the sections that decide how much work you will do when something goes wrong. A mail API fails in a specific way: a 4xx is usually a bad request or an unverified domain, a 5xx is worth retrying, and a timeout is ambiguous because the message may or may not have been accepted. The SDK's job is to make the response object tell you which, and the two sections exist for that.

Framework Integration is the section for the people who use Laravel or Symfony rather than plain PHP. The point of the PSR-18 design is that a framework integration has very little to do: construct the client with your injected HTTP client, then use the same API. The repository layout supports that story, with src/, tests/, doc/ and a composer.json, plus the tooling that suggests a serious library: phpstan.neon for static analysis, psalm.xml, phpunit.xml.dist for tests, .php-cs-fixer.php for style, .roave-backward-compatibility-check.xml for checking the public API against a previous release, and a CHANGELOG.md.

The backward compatibility check is the most interesting of those for an adopter, because it is a statement that the public API is meant to stay stable across versions. The .github/ directory and the Gitter chat link complete the picture of a vendor-maintained SDK.

Where mailgun-php is the wrong tool

The first wrong-tool case is provider neutrality. The transport is generic, but the API is not. The method names, the o: prefixed option keys, the domain-as-first-argument convention and the resource accessors are all Mailgun's own. A team whose stated goal is to swap providers later will find that the coupling is at the level of your application code, not your transport, and the abstraction they would need to write is a wrapper around a wrapper.

The second is a non-PHP stack or a PHP project without a PSR client already in place. The install command pulls in symfony/http-client and nyholm/psr7, and a project that has deliberately avoided those, or that has standardised on a different PSR-7 implementation for reasons of its own, has to reconcile that choice. The requirement is a real constraint, not a formality.

The third is the API surface age. The library covers messages, IPs, pools, analytics and subaccounts, and the coverage of any particular Mailgun endpoint depends on how recently it was added to the API. A feature that Mailgun shipped last month may not have a typed method yet, and the fallback is to make the call yourself with the injected PSR-18 client, which means you are outside the SDK's value objects and error handling. The alternative is the raw HTTP client described in the Requirements section, and at that point you are writing the same integration the SDK was meant to save you from.

The fourth is operational, and it is the one the SDK cannot help with. The domain must exist in app.mailgun.com, the region is bound to the client instance, and the pool operations are asynchronous. A team that wants to move a large sending infrastructure between regions or providers is doing an infrastructure migration, not swapping a library, and the SDK's convenience is a small part of that work.

Editorial conclusion

mailgun-php fits a PHP application that sends transactional email through Mailgun and wants a typed, PSR-compliant client rather than hand-rolled HTTP calls, and it does not fit a project that needs to stay provider-neutral, since the API surface is Mailgun-specific even though the transport is not. Before adopting it, confirm your HTTP client implements PSR-18 and your PSR-7 implementation PSR-17, and decide at the start whether you are on the US or EU endpoint, because the region is chosen at client construction time rather than per request.

Frequently asked questions

What does mailgun-php require?

PHP 7.4 or higher, a PSR-18 HTTP client such as symfony/http-client or guzzlehttp/guzzle, and a PSR-7 and PSR-17 implementation such as nyholm/psr7. The SDK is not coupled to any specific HTTP library, so you bring your own PSR-18 client.

How do I install the Mailgun PHP SDK?

Run composer require mailgun/mailgun-php symfony/http-client nyholm/psr7. Then create the client with Mailgun::create('your-api-key'), or pass the EU endpoint https://api.eu.mailgun.net as the second argument if you are on the EU region.

How do I send an email with attachments using mailgun-php?

Call $mg->messages()->send('example.com', $options) with from, to, subject, text or html, and an attachment array of arrays holding filePath and filename. Tracking and other Mailgun options are passed as o: prefixed keys such as o:tracking and o:tag.

What are Dynamic IP Pools in the Mailgun API?

They group dedicated IPs and link them to domains so sending traffic is spread across all IPs in the pool automatically. Pools are created with a name and description, and updateIpPool can change metadata, add or remove IPs and link or unlink domains in one call, with changes to linked domains propagating asynchronously.

Official sources

  1. License: MIT
  2. mailgun/mailgun-php on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mailgun-mailgun-php.svg)](https://hysenlabs.com/projects/mailgun-mailgun-php)

Was this useful?