---
title: "Sending WordPress email through Mailtrap's HTTP A… — Symonov"
description: "Routing WordPress mail through Mailtrap's Send API instead of SMTP unlocks bulk/transactional streams, categories, and live suppression. Here's how it w..."
url: https://symonov.com/blog/sending-wordpress-email-through-mailtrap-api-not-smtp-2026/
---

# Sending WordPress email through Mailtrap's HTTP API (not SMTP) in 2026

May 9, 2026 · 8 min read · by Serhii Symonov · `mailtrap` `wordpress` `wp-mail`

Most WordPress + Mailtrap tutorials end the same way: install a generic SMTP plugin, paste your Mailtrap SMTP credentials, and you’re done. That works — but every WordPress site that does it is leaving the more interesting half of Mailtrap’s product on the shelf. **The Mailtrap HTTP Send API exposes features the SMTP gateway physically cannot expose**: per-message stream routing (bulk vs transactional), automatic email categorization, custom tracking variables, template UUIDs, and a live view of your suppression list inside WP-admin. Switching from SMTP to the API also fixes the slow-and-flaky problems most WordPress hosts have with outbound port 587/465.

This post explains why the SMTP path is fragile from a WordPress process, what the Mailtrap API gives you instead, and the minimum viable code to swap one for the other inside a plugin — including the gotchas around `wp_mail()` filter ordering, attachment encoding, and graceful fallback when the network blip happens.

## Why SMTP from WordPress is fragile

PHP’s mail story on shared and managed WordPress hosts is a known mess. The default `mail()` binary points at a local Sendmail or Postfix that the host doesn’t actually want you using; SMTP plugins paper over it by opening an authenticated TCP session to a real provider. That session has costs that look invisible until you scale a bit:

- **Connection latency.** SMTP needs `MAIL FROM`, `RCPT TO`, `DATA`, then your message body, then `QUIT`. Each round-trip adds ~50–150 ms on a healthy network. At 10 mails per page load (think order-confirmation + admin notify + analytics), you’ve added a second of wall time before the user sees a response.
- **Blocked egress.** Cheaper hosts (and corporate firewalls) block outbound 587/465. Your test email works on staging, the production site silently drops mail with a “could not connect” buried in `error_log`. SMTP failure modes are excellent at hiding from people who don’t read PHP error logs.
- **Single-stream blindness.** SMTP gives you one channel into the provider. Mailtrap (and Postmark, Sendgrid, etc.) split sending into **transactional** and **bulk** streams behind the scenes; using SMTP, you can’t address those streams independently. Marketing newsletters and password resets compete in the same queue.
- **No metadata.** Suppression lists, custom tracking variables, template IDs — all communicated via SMTP through ad-hoc headers (when supported at all), which providers parse with varying enthusiasm.

The HTTP Send API solves the first three by design and the fourth by intention. One `POST` to `https://send.api.mailtrap.io/api/send`, one HTTPS round-trip, structured JSON in / structured JSON out, native fields for stream and category. WordPress already has `wp_remote_post()`; you don’t even need a vendor SDK.

## The drop-in replacement: filter `pre_wp_mail`

`wp_mail()` is the universal mail-sending entry point in WordPress. Every plugin that sends mail goes through it — Contact Form 7, WooCommerce, BuddyPress, WP-CLI, your custom code. There are two clean ways to intercept it:

1. **Replace it via `pre_wp_mail` filter** (added in WordPress 5.7). Returning a non-null value from this filter short-circuits the rest of `wp_mail()` and treats your return value as the result.
2. **Override the global `phpmailer` instance** via the `phpmailer_init` action. This is the older path, used by WP Mail SMTP and friends. It still calls PHPMailer’s SMTP transport — fine for SMTP plugins, useless when your goal is to talk HTTP.

For HTTP-API routing, `pre_wp_mail` is unambiguously the right hook. The minimum viable adapter looks like this:

