Email Campaign Engine - Documentation
Setup guide and full API reference for the open source, self-hosted Email Campaign Engine.
Get the code on GitHub or read the Email Broadcast overview first.
What the Engine Is
Requirements: PHP 8.1 or newer with the curl, openssl, and pdo_sqlite extensions, all included in standard PHP builds. It runs on shared hosting, a VPS, or Docker.
The pieces:
| File | What it does |
|---|---|
| public/api.php | The JSON API. Every endpoint documented below lives here. |
| public/webhook.php | Provider callbacks for delivery and engagement events. |
| public/click.php | Tracked click redirect with bot filtering. |
| public/unsub.php | Hosted one-click unsubscribe page. |
| public/admin.php | Password protected admin area: domains, messages, lists, series, contacts, reports, settings, SMTP test. |
| cli/cron.php | Scheduled sending entry point, run by cron or the Docker cron container. |
| config.php | Copied from config.sample.php, holds your keys and settings. |
| data/ | The SQLite database and queue files, created automatically. |
Quick Start with Docker
The app is then at http://localhost:8080/ with the admin area at admin.php. The compose file includes a second container that loops the cron script, so scheduled sending works out of the box with no host crontab.
Quick Start on Shared Hosting or a Server
For a quick local test:
On shared hosting, upload the project and point your docroot (or a subdomain) at the public/ folder. The SQLite database is created automatically in data/. Then add the cron job from the next section.
apiKey, adminPassword, and webhookKey to long random strings, serve over HTTPS, and set up SPF, DKIM, and DMARC on every sending domain with your provider.Cron and Scheduled Sending
Everything scheduled goes out through cli/cron.php. Add one crontab line:
Every 10 minutes keeps responders on time, hourly also works. The script is safe to run as often as you like: responder steps send once, each broadcast hour bucket sends once, and the auto scheduler runs once per day. Docker users can skip this, the compose cron container already loops it.
Each cron run does three things:
| Step | What happens |
|---|---|
| 1 | Sends autoresponder messages that are due. |
| 2 | Sends the current hour's bucket of today's broadcasts. |
| 3 | Once per day, when the auto scheduler is on in the admin settings, builds today's broadcasts from your lists. |
Setup Walkthrough
| Step | What to do |
|---|---|
| 1 | In config.php set apiKey, adminPassword, webhookKey, and publicUrl. |
| 2 | Log in to admin.php and add a sending domain with your provider's SMTP settings (table in the provider section below). The from address becomes alias@domain. |
| 3 | Send yourself a test email from the dashboard to confirm SMTP works. |
| 4 | Point your provider's event webhooks at your webhook URL (see the webhook section below). |
| 5 | Create a message. Placeholders: ##FNAME## first name, ##SUBID## click tracking id, ##DOMAIN## the sending domain, ##UNSUB## the hosted unsubscribe link. A tracked link looks like https://yourdomain.com/click.php/##SUBID##. |
| 6 | Upload contacts in the admin area or push them through the API. |
| 7 | Create a list (sending profile) and schedule a broadcast from the dashboard, or enable the auto scheduler in settings and check the message's weekday boxes. |
| 8 | For drips, create a series and subscribe addresses through the API, an upload, or your signup form. |
Step by step versions of the common jobs: sending a bulk broadcast, setting up a drip series, connecting a provider, and bounce and unsubscribe handling.
Provider SMTP Settings
| Provider | SMTP host | Username | Password |
|---|---|---|---|
| SendGrid | smtp.sendgrid.net | apikey | your SendGrid API key |
| Mailgun | smtp.mailgun.org | postmaster@your-domain | Mailgun SMTP password |
| Postmark | smtp.postmarkapp.com | server token | server token |
| Amazon SES | email-smtp.us-east-1.amazonaws.com (region specific) | SES SMTP username | SES SMTP password |
| SMTP2GO | mail.smtp2go.com | SMTP2GO username | SMTP2GO password |
| Brevo | smtp-relay.brevo.com | account email | Brevo SMTP key |
| SparkPost | smtp.sparkpostmail.com | SMTP_Injection | SparkPost API key |
| Elastic Email | smtp.elasticemail.com | account email | Elastic Email API key |
| Mailtrap | live.smtp.mailtrap.io | api | Mailtrap sending token |
| Mailjet | in-v3.mailjet.com | Mailjet API key | Mailjet secret key |
| SMTP.com | send.smtp.com | account username | SMTP.com password |
Provider Webhooks
where {provider} is one of: sendgrid, mailgun, postmark, ses, smtp2go, brevo, sparkpost, elasticemail, mailtrap, mailjet, smtpcom. The key is the webhookKey from your config.php.
How the API Works
X-API-Key header, or as an apiKey field in the JSON body. Endpoint paths use path info, and if your server does not support that, api.php?action=send works the same as api.php/send.
Send a Single Email
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | The recipient address. | |
| Sending Domain | domain | Must match a configured sending domain. The from address becomes the domain's alias at this domain. |
| Subject | subject | The email subject line. |
| HTML Body | html | The HTML content of the message. |
Add and Upload Contacts
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | The contact's email address. |
| Field Name | Field Key | Notes |
|---|---|---|
| First Name | fname | First name only, used by the ##FNAME## placeholder. |
| Last Name | lname | Last name only. |
| Feed Name | feed | A short code identifying which data source or campaign this contact came from. Used for tracking and filtering. Example: webform-jan |
| State | state | Two-letter US state code. Example: NY, FL |
| Country | country | Two-letter country code. Example: US |
| Signup URL | sourceurl | The page where this contact opted in. |
| Opt-In Date | optin | Date and time the contact opted in. Example: 05-01-2025 05:45:00 |
| IP Address | ip | IPv4 or IPv6 address at the time of opt-in. |
| Field Name | Field Key | Notes |
|---|---|---|
| Content | content | One record per line as email,fname,lname,feed,sourceurl,optin,ip,state,country. Only email is required, keep the commas for skipped fields. |
Drip Series (Responders)
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | The address to enroll. | |
| Series ID | series | The ID of the responder series, created and named in the admin area. |
| Field Name | Field Key | Notes |
|---|---|---|
| Start Time | starttime | Unix timestamp for when the first message should go out. During business hours message 0 goes out immediately. |
| First Name | fname | Used by the ##FNAME## placeholder. |
| Feed Name | feed | Tracking tag identifying the source of this enrollment. |
| Exclude ISPs | excludeISPs | Comma-separated ISP groups to skip. If the address belongs to one of them, enrollment is silently skipped. |
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | REQUIRED The address to remove. | |
| Series ID | series | OPTIONAL Remove from this one series. If omitted, the address is removed from all series. This does not touch the global suppression list, use the unsubscribe endpoint for that. |
| Field Name | Field Key | Notes |
|---|---|---|
| Content | content | REQUIRED Contact lines in the same format as contacts/upload. |
| Series ID | series | REQUIRED The series to enroll every line into. |
Unsubscribes and Suppression
unsub.php is wired to the ##UNSUB## placeholder and the List-Unsubscribe header, so mail clients show their native unsubscribe button. These endpoints are for unsubscribes coming from your own systems or bulk files. See bounces, complaints, and unsubscribes for the full picture.
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | REQUIRED The address to suppress. |
| Field Name | Field Key | Notes |
|---|---|---|
| Content | content | REQUIRED One email address per line. |
Broadcast Scheduling
| Field Name | Field Key | Notes |
|---|---|---|
| List ID | listID | The list (sending profile) to send to. |
| Message ID | offerid | The ID of the message to send. |
| Total To Send | totalToSend | How many emails to send today. The queue spreads them across the day. |
| Field Name | Field Key | Notes |
|---|---|---|
| Redirect Override | redirectUrl | Click redirect URL to use instead of the message's stored redirect. |
No fields. Builds today's broadcasts from your lists, one per list, rotating through the messages enabled for today's weekday.
No fields. Rebuilds the queue files and activity counts from the contact table.
Chatbot Auto Replies
chatbotApiUrl in config.php.
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | REQUIRED The address that sent the reply, and where the answer is sent. | |
| Sending Domain | domain | REQUIRED The configured sending domain to answer from. |
| Message | message | REQUIRED The incoming message text to answer. |
| Chatbot ID | chatbotID | OPTIONAL Which bot to use, defaults to the domain's setting. |
Conversions and Stats
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | REQUIRED The address that converted. |
| Field Name | Field Key | Notes |
|---|---|---|
| Date | date | OPTIONAL Day to report as m-d-Y, defaults to today. Returns delivered, bounces, opens, clicks, complaints, unsubs, the activity counts, and list sizes. |
Unsubscribe Links and Click Tracking
##UNSUB## in your HTML as the unsubscribe link. It becomes a hosted one-click unsubscribe page, and the same URL rides in the List-Unsubscribe header so mail clients show their native unsubscribe button.
Put https://yourdomain.com/click.php/##SUBID## around your links. The engine fills ##SUBID## per recipient, and the click redirects to the message's redirect URL with ##SUBID## substituted there too, so your landing page receives the tracking id.
Clicks count toward the contact's activity level, upgrading them to clicker so they get priority in future broadcasts. Known bots are sent to the defaultRedirect from config.php and are not counted. Setting botDetectProvider in config.php adds an optional LLM check on suspicious user agents.
Compliance Notes
| Rule |
|---|
| Only mail people who opted in, and honor opt-outs immediately. The suppression list, the hosted unsubscribe page, and the List-Unsubscribe header are there to keep you clean, use them. |
| CAN-SPAM and similar laws require a working unsubscribe link and your postal address in every commercial message, put both in your templates. |
| Set up SPF, DKIM, and DMARC on every sending domain with your provider before sending volume, or your mail will not reach the inbox. See the authentication guide. |
Set apiKey, adminPassword, and webhookKey to long random strings and serve over HTTPS before going live. |