Skip to content

Latest commit

 

History

History
242 lines (195 loc) · 10.8 KB

helo.md

File metadata and controls

242 lines (195 loc) · 10.8 KB

Helo

Postmaster supports Helo's message and recipient webhook events, API sending, suppression sync, and the existing dashboard, tracking, resend, and sandbox features. API sending and suppression sync use stechstudio/laravel-helo-email. Webhooks and SMTP sending don't need it. Domain verification events are outside this package's email-delivery scope.

Send through the API

Install the Helo package:

composer require stechstudio/laravel-helo-email

Set these values in the app's .env. You don't need to change config/mail.php; the package adds a helo mailer.

MAIL_MAILER=helo
MAIL_FROM_ADDRESS=sender@your-verified-domain.com
MAIL_FROM_NAME="Your app"
HELO_API_KEY=...
HELO_CHANNEL_ID=...
HELO_MAIL_TYPE=transactional
POSTMASTER_HELO_SIGNING_KEY=...

The API key needs access to the selected channel, sending, and suppression operations. A channel-scoped key can send without a channel ID, but suppression sync always needs the channel ID explicitly.

For several mailers, you can set key, channel_id, and mail_type directly on each mailer; see the package README. The HELO_CHANNEL_ID and HELO_MAIL_TYPE settings still determine suppression sync's scope. Use broadcast as the mail type for individual broadcast messages; Postmaster uses Helo's /send/broadcast/message endpoint. It does not manage bulk campaigns, domains, or channel provisioning.

Helo adds its own unsubscribe link and List-Unsubscribe headers to every broadcast message; you can't point them at your own page. Put {{{ helo: unsubscribe }}} in the template to choose where the link appears; without it, Helo appends one at the end. Helo suppresses anyone who unsubscribes for that channel's broadcast mail, and only Helo support can lift it. Transactional mail gets no link.

Send ordinary Laravel Mailables and notifications. The transport preserves To/Cc/Bcc, reply-to, HTML and text, attachments and inline images, custom headers, and Laravel's tags and metadata. Helo's messageId becomes the sent-message ID, so webhook events match the original records.

Helo applies channel tracking defaults. To override them for a message:

$mail->withSymfonyMessage(function ($message) {
    $message->getHeaders()->addTextHeader('X-Helo-TrackOpens', 'true');
    $message->getHeaders()->addTextHeader('X-Helo-TrackLinks', 'false');
});

Set X-Helo-Idempotency-Key on a message when you need Helo to deduplicate retries of the same submission. The transport forwards it as an HTTP header. Helo rejects a reused key unless the request body is identical, and Postmaster stamps every attempt with a new Message-ID, so the transport leaves Message-ID out of keyed requests and Helo assigns one. It does not automatically retry sends. HTTP failures and failed API results raise Symfony transport exceptions, which Laravel's failover mailer handles. The exception message includes Helo's error code and detail.

If Helo accepts a message but suppresses some recipients, those recipients are recorded as dropped, and EmailDropped fires. Other recipients retain their own delivery state. A delayed submission is recorded as deferred. If every recipient is suppressed, Helo answers HTTP 422 recipients_suppressed and the send fails with a transport exception. With a failover mailer, Laravel then tries the next mailer, so keep POSTMASTER_BLOCK_SUPPRESSED=true and run sync if suppressed addresses must not go out through another provider.

Configure webhooks

Create a webhook in Helo for the channel this app uses, with this URL:

https://your-app.example/webhooks/postmaster/helo

Select these events:

Helo event Postmaster status
message-accepted accepted, one event per recipient
email-delivered delivered
email-bounced bounced
email-opened opened
link-clicked clicked
recipient-complained complained
recipient-unsubscribed unsubscribed
recipient-resubscribed resubscribed

Postmaster ignores message-processed. Its own outbound record already marks the message sent, so you can leave that event unselected.

Copy the webhook's signing key into POSTMASTER_HELO_SIGNING_KEY, then run php artisan config:clear. Use the key exactly as provided, including any prefix. Keep the server clock accurate. The signature timestamp must be within five minutes; the event's own timestamp can be older on retries.

Each Helo webhook belongs to one channel and has its own signing key. If you send through several channels, create a webhook for each, all pointing at the same URL, and list every key separated by commas:

POSTMASTER_HELO_SIGNING_KEY=key-for-channel-a,key-for-channel-b

Hard bounces suppress an address; soft or unknown bounce types do not. Helo's live events report Hard and Soft, while its API schema documents Permanent and Transient; Postmaster accepts both. The receiving server's diagnostic is stored as the event response, and its status (such as 5.1.1) as the code. Unsubscribes suppress the address and fire EmailUnsubscribed. A current resubscribe clears an unsubscribe and fires EmailResubscribed. Resubscribes do not clear manual suppressions or complaints, and delivery does not override an unsubscribe. Both new statuses have event/model predicates and model query scopes.

