The official Mailgun PHP SDK — a clean, PSR-18 HTTP client wrapper around the Mailgun API.
- Requirements
- Installation
- Quick Start
- Sending Email
- IP Management
- Dynamic IP Pools
- Analytics
- Subaccounts
- Response Handling
- Debugging
- Framework Integration
- Contributing
- PHP 7.4 or higher
- A PSR-18 HTTP client (e.g.
symfony/http-client,guzzlehttp/guzzle) - A PSR-7 / PSR-17 implementation (e.g.
nyholm/psr7)
The SDK is not coupled to any specific HTTP library — bring your own PSR-18 client.
composer require mailgun/mailgun-php symfony/http-client nyholm/psr7EU region? Use
https://api.eu.mailgun.netas your endpoint (see below).
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');Note: The
$domainyou pass to any API call must match a domain configured in app.mailgun.com.
$mg->messages()->send('example.com', [
'from' => 'Alice <alice@example.com>',
'to' => 'bob@example.com',
'subject' => 'Hello from Mailgun!',
'text' => 'This is a plain-text body.',
'html' => '<p>This is an <strong>HTML</strong> body.</p>',
]);$mg->messages()->send('example.com', [
'from' => 'alice@example.com',
'to' => ['bob@example.com', 'carol@example.com'],
'subject' => 'Monthly report',
'text' => 'Please find the report attached.',
'attachment' => [['filePath' => '/tmp/report.pdf', 'filename' => 'report.pdf']],
'o:tracking' => 'yes',
'o:tag' => ['monthly', 'report'],
]);$mg->messages()->send('example.com', [
'from' => 'alice@example.com',
'to' => 'bob@example.com',
'subject' => 'See you tomorrow',
'text' => 'Scheduled for tomorrow morning.',
'o:deliverytime' => 'tomorrow 9am UTC',
]);$response = $mg->ips()->index();
foreach ($response->getItems() as $ip) {
echo $ip . PHP_EOL;
}
// Dedicated only
$dedicated = $mg->ips()->index(dedicated: true);
// With full details (pool assignments, subaccount ownership, timestamps)
$detailed = $mg->ips()->listIpsDetailed([
'pool_id' => 'my-pool-id', // filter by pool ('any' or 'none' also accepted)
'subaccount_id'=> 'sub-123',
'limit' => 25,
]);
foreach ($detailed->getItems() as $ip) {
echo "{$ip['address']} — pools: " . implode(', ', $ip['pool_ids']) . PHP_EOL;
}$ip = $mg->ips()->show('1.2.3.4');
echo $ip->getIp(); // "1.2.3.4"
echo $ip->getRdns(); // reverse DNS
var_dump($ip->getDedicated()); // bool// Add IP to a domain
$mg->ips()->assign('example.com', '1.2.3.4');
// Remove IP from a domain
$mg->ips()->unassign('example.com', '1.2.3.4');
// List IPs currently assigned to a domain
$response = $mg->ips()->domainIndex('example.com');
print_r($response->getItems());// Assign an IP to every domain in the account (async)
$ref = $mg->ips()->assignIpToAllDomains('1.2.3.4');
echo $ref->getReferenceId(); // track the async operation
// Remove an IP from all domains, replacing it with another
$ref = $mg->ips()->removeIpFromAllDomains('1.2.3.4', alternative: '5.6.7.8');
echo $ref->getMessage();$response = $mg->ips()->domainsByIp('1.2.3.4', limit: 20, search: 'example');
foreach ($response->getItems() as $domain) {
echo $domain . PHP_EOL;
}// Move an account IP into a dedicated IP band
$mg->ips()->placeAccountIpToBand('1.2.3.4');// Check how many dedicated IPs your plan allows
$available = $mg->ips()->numberOfIps();
// Provision a new dedicated IP
$mg->ips()->addDedicatedIp();Dynamic IP Pools let you group dedicated IPs and link them to domains, so sending traffic is spread across all IPs in the pool automatically.
// All pools in the account
$response = $mg->ips()->listIpPools();
foreach ($response->getIpPools() as $pool) {
echo "{$pool['pool_id']} — {$pool['name']}" . PHP_EOL;
echo " IPs: " . implode(', ', $pool['ips']) . PHP_EOL;
echo " Linked to domains: " . ($pool['is_linked'] ? 'yes' : 'no') . PHP_EOL;
}
// Single pool details
$pool = $mg->ips()->loadDIPPInformation('my-pool-id');
echo $pool->getPoolId(); // "my-pool-id"
echo $pool->getName(); // "Primary sending pool"
echo $pool->getDescription(); // "Main US sending pool"
print_r($pool->getIps()); // ["1.2.3.4", "5.6.7.8"]
var_dump($pool->isLinked()); // bool — whether domains are attached// Create a new pool
$mg->ips()->createIpPool('Primary Pool', 'Main US outbound pool');
// Modify pool metadata, add/remove IPs, link/unlink domains — all in one call
$mg->ips()->updateIpPool('my-pool-id', [
'name' => 'Primary Pool v2',
'add_ip' => '9.10.11.12',
'remove_ip' => '1.2.3.4',
'link_domain' => 'example.com',
'unlink_domain' => 'old.example.com',
]);// Add a single IP to a pool
$mg->ips()->addIpToPool('my-pool-id', '9.10.11.12');
// Add multiple IPs at once
$mg->ips()->addIpsToPool('my-pool-id', ['9.10.11.12', '13.14.15.16']);
// Remove an IP from a pool
$mg->ips()->removeIpFromPool('my-pool-id', '1.2.3.4');When a pool is linked to domains, adding or removing IPs propagates to all linked domains asynchronously after the API responds.
$response = $mg->ips()->getIpPoolDomains('my-pool-id', limit: 20);
foreach ($response->getDomains() as $domain) {
echo $domain['name'] . PHP_EOL;
}
// Paginate using the cursor from the previous response
if ($response->getNextPage()) {
$next = $mg->ips()->getIpPoolDomains('my-pool-id', page: $response->getNextPage());
}// Delete without replacement (pool must not be linked to any domains)
$mg->ips()->deleteDIPP('my-pool-id');
// Replace linked domains with a specific IP before deleting
$mg->ips()->deleteDIPP('my-pool-id', replacementIp: '1.2.3.4');
// Replace linked domains with another pool before deleting
$mg->ips()->deleteDIPP('my-pool-id', replacementPoolId: 'backup-pool-id');// Grant a subaccount access to a pool
$mg->ips()->delegateIpPool('my-pool-id', 'sub-account-id');
// Revoke subaccount access
$mg->ips()->revokeDelegatedIpPool('my-pool-id', 'sub-account-id');$result = $mg->metrics()->loadMetrics([
'start' => 'Sun, 22 Dec 2024 00:00:00 +0000',
'end' => 'Sun, 29 Dec 2024 00:00:00 +0000',
'resolution' => 'day',
'dimensions' => ['time'],
'metrics' => ['accepted_count', 'delivered_count', 'clicked_rate', 'opened_rate'],
'include_aggregates' => true,
'include_subaccounts' => true,
]);
foreach ($result->getItems() as $item) {
echo $item['dimensions']['time'] . ': ' . $item['metrics']['delivered_count'] . ' delivered' . PHP_EOL;
}// Create a subaccount
$mg->subaccounts()->create('Marketing Team');
// List all subaccounts
$items = $mg->subaccounts()->index();
print_r($items->getItems());
// Enable / disable
$mg->subaccounts()->enable($subAccountId);
$mg->subaccounts()->disable($subAccountId);// Pass the subaccount ID as the third argument to Mailgun::create()
$mg = Mailgun::create('your-api-key', 'https://api.mailgun.net', $subAccountId);
// All subsequent calls are scoped to that subaccount
$mg->messages()->send('example.com', [...]);All API methods return typed model objects with IDE-friendly getters by default.
$domain = $mg->domains()->show('example.com');
foreach ($domain->getInboundDNSRecords() as $record) {
echo $record->getType() . ': ' . $record->getValue() . PHP_EOL;
}Prefer raw arrays? Inject ArrayHydrator:
use Mailgun\Hydrator\ArrayHydrator;
use Mailgun\HttpClient\HttpClientConfigurator;
$configurator = new HttpClientConfigurator();
$configurator->setApiKey('your-api-key');
$mg = new Mailgun($configurator, new ArrayHydrator());
$data = $mg->domains()->show('example.com');
// $data is now a plain associative arrayNeed the raw response? Use NoopHydrator — note: no exceptions are thrown on non-200 responses when using this hydrator.
use Mailgun\Hydrator\NoopHydrator;
$mg = new Mailgun($configurator, new NoopHydrator());
$response = $mg->messages()->send('example.com', [...]);
// $response is a Psr\Http\Message\ResponseInterface
echo $response->getStatusCode();Route traffic through Mailgun's Postbin to inspect what the SDK sends:
use Mailgun\HttpClient\HttpClientConfigurator;
use Mailgun\Hydrator\NoopHydrator;
$configurator = new HttpClientConfigurator();
$configurator->setEndpoint('http://bin.mailgun.net/aecf68de'); // replace with your bin ID
$configurator->setApiKey('your-api-key');
$configurator->setDebug(true);
$mg = new Mailgun($configurator, new NoopHydrator());
$mg->messages()->send('example.com', [
'from' => 'alice@example.com',
'to' => 'bob@example.com',
'subject' => 'Debug test',
'text' => 'Checking what hits the wire.',
]);$client = $mg->httpClient();
$client->httpGet('/v3/domains', ['limit' => 5]);
$client->httpPost('/v3/some/path', ['key' => 'value']);
$client->httpPut('/v3/some/path', ['key' => 'value']);
$client->httpDelete('/v3/some/path');| Framework | Package |
|---|---|
| Symfony | tehplague/swiftmailer-mailgun-bundle |
| Yii2 | katanyoo/yii2-mailgun-mailer |
| CakePHP | narendravaghela/cakephp-mailgun |
| Drupal | drupal/mailgun |
| Laravel | Built-in — see Laravel Mail docs |
This is an open-source project under the MIT license, maintained by Mailgun and the community.
git clone git@github.com:mailgun/mailgun-php.git
cd mailgun-php
composer install
composer test- Test the
dev-masterbranch and open issues for anything broken - Review open pull requests
- Add tests for untested endpoints
- Improve documentation and examples
- Documentation: documentation.mailgun.com
- Issues: GitHub Issues
- Account support: app.mailgun.com/support
- More examples: doc/examples.md