Skip to content

SMS and WhatsApp (Twilio)

Paid add-on. Three transactional messages: a submission’s confirmation, a booked slot’s confirmation, and the resume link for an abandoned form. By SMS, or by WhatsApp with an approved template.

This document describes what the module guarantees and what it refuses. The user guide gives the short version, under “Sending an SMS or a WhatsApp message”.

This module does not do campaigns, and cannot do them

The three triggers each arise from a gesture by the person: they submit, they book, they ask for a link. Nowhere is there a screen, a bulk action or a route that would send to several people at once.

That is not a V1 oversight. An unsolicited SMS costs the person receiving it; in most of the countries where this plugin will be used, sending one without prior consent is an offence, and it is prosecuted against the site’s publisher.

NatureWhat is required
Transactionalnothing more: the consent is in the gesture, and the number comes from the person who typed it
Marketinga ticked box, designated in the setting; without it, nothing goes out

A marketing setting with no designated box sends nothing either, and says so: the diagnostic screen displays no_consent_field, so that you look for a missing field and not for an outage.

Opting out comes before everything

Whoever replied STOP receives nothing more, including if they tick a box the next day. An opt-out is more recent and more costly to express than a consent: ticking takes a second, replying STOP requires having received a message you did not want.

The list is not readable, and that is the point

It contains only fingerprints — never the numbers. Comparing is enough for what an opt-out asks: knowing whether that number is on it.

A displayed opt-out list would be a list of valid numbers, sorted by date, exportable with a copy-paste: exactly what it refuses to be. The screen therefore lets you check a number, add one, remove one. It does not let you browse it, because there is nothing to browse.

Twilio tells us about the STOPs, and we keep them

The module reads no replies — the plan rules that out, and reading replies would require a public endpoint nothing else justifies. It learns of them through the 21610 code Twilio returns when it refuses to deliver, and it then puts the recipient on the local list: without which every following submission would set off a refused send, and the carrier would be counting those attempts.

The list survives the erasure of the submissions. Erasing it would amount to writing again to whoever said STOP.

The number, and the zero that costs a message

Twilio accepts only the E.164 format: +33612345678. Nobody types that — you receive 06 12 34 56 78, +33 (0)6 12 34 56 78, 0033612345678.

The trap is the leading zero. In France, the United Kingdom, Belgium and elsewhere, it is not part of the international number: +330612345678 goes out with no visible error and never arrives. It is therefore removed — including in the +33 (0)6 … form, the one companies print their numbers in — except where it does belong to the number, as in Italy.

What cannot be formatted is refused, never completed: completing a national number without knowing which country it comes from would send a message to a stranger elsewhere. A form’s default country serves that purpose, and that purpose only — a number entered in international form does without it.

The module does not check that a number exists, nor that it is a mobile. That would require every carrier’s numbering database, which Twilio has and we do not; its refusal is logged with its code.

Three messages per hour per recipient

Whatever the reason, and with no setting to raise it. It is the only protection against a form submitted in a loop — by a robot, or by somebody reloading — and a setting that protects you from yourself ends up disabled.

Three are enough for a real journey: a confirmation, a slot, a resume link in the same hour. Beyond that, it is no longer a journey.

The limit counts what went out, not what was attempted: a service that is down must not end up forbidding the following sends.

One message, once

UNIQUE (submission, trigger): a submission does not receive its confirmation twice, and still receives its booking reminder. A duplicate email is forgiven; a duplicate SMS is billed, arrives on a telephone, and counts towards what the carrier considers harassment.

The row claim — UPDATE … WHERE state NOT IN ('sent','blocked') — decides who sends when two tasks cross, which happens as soon as a send takes longer than the retry interval.

The text, and what it costs

The merge tokens are the emails’: {field:id}. Sensitive fields do not resolve there — password, payment, file, signature — as for every output of the plugin.

{all_fields} is neutralised: in an SMS it would produce a message of several segments, billed as many times, and would copy into a carrier network values nobody decided to put there.

The seven-bit alphabet is not the one you think

An SMS is billed by segment: 160 characters, or 70 as soon as a single character falls outside the GSM alphabet. “é” and “à” are in it; “ç”, “œ” and the typographic apostrophe ”’” are not — and word processors substitute that last one for the straight apostrophe without saying so.

The panel therefore counts the segments as you write: “Thank you, your request has been recorded” fits in one segment; the same sentence with a typographic apostrophe costs two.

Too long: refused, not truncated

Truncating a transactional message cuts what is at the end, which is to say the resume link or the appointment time. The person then receives a message that looks complete and is in fact useless, and nobody notices. A refusal, on the other hand, shows on the diagnostic screen, with the length.

WhatsApp accepts only approved templates

Outside the conversation window, a WhatsApp message can only use a template validated by Meta, designated by its content id. Free text is refused — and when it is not, that is because the window is open, which the module cannot know since it reads no replies.

It therefore always requires a template: the only path that works every time. The text lives at Meta’s; the module supplies only the variables, numbered from 1 as Meta expects.

The failures, and which ones are replayed

ReasonReplayed
21211, 21214, 21614 — invalid number, or unable to receiveno
21408, 21265 — country not allowed on this accountno
21610 — the person replied STOPno, and they are added to the opt-out list
21617 — body too longno
63016, 63018 — WhatsApp outside an approved templateno
401, 403 — token refusedno: that is corrected on the account’s screen
429, 5xx, network cutyes — 1, 5 then 30 minutes

“Blocked” and “failed” do not say the same thing. Blocked: nothing was attempted — consent absent, opt-out, unreadable number, hourly ceiling; there is a setting to correct, or nothing to do. Failed: Twilio refused or did not answer. Conflating the two sends you looking for an outage where there is an unticked box.

“Accepted”, not “received”

Twilio takes the message on; the carrier delivers it, or does not. The module does not read delivery receipts, and calling that “delivered” would be asserting what we do not know.

What the log does not contain

Neither the number nor the message’s text. The delivery log is kept well beyond the submission; it records the API’s address, the return code and Twilio’s answer.

The sends table, meanwhile, carries only a fingerprint of the recipient: without which it would be a contacts file with the timestamp of every message received.

Resending

The submission’s detail carries a button per trigger. Resending costs a message — that is the difference from the plugin’s other resends, which correct a record in a CRM.

The checks are not bypassed: the row goes back through the opt-out list, the consent and the hourly ceiling. A button that let you force a send to somebody who said STOP would have no reason to exist.

The two events added to the other modules

Booking and recovery live elsewhere, and emitted nothing. They now each emit a public event:

  • solis_forms_pro_booking_reserved( $entry_id, $form_id, $slot );
  • solis_forms_pro_recovery_reminded( $entry_id, $form_id ).

The second is only emitted if the reminder email did go out: doubling a failed reminder with an SMS would send a reminder without the link it announces.

Going and reading those modules’ tables from this one would have tied together two paid add-ons that nothing guarantees are active together.

Schema

slf_twilio_messages: one row per submission and per trigger, with its state, its Twilio id, the recipient’s fingerprint and the reason for the last problem.

slf_twilio_optouts: one row per opted-out fingerprint, with its source — entered in the admin, or a refusal from Twilio.

The account lives in an option, token encrypted.

What V1 does not do

No two-way conversation, no reading of replies, no one-time-code verification, no campaign, and no WhatsApp message outside an approved template. The plan bounded it that way.