```php
add_filter('pre_wp_mail', __NAMESPACE__ . '\\route_to_mailtrap', 10, 2);

function route_to_mailtrap($null, array $atts): bool|null {
    // $null is null when no other filter has handled the call.
    // Returning anything non-null short-circuits wp_mail().

    if (!swifttrap_is_enabled()) return null; // graceful pass-through

    [$to, $subject, $message, $headers, $attachments] = array_values($atts);

    $payload = build_mailtrap_payload($to, $subject, $message, $headers, $attachments);
    $stream  = pick_stream($payload, $headers); // bulk vs transactional

    $endpoint = $stream === 'bulk'
        ? 'https://bulk.api.mailtrap.io/api/send'
        : 'https://send.api.mailtrap.io/api/send';

    $response = wp_remote_post($endpoint, [
        'headers' => [
            'Authorization' => 'Bearer ' . get_option('swifttrap_api_token'),
            'Content-Type'  => 'application/json',
        ],
        'body'    => wp_json_encode($payload),
        'timeout' => 12,
    ]);

    if (is_wp_error($response)) {
        // Don't lose the email — let core fall back to its default handler.
        do_action('swifttrap_send_error', $response, $atts);
        return null;
    }

    $code = wp_remote_retrieve_response_code($response);
    return $code >= 200 && $code < 300;
}
```

That’s the whole adapter, in roughly 30 lines of plumbing. The interesting bits are in `build_mailtrap_payload()` and `pick_stream()`.

## Building the payload: the parts that actually matter

Mailtrap’s Send API takes a structured JSON body. The parts you’ll touch every time:

- `from`: `{ email, name }`. Must match a verified sender on your sending domain — the API rejects unknown senders.
- `to`, `cc`, `bcc`: arrays of `{ email, name }` objects.
- `subject`, `text`, `html`: the message body. Send both `text` and `html` when you have them; the API will pick the right one per recipient.
- `attachments`: array of `{ content, filename, type, disposition }`. The `content` field is base64. WordPress passes attachments to `wp_mail()` as filesystem paths, so you read each file and `base64_encode` it.
- `category`: a string. Mailtrap groups stats and filters by this value; pick one per message type (“welcome”, “password-reset”, “order-confirmation”, etc.). Auto-derive it from the subject line if you don’t want every plugin to set it manually.
- `custom_variables`: an associative array. Useful for correlating sends with internal IDs (order ID, user ID, contact form submission ID).

For attachments specifically:

```php
$attachment_payloads = array_map(function (string $path) {
    if (!is_readable($path)) return null;
    $size = filesize($path);
    if ($size > 25 * 1024 * 1024) return null; // Mailtrap caps at 25 MB
    return [
        'content'     => base64_encode(file_get_contents($path)),
        'filename'    => basename($path),
        'type'        => mime_content_type($path) ?: 'application/octet-stream',
        'disposition' => 'attachment',
    ];
}, $attachments);
$attachment_payloads = array_filter($attachment_payloads);
```

Two non-obvious points: don’t `wp_mail()` an inline image with `disposition: inline` and the same `content_id` headers you’d use over SMTP — the API has its own `disposition: inline` field but the lookup by `Content-ID` is brittle. And the 25 MB ceiling is hard; size-check before you base64-encode, or you’ll OOM the PHP worker on a multi-MB binary attachment.

## Stream routing: bulk vs transactional

The single biggest reason to use the API over SMTP is access to Mailtrap’s two streams:

- **`send.api.mailtrap.io`** is the transactional stream. Higher priority, low-volume-friendly, what you want for password resets and order confirmations.
- **`bulk.api.mailtrap.io`** is the bulk stream. Optimized for high-volume promotional traffic; rate limits and reputation are managed separately so a marketing burst doesn’t poison your transactional reputation.

Routing decision needs to be **per-message**, not per-site. A clean pattern is to derive it from category:

```php
function pick_stream(array $payload, array $headers): string {
    $marketing_categories = ['marketing', 'newsletter', 'campaign', 'promotional'];
    $category = $payload['category'] ?? '';
    if (in_array(strtolower($category), $marketing_categories, true)) return 'bulk';

    // Allow plugins to opt into bulk per-message via a filter.
    return apply_filters('swifttrap_use_bulk_stream', false, $payload, $headers)
        ? 'bulk'
        : 'transactional';
}
```

