Before you start
This part is for developers. It needs a small code snippet, so skip it if Twilio works for you.
Twilio works almost everywhere, but local and domestic SMS gateways are often far cheaper for a site that only sends to one country. Listeo Notifications & OTP Verification 2.0 adds two filters that let you plug in any gateway with an HTTP API. Twilio stays the default provider: nothing changes on your site unless you add the snippet below.
You will need:
- Listeo Notifications & OTP Verification 2.0 or later. The filters do not exist in earlier versions.
- An SMS gateway with an HTTP API, plus its endpoint URL and credentials.
- Someone comfortable writing a few lines of PHP.
Step 1: Put your code in the right place
Your snippet must live outside the add-on, or the next update will delete it.
- Recommended – a small plugin of your own, or a single file in
wp-content/mu-plugins/. Files inmu-pluginsload automatically and need no activation. - Recommended – your child theme’s
functions.php. - Never – inside the Listeo Notifications & OTP Verification plugin folder. Editing the add-on’s own files means losing the change on every update, which is exactly the problem this filter solves.
Step 2: Add the filter
The filter is listeo_sms_pre_send. Return null and Listeo keeps using Twilio exactly as before. Return anything else and Twilio is never called.
Paste this into the file you created in Step 1, then replace the endpoint and credentials with the ones from your gateway.
<?php
add_filter( 'listeo_sms_pre_send', function( $result, $to, $body ) {
$endpoint = 'https://gateway.example.com/api/send';
$username = 'my-user';
$token = 'my-token';
$response = wp_remote_post( $endpoint, array(
'timeout' => 15,
'headers' => array(
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'username' => $username,
'to' => ltrim( $to, '+' ),
'message' => $body,
) ),
) );
if ( is_wp_error( $response ) ) {
return array(
'sent' => false,
'status' => 'error',
'error' => $response->get_error_message(),
);
}
$code = wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 200 !== $code ) {
return array(
'sent' => false,
'status' => 'error',
'error' => isset( $data['message'] ) ? $data['message'] : 'HTTP ' . $code,
);
}
return array(
'sent' => true,
'id' => isset( $data['message_id'] ) ? $data['message_id'] : '',
'status' => 'sent',
'error' => '',
);
}, 10, 3 );
What your callback receives
- $to – the destination number, already normalized by Listeo, so you do not repeat that work. International input, and any national number that your Default country code setting can complete, arrives in E.164 with a leading plus, for example
+972501234567. A number written with00instead of a plus is treated as international too, so0049…resolves even with no default country code set. If a visitor types a national number without a country code and the site has no default country code configured, the number is passed through with every non-digit character removed. That is the same value Twilio would have received, so if your gateway requires E.164, check for the leading plus yourself. - $body – the finished message text, with all merge tags already replaced.
If your gateway expects a local format rather than E.164, strip the prefix in your callback, for example ltrim( $to, '+' ).
What your callback can return
- null – keep the built-in Twilio transport. Twilio runs exactly as before. This is also what happens when no callback is attached at all.
- true or false – the message was sent, or it failed. Enough if you do not need the delivery log to show your gateway’s own message id.
- WP_Error – failed. Its error message is stored with the delivery log entry.
- An array –
array( 'sent' => bool, 'id' => string, 'status' => string, 'error' => string ).senttells Listeo whether delivery succeeded,idandstatusare your gateway’s own message id and status (recorded in the delivery log the same way a Twilio message SID is), anderroris the reason shown whensentis false. - Listeo_Notification_Result – accepted as well, so a callback written for the WhatsApp filter below cannot misfire here. On the WhatsApp filter the result is used exactly as it is. On
listeo_sms_pre_sendonly its sent or failed state, its message and its provider id are kept, and askipped()result counts as not sent (logged as a deliberate skip rather than an error) because this filter has no third state to report. - Any other value – treated as a failed send. Listeo never assumes that an unrecognized value means success, so a mistake in your callback appears in the delivery log instead of producing an OTP that was never delivered.
Step 3: Turn Debug Mode off and send a test
Debug Mode is evaluated before the filter. While it is on, every message is simulated and logged and your gateway is never contacted, so you cannot test a custom gateway with Debug Mode enabled.
- Go to Listeo Core > Notifications > Logs & Debug and turn Debug Mode off.
- Trigger a real message, for example request a login OTP or place a test booking.
- Come back to Logs & Debug and check the Delivery history table.
Where to see the result
The Delivery history table lists Time, Event, Recipient, Channel and Status as columns. The provider message id and the error text your callback returned are behind View details in the Details column.
Sending WhatsApp through your own gateway
WhatsApp has its own send path, so it has its own filter: listeo_notification_whatsapp_pre_send. It runs before both the Twilio and the Meta Cloud API WhatsApp providers.
It takes one argument more than the SMS filter, so register it with 10, 4:
add_filter( 'listeo_notification_whatsapp_pre_send', function( $result, $to, $body, $context ) {
// Same return values as listeo_sms_pre_send.
return null;
}, 10, 4 );
- $context – holds the merge-tag values used to build the message, and
array( 'test' => true )when the message comes from the admin test button. Use it if your gateway needs extra data about the message. - $to – always valid E.164 with a leading plus here. The WhatsApp channel rejects anything else before the filter runs, so the “no default country code” case cannot reach it.
The accepted return values are the same as for listeo_sms_pre_send, and the outcome is logged the same way.
Troubleshooting
-
Messages do not seem to go anywhere
-
The delivery log shows a failure although you think the callback worked
-
Your gateway rejects the number
-
Nothing you do has any effect
Other developer hooks
This add-on exposes more hooks than the two above, including listeo_sms_normalize_phone for country-specific numbering rules, listeo_sms_otp_request_allowed and listeo_sms_otp_global_hourly_limit for OTP rate limiting, listeo_notification_channels and listeo_notification_channel_allowed for the channel layer, and listeo_notification_whatsapp_meta_payload for approved WhatsApp templates. They are documented in the Listeo Developer Hooks Reference.