How To Build A Custom Bulk SMS Sender With PHP And MySQL
A custom bulk SMS platform can help an Australian business send appointment reminders, delivery updates, promotions, event notices, and account alerts from one dashboard. PHP handles the application logic, while MySQL stores contacts, campaigns, delivery records, user accounts, and opt-out preferences.
The basic workflow is straightforward: an authorised user uploads or creates contacts, writes a message, selects a group, and schedules a campaign. Your PHP application then places each recipient into a queue and sends messages through an SMS gateway API. A background worker processes that queue and records the provider’s response.
A reliable system needs more than a form and a send button. It must validate mobile numbers, prevent duplicate messages, protect customer data, manage failed deliveries, and follow Australian marketing rules. The design below provides a practical foundation that can later support multiple users, billing, templates, automation, and detailed reporting.
Plan The Sender Around A Queue
Start by choosing an SMS gateway that supports Australian mobile networks and provides a documented API. Compare message pricing in Australian dollars, delivery reporting, sender ID requirements, API limits, support hours, and coverage for Telstra, Optus, and Vodafone networks. Some providers require registration before an alphanumeric sender name can be displayed reliably.
Avoid sending hundreds of messages inside the same browser request. A campaign should create queue records first, then a worker should process those records in controlled batches. This keeps the dashboard responsive and makes it easier to retry temporary failures without sending the whole campaign again.
Your application can have four basic roles: administrator, campaign manager, operator, and viewer. An administrator manages gateway credentials and users, while an operator can prepare campaigns without seeing sensitive account settings. This separation is useful for agencies handling SMS marketing for clients in Sydney, Melbourne, Brisbane, or Perth.
Core components to include
- PHP application with authentication and role permissions
- MySQL database for contacts, campaigns, queues, and logs
- SMS gateway API using HTTPS and protected credentials
- Cron job or worker process for scheduled message delivery
Useful controls before sending
- Duplicate-number detection and phone-number validation
- Campaign preview showing recipients and estimated message count
- Test message option for an authorised internal number
- Pause, cancel, retry, and delivery-report functions
Design The MySQL Database
A relational structure keeps contact management and campaign reporting organised. Use separate tables for contacts, groups, campaigns, queue items, and delivery events rather than storing a comma-separated list of numbers in one campaign row. This allows one contact to belong to several groups and gives you an accurate audit trail.
For Australian numbers, normalise mobile numbers to international format before storing them. A local number such as 0412 345 678 can become 61412345678, while the plus sign can be added only when building the provider request. Never assume every number beginning with 04 is valid; validate length and digits, then let the gateway confirm final deliverability.
A compact schema might look like this:
CREATE TABLE contacts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
phone VARCHAR(20) NOT NULL UNIQUE,
opted_out TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE campaigns (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(160) NOT NULL,
message TEXT NOT NULL,
status ENUM('draft','queued','sending','complete','cancelled')
NOT NULL DEFAULT 'draft',
scheduled_at DATETIME NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE sms_queue (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
campaign_id INT UNSIGNED NOT NULL,
contact_id INT UNSIGNED NOT NULL,
status ENUM('pending','sent','failed','cancelled')
NOT NULL DEFAULT 'pending',
provider_id VARCHAR(120) NULL,
error_message VARCHAR(255) NULL,
attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
sent_at DATETIME NULL,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
FOREIGN KEY (contact_id) REFERENCES contacts(id)
);
Add indexes to status, scheduled_at, and campaign_id as the database grows. A separate unsubscribe_events table is also valuable because it records when and how a person opted out. Store gateway responses in a restricted log table, and encrypt backups because phone numbers and message content can be personal information.
Build The PHP Sending Layer
Use PDO with prepared statements for every database query. The campaign form should check the authenticated user, validate the message length, verify that the campaign has recipients, and create queue items only for contacts whose opted_out value is zero. Keep the gateway key in environment variables or a secrets manager rather than placing it in a public PHP file.
The sender should claim a small batch of pending records, mark them as processing or increment their attempt count, and then call the provider. If the gateway supports bulk requests, use its batch endpoint; otherwise, send individual requests with a sensible delay. The precise limit depends on your provider contract, so follow its rate policy rather than assuming that a fast loop is safe.
A simplified gateway method could be structured like this:
function sendSms(string $to, string $message): array
{
$payload = json_encode([
'to' => $to,
'message' => $message,
'sender' => $_ENV['SMS_SENDER_ID']
]);
$ch = curl_init($_ENV['SMS_API_URL']);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $_ENV['SMS_API_KEY']
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error || $code < 200 || $code >= 300) {
return ['ok' => false, 'error' => $error ?: 'Gateway request failed'];
}
return ['ok' => true, 'response' => json_decode($body, true)];
}
In production, never expose raw gateway errors to ordinary dashboard users. Convert them into clear statuses such as invalid recipient, insufficient balance, rate limited, or temporary provider failure. Retry transient errors with a maximum attempt count, while permanent failures should be recorded and excluded from automatic retries.
Add Consent Delivery And Reporting
Australian commercial SMS campaigns must follow the Spam Act 2003. Obtain consent before sending marketing messages, identify the sender clearly, and provide a functional unsubscribe method. A message containing an easy instruction such as “Reply STOP to opt out” needs a process that receives the reply through your provider’s inbound webhook and updates the contact immediately.
Time zones require careful handling. Australia has several zones, and daylight saving affects New South Wales, Victoria, Tasmania, South Australia, and the Australian Capital Territory, while Queensland, Western Australia, and the Northern Territory follow different arrangements. Store campaign times in UTC, convert them for the account’s configured location, and avoid sending promotional messages at unsuitable local hours.
Keep transactional and promotional traffic separate. A parcel notification may have a different consent basis from a sale announcement, and your database should record the message category. For a local retailer serving Melbourne customers and a Perth customer base, separate time-zone settings can prevent a campaign from arriving at an unreasonable hour.
Useful reporting includes submitted, delivered, failed, expired, and unsubscribed counts. Ask the SMS provider to send delivery receipts to a signed webhook endpoint. Verify the webhook signature, reject duplicate event IDs, and update records idempotently so a repeated callback does not distort campaign totals.
Secure And Launch The Dashboard
Protect every form against cross-site request forgery, escape displayed content to prevent cross-site scripting, and apply password hashing with PHP’s password_hash() function. Use HTTPS across the dashboard and API endpoints. Apply login throttling, session regeneration, two-factor authentication for administrators, and least-privilege database credentials.
CSV imports deserve special care. Limit file size, validate the expected columns, reject formulas that could create spreadsheet injection problems, and show a preview before saving rows. A contact import should report invalid, duplicate, opted-out, and accepted records separately instead of silently discarding data.
Before launch, test with a small internal group and several Australian mobile numbers. Confirm sender display, Unicode messages, long-message splitting, delivery receipts, STOP handling, queue recovery, and duplicate prevention. Check that a failed worker can restart without resending records already marked as sent.
A useful control panel should show balance warnings, queue size, recent errors, scheduled campaigns, and opt-out activity. If you advertise your service to local businesses, a polished dashboard with clear usage reports can make the platform easier to demonstrate and manage for clients.
Begin with a low-volume pilot, monitor gateway responses, and review consent records before enabling large campaigns. Then add reusable templates, contact segmentation, API access, client billing, and automated reminders as real usage reveals what the platform needs. A carefully queued PHP and MySQL sender can become a dependable communication tool while keeping Australian compliance and customer trust at the centre.