Contacts
Contacts are the people in your mailing list. Each contact has a status, optional tags, and an auto-generated unsubscribe token. The ContactService handles the full lifecycle.
Get the service via CI4's service locator:
Subscribing a contact
<?php
$contact = $contactService->subscribe([
'email' => 'ada@example.com',
'first_name' => 'Ada',
'last_name' => 'Lovelace',
]);
subscribe() returns a ContactDTO. It either creates a new contact or, if the email already exists and the contact was previously unsubscribed, re-subscribes them.
Adding tags at subscribe time
Pass a list of tag slugs as the second argument:
<?php
$contact = $contactService->subscribe(
['email' => 'ada@example.com'],
tags: ['newsletter', 'vip']
);
Tags are created automatically if they don't exist yet.
Enrolling in a drip on subscribe
Pass a dripCampaignId to automatically enroll the contact in a drip sequence after subscribing:
Contact statuses
| Status | Meaning |
|---|---|
subscribed |
Active — will receive emails |
unsubscribed |
Opted out — can be re-subscribed |
bounced |
Hard bounce — cannot be re-subscribed |
complained |
Marked as spam — cannot be re-subscribed |
Bounced and complained contacts
Calling subscribe() for a contact with bounced or complained status throws a CourierValidationException. These statuses require manual review before re-engagement.
Unsubscribing
Courier handles unsubscribes automatically via the tracking controller when a contact clicks their unsubscribe link. You can also unsubscribe programmatically by token:
Unsubscribing also cancels all active drip enrollments for that contact.
Managing tags
Applying tags
Tags that don't exist are created on the fly. Tags already applied are silently skipped — it's safe to call this multiple times.
Removing tags
Tags that aren't applied are silently ignored.
Looking up a contact
Custom fields
Contacts support a custom_fields JSON column for arbitrary data. Pass these in the data array when subscribing:
<?php
$contactService->subscribe([
'email' => 'ada@example.com',
'custom_fields' => json_encode(['plan' => 'pro', 'signup_source' => 'homepage']),
]);
Custom fields can also be used in segment rules.