Writing an SMS Template
The text message body of an event is a single plain file with no shell around it. That makes it the shortest template to write, and the easiest one to fill with markup by accident.
Overview
Every event carries a .txt alongside its mail body, and that file is the whole message: nothing is prepended or appended. Whatever the file produces is what arrives on the handset.
It is the same engine, the same variables and the same event as the mail. The differences all follow from the channel. There is no frame to inherit context from and no markup to lean on. An account without a mobile number is not a recipient at all.
Prerequisites
user-sms or admin-sms is set in the event's configuration entry. With both at 0 the file is inert no matter what it contains.
Structure
No Shell, No Markup
The mail path concatenates a header, a body slot and a footer first. The text path skips that entirely, so the file stands alone.
Your service has been activated.
Service Information
------------------------------------
{$service_name} - {$service_amount}
Service Details
------------------------------------
{$service_detail_link}
Which File the Channel Reads
| Channel | Body file | Shell | Subject |
|---|---|---|---|
{name}.html | header, content slot, footer | subject from the .json, with variables filled in | |
| text message | {name}.txt | none | none: the .json is not read for this channel |
The engine understands a title key in the .json and would return it for the text channel. The dispatcher does not carry it further, and no shipped template declares one.
Walkthrough
1. Write the Body
- Open the event's
.txtin every installed language directory. It was created with the trio; if it is missing, the text channel has nothing to read. - Write the message as a customer would want to receive it: what happened, to which service, and where to look. One or two short lines and a link.
- Use the same placeholder syntax as the mail body. The engine is the same, so a syntax error fails the same way and leaves the whole file unrendered.
- Prefer the short variables. A description field or a ticket message is not sized for this channel even when it fits.
2. Enable the Channel
- Set
user-smsto 1 in the event's configuration entry for a message to the account holder. Setadmin-smsfor a copy to staff. - Staff numbers resolve from the event's departments plus the entry's own phone list. They fall back to the root administrator when the switch is on and nothing else resolved.
- Leave both at 0 for any event whose text version is not worth a charge. Most events ship that way on purpose.
3. Keep It Plain
- Strip anything that might carry markup at the point of printing: a ticket reply, a description, an operator-written note.
- Watch the alphabet: one character outside the basic set changes how the whole message is counted.
- Keep the wording independent of the mail. The text version is the message for a reader who will never open the mail.
4. Verify the Send
- Trigger the real flow with an account that actually has a mobile number, and check the returned status. A
no_recipientsanswer with the mail arriving normally means the number is what is missing. - Read the delivered file from the sandbox module's output directory. It records the sender name, the destination and the exact body, so trailing whitespace and stray tags become visible.
- Repeat in the other language. A text body is small enough that a missing translation is easy to overlook and produces silence rather than an error.
Reference
Length and Encoding
The notification path does not measure, split or truncate a text body. It reads the file and hands the string to the module. Any limit is the gateway's, and any cost is per message part.
That arithmetic exists in the platform, in the credit-funded international messaging panel rather than in notification templates. It is still the right model to size a template against.
| Encoding | When it applies | Single message | Per part once split |
|---|---|---|---|
| basic alphabet | every character is in the standard messaging alphabet | 160 | 153 |
| wide | one single character outside it, anywhere in the message | 70 | 67 |
What the Module Receives
A text module is handed a body, a destination and a dialling code, and is asked to submit. The notification path uses only this much of its surface.
$sms = new $smsModule();
// The already-rendered body. Passing a template name here instead is the module's
// own shortcut and is what the low-level sender uses; the dispatch never does.
$sms->body($item['body']);
// One recipient, or a list. The dialling code is a separate argument because the
// account stores it separately from the number.
$sms->addNumber($item['recipient'], $item['recipient_cc']);
$sent = $sms->submit();
if (!$sent) $error = $sms->getError();
Example
The text half of an event, next to the mail half it shares everything else with.
{$company_name}: {$service_name} has used {$quota_percent}% of its quota.
{* A condition costs nothing in the output, so the message stays one line longer
only when it has to. Comments like this one are stripped entirely. *}
{if $quota_percent >= 100}New uploads are refused until the allowance is raised.
{/if}
{$service_detail_link}
{* {$admin_reply} is the reply as it was stored: line breaks were turned into
newlines, but every other tag the editor produced is still in there. *}
Ticket {$ticket_num} has been answered.
{$admin_reply|strip_tags|truncate:120}
{$ticket_link}
// One call. Which files are read depends on the recipient's channel, and both
// channels can be produced for the same person in the same dispatch.
$result = \Notification::dispatch('service', 'acme-quota-reached', [
'entity' => $service,
'percent' => 100,
]);
// Every recipient row says which channel it was written for, so a missing text
// message is visible here rather than only in the gateway's report.
foreach ($result['items'] ?? [] as $item)
if (($item['channel'] ?? '') === 'sms')
Logger::getInstance()->info('quota notice queued as text', [
'recipient' => $item['recipient'] ?? '',
'queue_id' => $item['queue_id'] ?? 0,
]);
// Overrides the four channel switches for this call only and drops the staff copy.
// It also bypasses the recipient's category preference, so reserve it for a message
// the account explicitly asked for, such as a verification code.
$result = \Notification::dispatch('user', 'gsm-activation', [
'entity' => $userId,
'code' => $code,
'_force_channels' => ['sms'],
'_sync' => true, // deliver inline: the customer is waiting on this
]);
Pitfalls
Ticket messages are stored as the editor produced them; only the line breaks are normalised on the way out. Printing one into a text body sends the tags along, and the recipient pays for the characters. Strip at the point of printing rather than trusting the variable.
An account without a mobile number never enters the recipient list. The mail still goes out, the dispatch still reports success, and the text message that was never sent leaves no trace. When the text half seems missing, check the account before the template.
The counting switches to the wide encoding as soon as a single character falls outside the basic alphabet. The limit drops from a hundred and sixty to seventy. A translation that reads as long as its English original routinely costs twice as many parts.
A file that produces nothing is skipped silently. That is exactly right when the event has no text version, and indistinguishable from a mistake when it does. If an event should not send text, turn the channel off in its configuration entry. An empty file is not a way to say it.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.