The `apply_filters()` hook is the integration seam. A custom newsletter plugin doesn’t need a settings page — it just sets `add_filter('swifttrap_use_bulk_stream', '__return_true')` for the duration of its sending loop and you’re routed to the bulk endpoint.

## Suppression list visibility

Most deliverability problems aren’t “emails fail” — they’re “emails succeed and then a recipient marks them as spam, and three weeks later your sender reputation has drifted enough that the next batch sees a 30% open rate.” The fix is to **see your suppression list inside WP-admin, not only in the Mailtrap dashboard**.

The `/api/accounts/{account_id}/sending_domains/{domain_id}/suppressions` endpoint returns the live list: bounces, complaints, manual blocks, unsubscribes. Cache it for ~5 minutes (don’t hit the API on every admin pageview), render it on a Stats page, and let the user export or clear individual entries. Now the deliverability problem becomes visible before the next campaign batch.

```php
function fetch_suppressions(): array {
    $cached = get_transient('swifttrap_suppressions');
    if ($cached !== false) return $cached;

    $response = wp_remote_get(sprintf(
        'https://mailtrap.io/api/accounts/%d/sending_domains/%d/suppressions',
        get_option('swifttrap_account_id'),
        get_option('swifttrap_domain_id'),
    ), [
        'headers' => ['Authorization' => 'Bearer ' . get_option('swifttrap_api_token')],
        'timeout' => 8,
    ]);

    if (is_wp_error($response)) return [];
    $data = json_decode(wp_remote_retrieve_body($response), true) ?: [];

    set_transient('swifttrap_suppressions', $data, 5 * MINUTE_IN_SECONDS);
    return $data;
}
```

Five lines of plumbing, one feature your users won’t get from any SMTP plugin.

## Graceful fallback and observability

Two failure modes to handle:

**Token misconfigured / expired.** Don’t drop the email — return `null` from `pre_wp_mail` so core’s PHPMailer takes over. The user gets a working mail (probably via the host’s `mail()`), and you log a `WP_Error` for the admin to triage. The worst outcome is a slow log line; the best is no user impact.

**Network failure.** Same play. `wp_remote_post()` returns a `WP_Error` on timeout or DNS failure. Pass it to a `do_action('swifttrap_send_error',...)` hook, fall through to core, and let the admin see the error rate via the email log table.

The email log itself is worth shipping. Store one row per `wp_mail()` call: timestamp, recipient (first only — privacy), subject, status code, latency, category, stream. Keep it in a custom table with a configurable retention window (default 30 days). Now you have an audit trail without depending on Mailtrap’s dashboard, and “did that order email actually go out?” becomes a one-click answer.

## When SMTP is still the right choice

Honest framing: not every WordPress site should use the Mailtrap API. If you might switch providers next month (Mailtrap → SendGrid → Postmark → SES is a common path while sites scale), the SMTP plugin abstracts over all of them and saves you a swap. WP Mail SMTP and Post SMTP are the right tools for that. The HTTP API is the right choice when you’ve decided on Mailtrap and want every feature it offers — categories, streams, suppression visibility, templates — without hand-rolling the integration.

If you don’t want to write the adapter yourself, **SwiftTrap for Mailtrap** is the packaged version of the patterns above. ~30 KB, no vendor SDK, free, GPL, [on the WordPress.org directory](https://wordpress.org/plugins/swifttrap-for-mailtrap/). It implements `pre_wp_mail` interception, stream routing, the suppression-list page, and a local email log out of the box. If you’d rather read the code first, the source is on [GitHub](https://github.com/symonovserhii/swifttrap-for-mailtrap).

related plugin

### [SwiftTrap for Mailtrap](https://symonov.com/plugins/swifttrap-for-mailtrap/)

Send WordPress emails through the Mailtrap Email API (not SMTP). Bulk and transactional streams, categories, suppression list, email log.

[see the plugin](https://symonov.com/plugins/swifttrap-for-mailtrap/)
