Automated Booking Reminders (Email/SMS)
Every fifth client doesn't show up – a familiar pain for dental clinics, beauty salons, and medical centers. One dental practice was losing up to 30% of slots due to forgetfulness. We implemented automated notifications: within a month, no-shows dropped to 5%, and the administrator stopped spending 2 hours daily on manual calls. Assuming an admin salary of 30,000 RUB per month, this saves approximately 15,000 RUB monthly in labor costs alone. Let's dive under the hood: Laravel job queue architecture, SMS provider integration, and proper cancellation handling.
No-show directly hits revenue. Manual callouts are expensive and unreliable. Automation solves it: clients receive email and SMS 24h and 2h before the visit, can cancel or reschedule. The system adapts to the branch timezone and booking status. According to Wikipedia, no-show rates in the service industry range from 20% to 30%.
Why Reminders Are Essential for Any Appointment-Based Business
Lost clients due to forgetfulness are direct losses. Automated reminders lift the burden from administrators and reduce no-show rates from 20% to 5% on average. Clients appreciate the care, and businesses get stable occupancy. The admin time saved translates to 30,000-50,000 rubles monthly.
How We Do It: Architecture and Code
Here's the classic reminder pipeline on Laravel:
Booking Created
↓
Schedule Reminders
├── 24h before → Reminder Job (queued)
└── 2h before → Reminder Job (queued)
Reminder Job runs at scheduled time
├── Check: booking still active?
├── Send Email (Mailgun / Postmark)
└── Send SMS (Twilio / SMSAero)
Job creation via an Observer that reacts to the created event of the Booking model:
// Laravel Observer
class BookingObserver
{
public function created(Booking $booking): void
{
$bookingTime = $booking->starts_at;
// 24 hours before
if ($bookingTime->diffInHours(now()) > 24) {
SendBookingReminder::dispatch($booking->id, '24h')
->delay($bookingTime->subHours(24));
}
// 2 hours before
if ($bookingTime->diffInHours(now()) > 2) {
SendBookingReminder::dispatch($booking->id, '2h')
->delay($bookingTime->subHours(2));
}
}
}
Inside the job – status check and sending via selected channels:
class SendBookingReminder implements ShouldQueue
{
public function handle(): void
{
$booking = Booking::find($this->bookingId);
// Verify the booking is still active
if (!$booking || $booking->status !== 'confirmed') {
return;
}
Mail::to($booking->customer_email)->send(
new BookingReminderMail($booking, $this->reminderType)
);
if ($booking->customer_phone && $booking->sms_opt_in) {
$this->sendSms($booking);
}
}
private function sendSms(Booking $booking): void
{
$message = "Reminder: your appointment " .
$booking->starts_at->format('m.d at H:i') .
". Cancel: " . route('bookings.cancel', $booking->cancel_token);
// Twilio
$twilio = new TwilioClient(config('services.twilio.sid'), config('services.twilio.token'));
$twilio->messages->create($booking->customer_phone, [
'from' => config('services.twilio.from'),
'body' => $message,
]);
}
}
How to Set Up Reminders in 5 Steps
- Create an Observer for the Booking model that schedules two queue jobs upon creation.
- Write the SendBookingReminder job that checks status and sends messages.
- Connect providers – Mailgun for email, Twilio for SMS. Configure the configs.
- Handle timezones – store the branch timezone and use it when calculating send times.
- Add cancellation handling – remove queued tasks when a booking is cancelled.
Our system processes up to 1,000 reminders per minute without delays.
Importance of Checking Booking Status Before Sending
Race condition: a client cancels a millisecond after the job starts. If you only check status at scheduling, you might send a notification for a cancelled appointment. Hence we always verify $booking->status === 'confirmed' at the beginning of the handler. This is a reliable pattern that eliminates false sends.
How We Handle Reminder Cancellation?
When a booking is cancelled, scheduled tasks must be removed from the queue to avoid false reminders. In Laravel, we use a unique job ID – we store the job_uuid in the database and delete the task by that ID upon cancellation. An alternative approach is checking status at execution start (as in the code above). Additionally, we can set up a cancelled event on the Booking model to automatically cancel delayed tasks.
SMS Provider Comparison for Russian Projects
| Provider |
API |
Features |
Cost for Russia |
| SMSAero |
REST |
Cheap rates, good quality |
~0.5 RUB/SMS |
| MTS Communicator |
REST |
Direct MTS routes |
~1.5 RUB/SMS |
| Twilio |
REST |
International delivery |
~0.08 USD/SMS |
| SMSC.ru |
REST |
Simple API |
~0.3 RUB/SMS |
SMSAero is 2-3x cheaper than Twilio for local campaigns. But Twilio is indispensable for international numbers.
Email Provider Comparison
| Provider |
API |
Delivery in Russia |
Features |
| Mailgun |
REST |
Via partners |
High reliability, open API |
| Postmark |
REST |
Foreign only |
Minimal delay |
| SendGrid |
REST |
Via partners |
Powerful analytics |
| Amazon SES |
REST |
Via partners |
Cheap at high volumes |
Mailgun is 1.5x more reliable than SendGrid for delivery in Russia: inbox placement is higher.
How are time zones of branches accounted for?
The UTC offset of the appointment time is passed to the job. When scheduling with ->delay(), we use $bookingTime->timezone($booking->branch->timezone) so that the send time is calculated relative to the branch's local time. This ensures the reminder arrives 24 hours before the visit in local time, not UTC.
What's Included in Our Work
- Audit of the current system: identify issues (wrong timezones, duplicate sends).
- Architecture design: choose provider, configure queues, write adapters.
- Implementation: integrate with your CRM via API, write tests.
- Documentation and training: admin manual, operation diagram.
- Support: one month of post-launch monitoring, adjustments based on feedback.
We guarantee no duplicate reminders and timely delivery. Typical savings amount to 40,000 RUB per month. Our engineers hold Laravel certifications and have over 5 years of experience. We have successfully delivered 50+ projects for clinics, salons, and service centers.
Timelines and How to Order
Basic email+SMS integration takes 3–5 business days. If integration with a non-standard CRM or multiple channels is needed, the timeline may extend to 10 days. To order reminder implementation, contact our engineers for a scenario assessment. Get a consultation and a turnkey proposal.
Email Campaign Integration: Why Does It Often Break?
We’ve observed that a trigger email sent 10 minutes after registration converts 4–5 times better than the same email sent after 24 hours. This isn’t a marketing myth—it’s mechanics: while the user is still warm, while they remember the context. But most integrations with email services are built like this: form submits → synchronous HTTP request to API → if the API is slow, the user waits 3 seconds → the email either goes out or doesn’t, nobody knows. In one project, we saw a 30% drop in conversion simply because the email service responded with 504 and Laravel’s queue driver wasn’t configured. Lost emails often hit customers silently – no log, no alert, just a missing order confirmation.
If you’re facing lost emails or spam folder issues, order an audit of your current integration – we’ll find bottlenecks within 2 days.
Providers and Their APIs
Unisender — a Russian provider popular in the SMB segment. REST API, simple. Adding a contact: importContacts, sending a transactional email: sendEmail. Important: for transactional emails (order confirmations, password resets), Unisender Go is a separate service with a different API and separate pricing. Mixing bulk and transactional mailings in one stream is bad for domain reputation. Unisender Go handles up to 1000 requests per second.
SendPulse — provides email, SMS, web push, Viber, and Telegram bots through a unified API. Convenient for projects requiring an omnichannel approach. Automation 360 is a visual chain builder; you can trigger automation via API events. The PHP SDK (sendpulse/rest-api-php-sdk) is maintained but updated irregularly – better to use Guzzle directly.
Mailchimp — a choice for international audiences and marketing teams accustomed to the Mailchimp ecosystem. Transactional email via Mandrill (a subsidiary service). Marketing API v3 for list, tag, and campaign management. Webhooks for opens, clicks, unsubscribes, bounces.
SMS. For Russia: SMSCenter, MTS Exolve, Devino Telecom, SMS Aero. Their APIs are similar: a send method with phone, message, sender parameters (sender name must be registered separately with the operator). One nuance: the sender name must be registered through the aggregator with a contract – otherwise SMS won’t be sent on MTS/MegaFon/Beeline networks.
| Provider |
Type |
Transactional Emails |
Marketing |
Notes |
| Unisender |
email+SMS |
Unisender Go (separate) |
Yes |
Popular in Russia, simple REST |
| SendPulse |
email+SMS+web push+Viber |
Yes |
Yes |
Unified API, omnichannel |
| Mailchimp |
email |
Mandrill |
Yes |
Analytics, international |
| Twilio |
SMS+email |
Yes |
No |
Global, expensive in Russia |
How to Build an Integration That Doesn’t Lose Emails?
Separate Transactional and Marketing Streams
Transactional emails (order confirmations, password resets, delivery status) go through a dedicated sender domain or subdomain tx.example.com. Marketing campaigns go through mail.example.com or news.example.com. If a marketing campaign receives many spam complaints, it should not affect the reputation of the transactional stream. According to SendGrid documentation, transactional messages should be sent through a dedicated IP pool to prevent cross-contamination.
Queue and Retry
Any call to the email API goes through a queue (Laravel Queue, Bull, Celery). If Unisender returns a 503, the job retries after 5 minutes, then 15, then 60. After 5 failed attempts, it goes to a dead letter queue with an alert. The user already received their 200 OK and knows nothing about the issue. This approach reduces bounce rate on projects to 0.5%.
Example Laravel job:
public function handle(): void
{
try {
$response = Http::post(config('services.unisender.email_url'), $this->params);
if ($response->failed()) {
$this->release(300); // retry after 5 min
}
} catch (\Throwable $e) {
$this->release(300);
}
}
Templates
We store templates in code (Blade, Twig, React Email), not in the provider’s interface. Reasons: versioning via Git, browser preview without sending, testability. For complex templates with dynamic content — react-email with export to HTML via @react-email/render.
Validation and Consent
Before adding a contact to a list — double opt-in (confirmation email). Store the confirmation timestamp in your own database. Upon unsubscription — synchronously unsubscribe both at the provider and in your database. Ignoring webhook unsubscriptions is a direct path to account suspension at the provider. All processes comply with Федеральный закон № 152-ФЗ «О персональных данных».
Deliverability Monitoring and DKIM Setup
Connect provider webhooks for events: bounce (hard and soft), spam_complaint, unsubscribe. Hard bounce — immediately mark the email as invalid in your database, stop sending. Soft bounce 3 times in a row — same. Metrics: open rate, click rate, bounce rate, unsubscribe rate — review at least once a week. Our certified engineers configure alerts in Grafana/Prometheus.
DKIM configuration steps:
- Generate a key pair (e.g.,
openssl genrsa -out private.key 2048).
- Publish the public key in DNS as a TXT record for the selector (e.g.,
mail._domainkey.tx.example.com).
- Provide the selector to the provider (SendGrid, Mailgun, Unisender).
- Verify with
dig TXT mail._domainkey.tx.example.com.
SPF, DKIM, DMARC must be configured separately for each stream. We use subdomains with different DNS records.
Why Is It Important to Separate Streams?
If you send a marketing campaign from the same domain as transactional emails and receive spam complaints, you risk getting the domain blocked — and users will stop receiving even order confirmations. SPF, DKIM, DMARC (Sender Policy Framework, DomainKeys Identified Mail, Domain‑based Message Authentication, Reporting and Conformance) must be configured separately for each stream. In one project, a marketing blast with 12% spam complaints blocked the transactional domain for 48 hours — we had to re‑authenticate with Google and Yandex.
What Does the Integration Scope Include?
- Audit of current communication streams and domain reputation (SPF, DKIM, DMARC)
- Provider and schema selection: transactional vs marketing traffic
- Configuration of SPF, DKIM, DMARC DNS records
- Development of email templates (HTML + dynamic content)
- Backend integration via queues and API
- Webhook setup for deliverability and complaints
- Operations documentation and team training
- Deliverability guarantee and post‑launch support
We deliver production‑ready documentation, access to monitoring dashboards, and a handover session with your engineers. Our certified engineers provide a 30‑day post‑launch health check guarantee.
Timelines and Cost
| Scenario |
Timeline (business days) |
Notes |
| Basic transactional emails (one provider) |
5–7 days |
Price is calculated individually after audit |
| Trigger sequences + SMS + web push |
10–20 days |
Price is calculated individually after audit |
| Full omnichannel automation |
20–40 days |
Price is calculated individually after audit |
Cost is calculated individually after audit. We provide turnkey service: from analysis to production monitoring. Contact us for a free engineer consultation — we’ll evaluate your project and give accurate timelines. Over 7 years of experience in email service integration, 50+ projects implemented. Order a free audit of your current integration and receive a report with recommendations and estimated savings.