Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/DataTalksClub/datamailer/llms.txt

Use this file to discover all available pages before exploring further.

A campaign in Datamailer is a one-to-many email send from a client to an audience. The design prioritizes auditability: every intended recipient gets their own database row before a single message is sent, and every engagement event is recorded as an immutable append to the event log. This means you can always answer the question “why was this person skipped?” long after the campaign has finished sending.

Campaign Statuses

A campaign moves through a linear status machine from creation to completion. Terminal statuses are sent, cancelled, and failed.
StatusDescription
draftBeing composed; not yet queued for sending
queuedAccepted for sending; batch payloads enqueued to SQS
snapshottingLambda is expanding filters into campaign_recipient rows
sendingLambda workers are sending messages through SES
sentAll recipients have been processed
cancelledCancelled before or during sending
failedAn unrecoverable error occurred during snapshotting or sending

Campaign Fields

FieldDescription
clientThe client that owns this campaign
audienceThe audience being targeted
subjectEmail subject line
preview_textPreview snippet shown in inbox clients
html_bodyFull HTML email body
text_bodyPlain-text fallback body
include_tagsSend only to contacts with all of these tags (slugs)
exclude_tagsExclude contacts with any of these tags (slugs)
scheduled_atOptional future send time
sent_atWhen sending completed
Tag filter values are stored as sorted slug arrays and normalized via slugify when the campaign is saved.

Recipient Snapshot

Before any email is sent, Datamailer expands the campaign’s audience + tag filters into a CampaignRecipient row for every candidate contact. For a 120k-recipient campaign, this creates 120k rows — that is intentional. The snapshot is the audit trail. Each recipient row is immediately evaluated against suppression and subscription rules. Recipients who cannot receive the email are marked skipped with a specific skip_reason.

Skip Reasons

Skip ReasonCondition
hard_bouncecontact.hard_bounced_at is set
complaintcontact.complained_at is set
global_unsubscribecontact.global_unsubscribed_at is set
invalid_emailEmail validation status is non-deliverable
unverifiedcontact.verified_at is null
audience_unsubscribeAudience-level subscription is unsubscribed
client_unsubscribeClient-scoped subscription is unsubscribed
suppressedNo active client-scoped subscription found
duplicateContact appears more than once in the candidate set
Every recipient row is checked again during the Lambda send worker execution. If a contact’s suppression state changes between snapshotting and sending — for example, a new hard bounce arrives — the worker will skip the recipient and update the row rather than sending a duplicate.

Recipient Row Fields

FieldDescription
campaignParent campaign
contactThe contact being sent to
emailEmail address at snapshot time
statuspending, sent, skipped, failed, bounced, complained, unsubscribed
skip_reasonPopulated when status is skipped
tracking_token_hashHash of the open-pixel tracking token
unsubscribe_token_hashHash of the unsubscribe link token
ses_message_idSES message ID returned after successful send
sent_at, delivered_atSend and delivery timestamps
first_opened_at, first_clicked_atFirst engagement timestamps
open_count, click_countTotal engagement counts

Engagement Statistics

Campaign-level stats are aggregate counters maintained on the Campaign row and refreshed after each batch of sends and after each tracking event.
StatDescription
recipient_countTotal non-skipped recipients
sent_countRecipients where SES accepted the message
skipped_countRecipients skipped at snapshot time
delivered_countRecipients confirmed delivered by SES
unique_open_countRecipients with at least one open
open_countTotal open events (including repeat opens)
unique_click_countRecipients with at least one click
click_countTotal click events
unsubscribe_countRecipients who unsubscribed via the campaign link
bounce_countRecipients with a hard bounce
complaint_countRecipients who marked the message as spam

Derived Rates

RateFormula
Open rateunique_open_count / sent_count
Click rateunique_click_count / sent_count
Click-to-open rateunique_click_count / unique_open_count
Bounce ratebounce_count / sent_count
Unsubscribe rateunsubscribe_count / sent_count

Campaign Send Flow

1

Create campaign in draft

A staff user creates a campaign for a client and audience, sets the subject, body, and optional tag filters, and saves it in draft status.
2

Queue the campaign

The staff user triggers sending. Datamailer acquires a row-level lock on the campaign, verifies it is still in draft, then immediately calls snapshot_campaign_recipients within the same transaction.
3

Snapshot recipients

Datamailer queries all contacts with a subscription to the campaign’s audience and client, applies include_tags and exclude_tags filters, and creates one CampaignRecipient row per contact. Each row is marked pending or skipped based on suppression and subscription state.
4

Enqueue SQS batches

Pending recipient IDs are split into batches (default size controlled by CAMPAIGN_EMAIL_BATCH_SIZE). One SQS message is enqueued per batch. The campaign status moves to queued. A queued EmailEvent is appended for each pending recipient.
5

Lambda sends via SES

Lambda workers consume SQS messages. For each recipient ID in a batch, the worker re-checks suppression state, renders the email with tracking URLs, calls SES SendEmail, and updates the recipient row to sent with the returned ses_message_id.
6

SES delivers and publishes events

SES delivers the message and publishes delivery, open, click, bounce, and complaint notifications to the ses-webhooks SQS queue via SNS. Datamailer’s webhook worker processes these events and updates the recipient row and campaign aggregate counters.

Build docs developers (and LLMs) love