Helo receives HTTP 200 for accepted webhooks, including when POSTMASTER_QUEUE_WEBHOOKS=true. Run a queue worker when you enable that setting. Signature verification happens before queueing.

Sync suppressions

php artisan postmaster:sync --provider=helo --dry-run
php artisan postmaster:sync --provider=helo

Sync needs stechstudio/laravel-helo-email installed, and uses HELO_API_KEY, HELO_CHANNEL_ID, and HELO_MAIL_TYPE from its config.

Helo scopes suppressions by channel and mail type. Postmaster's address list is global. Configure sync for one channel and mail type, and keep the webhook and sending configuration aligned. This is not a separate suppression list for each tenant. If you send across independent Helo channels or mix broadcast and transactional mail, disable local address tracking/blocking and leave scoped suppression enforcement to Helo. Otherwise someone who unsubscribes from your newsletter would also stop getting their password resets: Helo blocks only further broadcast mail, but Postmaster's global list blocks everything.

POSTMASTER_TRACK_ADDRESSES=false
POSTMASTER_BLOCK_SUPPRESSED=false

Do not enable Helo suppression sync in that arrangement: leave HELO_CHANNEL_ID unset and configure the channel on each mailer. Message delivery tracking still works across channels.

Sync maps Helo's bounce, complaint, unsubscribe, and manual reasons to Postmaster's corresponding reasons. It fetches every page before changing local data. A failed or malformed response cannot clear the local list.

Helo only permits API removal of bounce and manual suppressions. It can reject an unsubscribe or complaint removal inside an HTTP 200 response; Postmaster reports that as a provider failure. The existing Postmaster::unsuppress() operation still lifts the local row and returns Helo in its manual list so the operator knows Helo did not remove it.

Use SMTP instead

Use Laravel's standard SMTP mailer with smtp.helohq.com, port 587, and a Helo SMTP user's credentials. SMTP users are scoped to a channel. SMTP sending and webhooks don't need stechstudio/laravel-helo-email; suppression sync does, with the same settings as above. The install wizard detects Helo from the SMTP hostname.

Helo's SMTP server answers each message with its message ID, and Postmaster records that ID, so webhooks match SMTP sends the same way they match API sends. SMTP has three limits the API transport doesn't:

  • Helo ignores Laravel's tags and metadata over SMTP, so its webhooks carry none. Postmaster still stores them on its own records.
  • A Helo SMTP user sends only one mail type, transactional or broadcast.
  • If some recipients are suppressed, Helo drops them without any event, and their rows stay sent. If every recipient is suppressed, the send fails with 554 All recipients are suppressed. Keep POSTMASTER_BLOCK_SUPPRESSED=true and run sync so Postmaster blocks those addresses before sending.

Helo documents the X-Helo-TrackOpens and X-Helo-TrackLinks headers for SMTP as well.

Run the first live test

Use a Laravel app with this branch installed and a publicly reachable HTTPS webhook endpoint. The package's ordinary Workbench preview deliberately uses sandbox/log delivery, so adding keys there alone will not send real mail.

  1. Choose a Helo channel and verified sender domain. In Helo account test mode, the recipient must also use a verified domain.

  2. Set the API key, channel ID, sender, and webhook signing key above. Use a normal delivery channel to test actual inbox delivery; a Helo sandbox channel simulates events.

  3. Run php artisan migrate, then php artisan config:clear. Enable stored content/attachments if you want to test preview, resend, and release.

  4. Register the public webhook URL and the eight events above in Helo.

  5. Run:

    php artisan postmaster:verify --provider=helo --to=recipient@example.com

    Use a shared cache store such as file, database, or redis, so the CLI can see webhook events from the web process.

  6. Check that a real delivery updates the original message row. Then test an attachment and an inline image, an open/click with tracking enabled, and a resend. Run suppression sync with --dry-run before changing local state.

Test bounces without hurting your reputation by sending to Helo's simulator: hard@bounce-test.helohq.com (suppressed) or soft+mailboxfull@bounce-test.helohq.com (not suppressed). Unsubscribe and resubscribe links appear in broadcast mail. Gmail fetches the open pixel through a proxy, so an open can arrive late or not at all.

The webhook fixtures in tests/fixtures/helo are redacted payloads captured from the live API on 2026-10-03. That run covered API and SMTP sending, every event except recipient-complained, suppression sync both ways, unsuppress, idempotent retries, and signature rejection. Gmail does not report spam complaints back to Helo, so complaint handling is tested only against Helo's documented payload.

Sources