Need An Online Store? Hire A Developer Business Legal Documents Better Images/Video Want More Sales?
Need An Online Store? Want More Sales?

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.

In all URLs below, replace yourdomain.com with the domain where you installed the engine. Prefer a managed version? The hosted Email Broadcast app runs this same engine for you, contact our team to get set up.

What the Engine Is

One PHP app, one SQLite file, 11 email providers over plain SMTP - read this first
Show / Hide
Email Campaign Engine is a self-hosted email campaign system: bulk broadcasts, drip autoresponder series, delivery and engagement webhooks, click tracking, and unsubscribe management, working with 11 email providers over plain SMTP. It is one small PHP application storing everything in a single SQLite file, with no framework, no Composer, and no dependencies. The source is MIT licensed at github.com/AIAppsAPI/Email-Campaign-Engine. Sending goes through a built-in dependency-free SMTP client (SSL and STARTTLS), so any standard SMTP relay works for sending even if it is not one of the 11 providers with webhook parsers.

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:

FileWhat it does
public/api.phpThe JSON API. Every endpoint documented below lives here.
public/webhook.phpProvider callbacks for delivery and engagement events.
public/click.phpTracked click redirect with bot filtering.
public/unsub.phpHosted one-click unsubscribe page.
public/admin.phpPassword protected admin area: domains, messages, lists, series, contacts, reports, settings, SMTP test.
cli/cron.phpScheduled sending entry point, run by cron or the Docker cron container.
config.phpCopied from config.sample.php, holds your keys and settings.
data/The SQLite database and queue files, created automatically.

Quick Start with Docker

Two commands, cron included
Show / Hide
cp config.sample.php config.php # edit config.php: set apiKey, adminPassword, webhookKey, publicUrl docker compose up --build

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

Upload, point the docroot, add one cron line
Show / Hide

For a quick local test:

cp config.sample.php config.php # edit config.php: set apiKey, adminPassword, webhookKey, publicUrl php -S localhost:8080 -t public

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.

Before going live: set 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

One crontab line runs everything on time
Show / Hide

Everything scheduled goes out through cli/cron.php. Add one crontab line:

*/10 * * * * php /path/to/Email-Campaign-Engine/cli/cron.php

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:

StepWhat happens
1Sends autoresponder messages that are due.
2Sends the current hour's bucket of today's broadcasts.
3Once per day, when the auto scheduler is on in the admin settings, builds today's broadcasts from your lists.

Setup Walkthrough

From empty install to first broadcast
Show / Hide
StepWhat to do
1In config.php set apiKey, adminPassword, webhookKey, and publicUrl.
2Log 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.
3Send yourself a test email from the dashboard to confirm SMTP works.
4Point your provider's event webhooks at your webhook URL (see the webhook section below).
5Create 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##.
6Upload contacts in the admin area or push them through the API.
7Create 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.
8For 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

Host, username, and password for each of the 11 providers
Show / Hide
When you add a sending domain in the admin area, you enter your provider's SMTP host, username, and password. Use port 465 for SSL or 587 for STARTTLS, both work. Any other standard SMTP relay also works for sending, you just will not get its webhook events.
ProviderSMTP hostUsernamePassword
SendGridsmtp.sendgrid.netapikeyyour SendGrid API key
Mailgunsmtp.mailgun.orgpostmaster@your-domainMailgun SMTP password
Postmarksmtp.postmarkapp.comserver tokenserver token
Amazon SESemail-smtp.us-east-1.amazonaws.com (region specific)SES SMTP usernameSES SMTP password
SMTP2GOmail.smtp2go.comSMTP2GO usernameSMTP2GO password
Brevosmtp-relay.brevo.comaccount emailBrevo SMTP key
SparkPostsmtp.sparkpostmail.comSMTP_InjectionSparkPost API key
Elastic Emailsmtp.elasticemail.comaccount emailElastic Email API key
Mailtraplive.smtp.mailtrap.ioapiMailtrap sending token
Mailjetin-v3.mailjet.comMailjet API keyMailjet secret key
SMTP.comsend.smtp.comaccount usernameSMTP.com password

Provider Webhooks

Delivered, bounces, opens, clicks, complaints, unsubscribes
Show / Hide
Point each provider's event webhooks at your install's webhook endpoint. The engine parses delivered, soft and hard bounces, opens, clicks, complaints, and unsubscribes as reports come in. Hard bounces, spam complaints, and unsubscribes suppress the address everywhere automatically.
POST Webhook URL pattern
https://yourdomain.com/webhook.php/{provider}?key=YOURWEBHOOKKEY

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.

Amazon SES: subscribe the webhook URL to your SNS topic, the engine confirms the SNS subscription automatically.

How the API Works

Authentication and request format - read before the endpoints
Show / Hide
All endpoints are POST with a JSON body. Send your key in the 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.
POST Example request
curl -X POST https://yourdomain.com/api.php/send \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_KEY" \ -d '{"email": "amy@example.com", "domain": "mail.yourbrand.com", "subject": "Hello", "html": "<p>Hello from the engine</p>"}'

Send a Single Email

api.php/send
Show / Hide
Sends one email right now through the sending domain's SMTP settings. The domain must already be configured in the admin area. For sending to a whole list, use the broadcast endpoints or the dashboard instead.
POST Endpoint
https://yourdomain.com/api.php/send
Required Fields
Field NameField KeyNotes
Email AddressemailThe recipient address.
Sending DomaindomainMust match a configured sending domain. The from address becomes the domain's alias at this domain.
SubjectsubjectThe email subject line.
HTML BodyhtmlThe HTML content of the message.

Add and Upload Contacts

