Events
Courier fires CI4 events at key points in the contact and email lifecycle. You can listen to these to add your own logic — sync to a CRM, log to analytics, send a Slack notification, whatever you need.
Available events
| Constant | Fires when... | Payload |
|---|---|---|
CourierEvents::CONTACT_SUBSCRIBED |
A contact subscribes (new or re-subscribe) | ContactDTO |
CourierEvents::CONTACT_UNSUBSCRIBED |
A contact unsubscribes | ContactDTO |
CourierEvents::CONTACT_BOUNCED |
A hard bounce is received via webhook | ContactDTO |
CourierEvents::CONTACT_COMPLAINED |
A spam complaint is received via webhook | ContactDTO |
CourierEvents::EMAIL_SENT |
An email is successfully delivered (or logged in test mode) | SendDTO |
CourierEvents::EMAIL_FAILED |
An email fails to deliver | SendDTO |
'courier_enrollment_failed' |
A drip enrollment exhausts all retry attempts | DripEnrollmentDTO, string $errorMessage |
Registering a listener
Add listeners in app/Config/Events.php:
<?php
use Myth\Courier\Events\CourierEvents;
Events::on(CourierEvents::CONTACT_SUBSCRIBED, static function ($contact): void {
// $contact is a ContactDTO
log_message('info', "New subscriber: {$contact->email}");
});
Syncing to a CRM on subscribe
<?php
Events::on(CourierEvents::CONTACT_SUBSCRIBED, static function ($contact): void {
service('crmService')->upsertContact([
'email' => $contact->email,
'first_name' => $contact->first_name,
'tags' => $contact->tags ?? [],
]);
});
Alerting on delivery failures
<?php
Events::on(CourierEvents::EMAIL_FAILED, static function ($send): void {
// $send is a SendDTO with contact_id, campaign_id, and status
log_message('error', "Email delivery failed for send #{$send->id}");
// notify your team, update a dashboard, etc.
});
Tracking unsubscribes externally
<?php
Events::on(CourierEvents::CONTACT_UNSUBSCRIBED, static function ($contact): void {
service('analyticsService')->track('email.unsubscribed', [
'email' => $contact->email,
]);
});
Syncing suppressions to a CRM
Bounce and complaint events fire after Courier has already updated the contact's status and cancelled their drip enrollments. The $contact payload reflects the new suppressed status.
<?php
Events::on(CourierEvents::CONTACT_BOUNCED, static function ($contact): void {
// $contact->status is ContactStatus::Bounced at this point
service('crmService')->suppressContact($contact->email, reason: 'bounce');
});
Events::on(CourierEvents::CONTACT_COMPLAINED, static function ($contact): void {
// $contact->status is ContactStatus::Complained at this point
service('crmService')->suppressContact($contact->email, reason: 'complaint');
});
Webhook required
These events only fire when Courier receives a bounce or complaint via the webhook endpoint. They won't fire for addresses that bounce silently at the SMTP level without ESP feedback. See Tracking — Bounce and complaint webhooks for setup instructions.
Handling failed drip enrollments
When a drip step can't be delivered after all retry attempts, Courier fires courier_enrollment_failed with the enrollment and a short error message. Use this to alert your team, tag the contact, or queue a manual follow-up:
<?php
Events::on('courier_enrollment_failed', static function ($enrollment, string $errorMessage): void {
// $enrollment->status is EnrollmentStatus::Failed at this point
log_message('error', "Drip enrollment #{$enrollment->id} failed permanently: {$errorMessage}");
// Optionally tag the contact for manual review
service('contactService')->addTag($enrollment->contact_id, 'drip-send-failed');
});
The enrollment is already marked failed when this fires — it won't be retried again. If you want to give a contact another shot after fixing the underlying issue, you'll need to cancel the failed enrollment and re-enroll them manually.
Retry configuration
The number of attempts and the delay between them are controlled by $maxRetries and $retryDelayMinutes.
Error handling in listeners
Courier wraps each event trigger in a try/catch. If your listener throws an exception, Courier logs the error and continues — it won't interrupt the send or unsubscribe flow. That said, it's still a good idea to keep listeners fast and handle their own errors gracefully.