Send SMS through a local gateway instead of Twilio by adding one filter, so every notification and OTP goes out through any provider with an HTTP API, and your code survives plugin updates.


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.

Note One filter covers everything the add-on sends: around 20 booking and listing notifications, the scheduled booking reminders, and the phone OTP used by registration and login. They all pass through the same send function.


Step 1: Put your code in the right place

Your snippet must live outside the add-on, or the next update will delete it.

  1. Recommended – a small plugin of your own, or a single file in wp-content/mu-plugins/. Files in mu-plugins load automatically and need no activation.
  2. Recommended – your child theme’s functions.php.
  3. 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.

Warning – Do not edit class-listeo-notifications.php or any other file inside the add-on. Use the filter instead. Plugin updates overwrite those files without asking.


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

  1. $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 with 00 instead of a plus is treated as international too, so 0049… 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.
  2. $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

  1. null – keep the built-in Twilio transport. Twilio runs exactly as before. This is also what happens when no callback is attached at all.
  2. 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.
  3. WP_Error – failed. Its error message is stored with the delivery log entry.
  4. An array – array( 'sent' => bool, 'id' => string, 'status' => string, 'error' => string ). sent tells Listeo whether delivery succeeded, id and status are your gateway’s own message id and status (recorded in the delivery log the same way a Twilio message SID is), and error is the reason shown when sent is false.
  5. 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_send only its sent or failed state, its message and its provider id are kept, and a skipped() result counts as not sent (logged as a deliberate skip rather than an error) because this filter has no third state to report.
  6. 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.

Note The sent key is checked with PHP’s empty(), so false, 0, '' and the string '0' all count as “not sent”.


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.

  1. Go to Listeo Core > Notifications > Logs & Debug and turn Debug Mode off.
  2. Trigger a real message, for example request a login OTP or place a test booking.
  3. 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.

Tip Test with an OTP request rather than a booking. It is the fastest round trip, it goes through the same send function, and it tells you immediately whether your gateway accepted the number format.


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 );
  1. $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.
  2. $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

Important Your callback runs on every SMS the site sends, including the login and registration OTP. If it throws or hangs, phone verification stops working. Keep the request timeout short, handle is_wp_error(), and test an OTP request before you consider the integration done.


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.