Email Campaigns
Email campaigns let you send one-time broadcast messages to a targeted segment of contacts. Miranda handles the full lifecycle from draft composition through batched delivery.
Campaign model
Section titled “Campaign model”| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
foundation_id | UUID | Owning foundation |
name | string | Internal campaign name |
subject | string? | Email subject line (supports merge fields) |
body | string | HTML body (supports merge fields) |
channel | CampaignChannel | email for email campaigns |
status | CampaignStatus | Lifecycle state (see below) |
segment_criteria | JSON? | Targeting criteria for audience resolution |
stats | JSON? | Delivery statistics |
scheduled_at | datetime? | Scheduled send time |
sent_at | datetime? | When the campaign was actually sent |
Campaign statuses
Section titled “Campaign statuses”| Status | Description |
|---|---|
draft | Being composed, not yet sent |
scheduled | Queued for future send |
sending | Currently being dispatched in batches |
sent | All batches dispatched |
Send flow
Section titled “Send flow”The CampaignService::send() method orchestrates delivery:
- Status update — Campaign moves to
sending, stats initialized to zeros - Criteria enhancement —
email_opt_in: trueis automatically added to segment criteria - Audience resolution —
SegmentationService::resolve()builds the contact list - Batch dispatch — Contact IDs are chunked into batches of 50, each dispatched as a
SendCampaignBatchjob
// CampaignService::send()$criteria = $campaign->segment_criteria ?? [];$criteria['email_opt_in'] = $criteria['email_opt_in'] ?? true;
$contactIds = $this->segmentationService ->resolve($campaign->foundation_id, $criteria) ->pluck('id') ->toArray();
$chunks = array_chunk($contactIds, 50);foreach ($chunks as $chunk) { SendCampaignBatch::dispatch($campaign, $chunk);}If the resolved segment is empty, the campaign is marked as sent immediately.
Email delivery
Section titled “Email delivery”Each contact in a batch is sent an email via the EmailService:
- Merge fields are replaced in subject and body
- Email dispatched via Resend API (
https://api.resend.com/emails) - CommunicationLog record created for audit trail
- Activity recorded on the contact’s timeline (
ActivityType::EmailSent)
The sender address defaults to the foundation’s email, falling back to the system default in config/mail.php.
Merge fields
Section titled “Merge fields”Available merge placeholders (used in subject and body):
- first_name — Contact’s first name
- last_name — Contact’s last name
- foundation_name — Foundation’s name
Wrap each placeholder in double curly braces in your content (e.g., the first_name placeholder becomes the contact’s actual first name at send time).
Campaign statistics
Section titled “Campaign statistics”The stats JSON field tracks delivery metrics:
{ "sent": 150, "delivered": 145, "opened": 72, "clicked": 23, "bounced": 5}Retrieve stats via GET /api/campaigns/{campaign}/stats.
Plan limits
Section titled “Plan limits”| Plan | Emails per month |
|---|---|
| Starter | 1,000 |
| Growth | 5,000 |
| Pro | Unlimited |
API endpoints
Section titled “API endpoints”| Method | Path | Description |
|---|---|---|
| GET | /api/campaigns | List campaigns |
| POST | /api/campaigns | Create a campaign |
| GET | /api/campaigns/{id} | Show a campaign |
| PUT | /api/campaigns/{id} | Update a campaign |
| DELETE | /api/campaigns/{id} | Delete a campaign |
| POST | /api/campaigns/{id}/send | Send a campaign |
| GET | /api/campaigns/{id}/stats | Get delivery statistics |
Communication logging
Section titled “Communication logging”Every email sent creates a CommunicationLog record:
| Field | Value |
|---|---|
channel | email |
direction | outbound |
subject | Merged subject line |
body_preview | First 500 characters of plain text body |
status | sent or failed |
campaign_id | Link to the campaign |