api.php/contacts/add | api.php/contacts/upload
Show / Hide
contacts/add adds one contact, wire it to your signup form. contacts/upload bulk imports many contacts in one call. Contacts are stored with full profile information and organized by ISP group.
POST Add one contact
https://yourdomain.com/api.php/contacts/add
Required Fields
Field NameField KeyNotes
Email AddressemailThe contact's email address.
Optional Fields
Field NameField KeyNotes
First NamefnameFirst name only, used by the ##FNAME## placeholder.
Last NamelnameLast name only.
Feed NamefeedA short code identifying which data source or campaign this contact came from. Used for tracking and filtering. Example: webform-jan
StatestateTwo-letter US state code. Example: NY, FL
CountrycountryTwo-letter country code. Example: US
Signup URLsourceurlThe page where this contact opted in.
Opt-In DateoptinDate and time the contact opted in. Example: 05-01-2025 05:45:00
IP AddressipIPv4 or IPv6 address at the time of opt-in.

POST Bulk import
https://yourdomain.com/api.php/contacts/upload
Required Fields
Field NameField KeyNotes
ContentcontentOne 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)

api.php/responder/create | responder/unsub | responders/upload
Show / Hide
A responder series is a drip sequence: each subscriber gets their own independent schedule starting from when they enrolled, on the daily, weekly, or monthly rhythm you configured for the series in the admin area. Quiet hours and skip days are respected automatically. See the drip campaign setup guide for the full walkthrough.
POST Subscribe an address to a series
https://yourdomain.com/api.php/responder/create
Required Fields
Field NameField KeyNotes
Email AddressemailThe address to enroll.
Series IDseriesThe ID of the responder series, created and named in the admin area.
Optional Fields
Field NameField KeyNotes
Start TimestarttimeUnix timestamp for when the first message should go out. During business hours message 0 goes out immediately.
First NamefnameUsed by the ##FNAME## placeholder.
Feed NamefeedTracking tag identifying the source of this enrollment.
Exclude ISPsexcludeISPsComma-separated ISP groups to skip. If the address belongs to one of them, enrollment is silently skipped.

POST Remove an address from a series
https://yourdomain.com/api.php/responder/unsub
Field NameField KeyNotes
Email AddressemailREQUIRED The address to remove.
Series IDseriesOPTIONAL 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.

POST Bulk subscribe
https://yourdomain.com/api.php/responders/upload
Field NameField KeyNotes
ContentcontentREQUIRED Contact lines in the same format as contacts/upload.
Series IDseriesREQUIRED The series to enroll every line into.

Unsubscribes and Suppression

api.php/unsub | api.php/unsubs/upload | unsub.php
Show / Hide
Unsubscribing an address removes it from broadcasts and all series and adds it to the suppression list, which survives re-imports. Hard bounces, spam complaints, and webhook unsubscribes suppress automatically. The hosted one-click unsubscribe page at 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.
POST Unsubscribe one address from everything
https://yourdomain.com/api.php/unsub
Field NameField KeyNotes
Email AddressemailREQUIRED The address to suppress.

POST Bulk unsubscribe
https://yourdomain.com/api.php/unsubs/upload
Field NameField KeyNotes
ContentcontentREQUIRED One email address per line.

Broadcast Scheduling

api.php/broadcast/schedule | broadcast/autoschedule | broadcast/dataquery
Show / Hide
broadcast/schedule queues a broadcast for today: the engine fills the queue from the list, engaged contacts first, and spreads sending across the business day, which cron sends as each hour comes up. broadcast/autoschedule runs the auto scheduler immediately instead of waiting for the daily cron pass. broadcast/dataquery rebuilds the queue files and activity counts from the contact table, and runs automatically before scheduling when the data is older than a day. See how to send bulk email for the full walkthrough.
POST Queue a broadcast for today
https://yourdomain.com/api.php/broadcast/schedule
Required Fields
Field NameField KeyNotes
List IDlistIDThe list (sending profile) to send to.
Message IDofferidThe ID of the message to send.
Total To SendtotalToSendHow many emails to send today. The queue spreads them across the day.
Optional Fields
Field NameField KeyNotes
Redirect OverrideredirectUrlClick redirect URL to use instead of the message's stored redirect.

POST Run the auto scheduler now
https://yourdomain.com/api.php/broadcast/autoschedule

No fields. Builds today's broadcasts from your lists, one per list, rotating through the messages enabled for today's weekday.


POST Rebuild queue data
https://yourdomain.com/api.php/broadcast/dataquery

No fields. Rebuilds the queue files and activity counts from the contact table.

Chatbot Auto Replies

api.php/chatbot/respond
Show / Hide
Forward an incoming reply to the chatbot hook and the bot's answer is emailed back automatically. Works with any chat API speaking the same format, such as a self-hosted AbraTabia AI Storyteller install. Requires chatbotApiUrl in config.php.
POST Endpoint
https://yourdomain.com/api.php/chatbot/respond
Field NameField KeyNotes
Email AddressemailREQUIRED The address that sent the reply, and where the answer is sent.
Sending DomaindomainREQUIRED The configured sending domain to answer from.
MessagemessageREQUIRED The incoming message text to answer.
Chatbot IDchatbotIDOPTIONAL Which bot to use, defaults to the domain's setting.

Conversions and Stats

api.php/convert | api.php/stats
Show / Hide
convert marks an address as a converter so it gets top priority in future broadcasts, call it from your checkout or lead endpoint. stats returns the day's numbers for your reporting.
POST Mark a conversion
https://yourdomain.com/api.php/convert
Field NameField KeyNotes
Email AddressemailREQUIRED The address that converted.

POST Daily stats
https://yourdomain.com/api.php/stats
Field NameField KeyNotes
DatedateOPTIONAL 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##, ##SUBID##, and bot filtering
Show / Hide
Put ##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

Opt-ins, CAN-SPAM, authentication
Show / Hide
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.