SMS Campaign Engine - Documentation
Setup guide and full API reference for the open source, self-hosted SMS Campaign Engine.
Get the code on GitHub or read the SMS Broadcast overview first.
What the Engine Is
Requirements: PHP 8.1 or newer with the curl and pdo_sqlite extensions, both 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 reports and incoming replies. |
| public/click.php | Tracked click redirect with bot filtering. |
| public/admin.php | Password protected admin area: numbers, messages, lists, series, contacts, reports, settings. |
| 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 and serve over HTTPS.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 number with your provider and API key (formats in the provider section below). |
| 3 | Point your provider's webhooks (incoming messages and delivery reports) at your webhook URL (see the webhook section below). |
| 4 | Create a message. Placeholders: ##FNAME## first name, ##SUBID## click tracking id, ##DOMAIN## the number's sending domain, ##INCOMING## the reply-to number. A tracked link looks like https://yourdomain.com/click.php/##SUBID##. |
| 5 | Upload contacts in the admin area or push them through the API. |
| 6 | 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. |
| 7 | For drips, create a series and subscribe phones 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 AI auto replies.
Provider API Key Formats
| Provider | api_key value |
|---|---|
| twilio | AccountSid:AuthToken |
| telnyx | API key |
| vonage | api_key:api_secret |
| plivo | auth_id:auth_token |
| sinch | service_plan_id:api_token |
| bandwidth | username:password:accountId:applicationId |
| clicksend | username:api_key |
| messagebird | Access key |
| textmagic | username:api_key |
| clickbuzz | API key |
| volt | Bearer token |
| ahoi | API key |
Provider Webhooks
where {provider} is one of: twilio, telnyx, vonage, plivo, sinch, bandwidth, clicksend, messagebird, textmagic, clickbuzz, volt, ahoi. The key is the webhookKey from your config.php.
Delivery callbacks record delivered and undelivered counts and learn each phone's carrier as reports come in. Incoming replies are checked for STOP, and a sending number can instead route replies to the chatbot hook for automatic answers, see AI text message auto replies.
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.
tmobile, att, verizon, uscellular, boost, other. Always use these exact lowercase values. If you do not know the carrier, pass other.Send a Single SMS Message
| Field Name | Field Key | Notes |
|---|---|---|
| Phone Number | phone | 10-digit US phone number, no plus sign, no country code. Example: 5551234567 |
| Sending Number | sendingNumber | The number the message comes from. Must match a configured sending number. 10-digit format. |
| Message | message | The text to send. Standard SMS length limits apply (160 characters for a single segment), longer messages send as multi-part. |
Add and Upload Contacts
| Field Name | Field Key | Notes |
|---|---|---|
| Phone Number | phone | 10-digit US phone number. |
| Field Name | Field Key | Notes |
|---|---|---|
| Email Address | Contact email address. | |
| 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 |
| Carrier | carrier | One of the carrier values from the API basics section. Use other if unknown. |
| 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,sourceurl,optin,phone,fname,country,state,ip,feed,carrier,unused,lname. Only phone is required, keep the commas for skipped fields. |
Drip Series (Responders)
| Field Name | Field Key | Notes |
|---|---|---|
| Phone Number | phone | 10-digit US phone number 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. When the subscriber's local time of day allows it, message 0 goes out immediately. |
| First Name | fname | Used by the ##FNAME## placeholder. |
| Feed Name | feed | Tracking tag identifying the source of this enrollment. |
| Carrier | carrier | One of the carrier values from the API basics section. |
| Exclude Carriers | excludeCarriers | Comma-separated carriers to skip. If the subscriber's carrier is in this list, enrollment is silently skipped. Example: verizon,tmobile |
| Field Name | Field Key | Notes |
|---|---|---|
| Phone Number | phone | REQUIRED The phone to remove. |
| Series ID | series | OPTIONAL Remove from this one series. If omitted, the phone 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
| Field Name | Field Key | Notes |
|---|---|---|
| Phone Number | phone | REQUIRED 10-digit US phone number to suppress. |
| Field Name | Field Key | Notes |
|---|---|---|
| Content | content | REQUIRED One phone number 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 messages to send today. The queue spreads them across hour buckets through the day. |
| Field Name | Field Key | Notes |
|---|---|---|
| Message Override | message | Message text to use instead of the stored message content. |
| 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.
Conversions and Stats
| Field Name | Field Key | Notes |
|---|---|---|
| Phone Number | phone | REQUIRED The phone that converted. |
| Field Name | Field Key | Notes |
|---|---|---|
| Date | date | OPTIONAL Day to report as m-d-Y, defaults to today. Returns delivered, undelivered, clicks, unsubs, the activity counts, and list sizes. |
Click Tracking and Bot Filtering
https://yourdomain.com/click.php/##SUBID## in your message text. 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 and enabling the setting also asks an LLM about suspicious user agents before counting the click.
Compliance Notes
| Rule |
|---|
| Only message people who opted in, and honor opt-outs immediately. The engine's STOP handling, suppression list, and unsub uploads are there to keep you clean, use them. |
| US carriers require registered campaigns (10DLC, toll-free verification, or short codes) for application traffic. Register your numbers with your provider before sending volume. See the 10DLC registration guide. |
| Include opt-out language, like "Reply STOP to opt out", in your messages. The TCPA compliance guide covers the consent rules in depth. |
Set apiKey, adminPassword, and webhookKey to long random strings and serve over HTTPS before going live. |