Custom module to send SMS messages from Suitecrm - sharing my code

I built a custom module with logic hooks to send SMS messages from Suitecrm. It uses an external service. Hope someone can find the code helpful. I’m fairly new to Suitecrm so might not be the most well written code, but it works!! Lots of logging to be helpful…


text/x-generic send_sms_hook.php ( PHP script, ASCII text, with CRLF line terminators )
<?php

class SendSMSHook
{
   
    public static function sms_log($message)
    {
        $logFile = 'custom/modules/CuSMS_SMS/sms_hook.log';
        $date = date('Y-m-d H:i:s');
        file_put_contents($logFile, "[$date] $message\n", FILE_APPEND);
    }

public function sendSMS($bean, $event, $arguments)
{
    self::sms_log("Hook triggered for record {$bean->id}");
    
    
    if (!empty($bean->contact_id_c)) {
        $contact = BeanFactory::getBean('Contacts', $bean->contact_id_c);
        if ($contact && !empty($contact->phone_work)) {
            $bean->phone_number_cs = $contact->phone_work;
            self::sms_log("Auto-filled phone_number_cs from related contact: {$contact->phone_work}");
        } else {
            self::sms_log("Related contact found, but no phone_work field.");
        }
    } else {
        self::sms_log("No contact_id_c found on record.");
    }


if (!empty($bean->account_id_c)) {  
    $account = BeanFactory::getBean('Accounts', $bean->account_id_c);
    if ($account && !empty($account->phone_office)) {
        $bean->phone_number_cs = $account->phone_office;
        self::sms_log("Auto-filled phone_number_cs from related account: {$account->phone_office}");
    } else {
        self::sms_log("Related account found, but no phone_office field.");
    }
} else {
    self::sms_log("No account_id_c found on record.");
}

    $recipient = $bean->phone_number_cs;
    $message   = $bean->message_cs;

    self::sms_log("Recipient phone number is '{$recipient}'");

    if (empty($recipient)) {
        self::sms_log("No recipient phone number on record {$bean->id}");
        return;
    }


    $baseUrl  = 'MYURL';
    $deviceId = '';
    $apiKey   = 'APIKEY';

    $url = $baseUrl . "gateway/devices/{$deviceId}/send-sms";

    $data = json_encode([
        'recipients' => [$recipient],
        'message'    => $message
    ]);

    $headers = [
        "x-api-key: $apiKey",
        "Content-Type: application/json"
    ];


    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    $error    = curl_error($ch);
    curl_close($ch);


    if ($error) {
        self::sms_log("cURL error sending SMS: $error");
    } else {
        self::sms_log("SMS sent successfully to $recipient. Response: $response");
    }
}

    public function sendBulkSMSFromTargetList($bean, $message)
    {
        $targetListName = $bean->target_list_id_c;
        self::sms_log("Target List Name (from target_list_id_c): {$targetListName}");

        if (empty($targetListName)) {
            self::sms_log("No target list name set in the SMS record.");
            return;
        }

        $targetListBean = BeanFactory::newBean('ProspectLists');
        $targetListBeanList = $targetListBean->get_full_list("name", "name = '{$targetListName}'");

        if (empty($targetListBeanList)) {
            self::sms_log("No ProspectList found with name: {$targetListName}");
            return;
        }

        $targetListBean = $targetListBeanList[0];
        self::sms_log("Found Target List: {$targetListBean->name} (ID: {$targetListBean->id})");

        $contactIds = [];

        if ($targetListBean->load_relationship('prospects')) {
            $prospects = $targetListBean->prospects->getBeans();
            self::sms_log("Retrieved " . count($prospects) . " prospects.");
            foreach ($prospects as $prospect) {
                if (!empty($prospect->phone_work)) {
                    $contactIds[] = ['id' => $prospect->id, 'type' => 'Prospect', 'number' => $prospect->phone_work];
                } else {
                    self::sms_log("Prospect {$prospect->id} has no phone_work number.");
                }
            }
        } else {
            self::sms_log("Failed to load 'prospects' relationship on target list.");
        }

        if ($targetListBean->load_relationship('contacts')) {
            $contacts = $targetListBean->contacts->getBeans();
            self::sms_log("Retrieved " . count($contacts) . " contacts.");
            foreach ($contacts as $contact) {
                if (!empty($contact->phone_work)) {
                    $contactIds[] = ['id' => $contact->id, 'type' => 'Contact', 'number' => $contact->phone_work];
                } else {
                    self::sms_log("Contact {$contact->id} has no phone_work number.");
                }
            }
        } else {
            self::sms_log("Failed to load 'contacts' relationship on target list.");
        }

        if ($targetListBean->load_relationship('leads')) {
            $leads = $targetListBean->leads->getBeans();
            self::sms_log("Retrieved " . count($leads) . " leads.");
            foreach ($leads as $lead) {
                if (!empty($lead->phone_work)) {
                    $contactIds[] = ['id' => $lead->id, 'type' => 'Lead', 'number' => $lead->phone_work];
                } else {
                    self::sms_log("Lead {$lead->id} has no phone_work number.");
                }
            }
        } else {
            self::sms_log("Failed to load 'leads' relationship on target list.");
        }

        self::sms_log("Total contacts to send SMS to: " . count($contactIds));

        foreach ($contactIds as $entry) {
            self::sms_log("Sending SMS to {$entry['type']} ID {$entry['id']} at {$entry['number']}");
            $this->sendSingleSMS($entry['number'], $message);
        }

        self::sms_log("=== Bulk SMS processing complete ===");
    }

    public function sendSingleSMS($phoneNumber, $message)
    {
        self::sms_log("Preparing to send SMS to {$phoneNumber}");

        $baseUrl  = '';
        $deviceId = '';
        $apiKey   = '';

        $url = $baseUrl . "gateway/devices/{$deviceId}/send-sms";

        $data = json_encode([
            'recipients' => [$phoneNumber],
            'message'    => $message
        ]);

        $headers = [
            "x-api-key: $apiKey",
            "Content-Type: application/json"
        ];

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);
        $error    = curl_error($ch);
        curl_close($ch);

        if ($error) {
            self::sms_log("cURL error sending SMS: $error");
        } else {
            self::sms_log("SMS sent successfully to {$phoneNumber}. Response: {$response}");
        }
    }
}
4 Likes

Another approach is to use the hook to send a webhook to n8n and have n8n process the SMS.

1 Like

Please how to use this code ?
How can I install it in my SuiteCRM and start sending SMS from the CRM ?
I am not a programmer

For non-programmers, it’s usually best to stick to extensions.

If you want to integrate your CRM with more processes, low code platforms like n8n are the next easiest option. It is still a learning curve, but you’ll have plenty of options going forward.

I’ve got a whole series of videos on how to integrate SuiteCRM with n8n and other application:

(including the initial setup that you’ll need to go through one time).

A preview on an SMS sending video from SuiteCRM is here:


I’ll record and publish it sooner or later. Here is already the n8n screenshot showing the principle.

The script from above requires several further details like a custom module, placing the file correctly and adding some more specific integrations / API key.
It’s not possible to ā€˜plug n play’ this script.

2 Likes

Hiya, big fan. I’ve watched most of your videos on Suitecrm! Yeah a webhook probably would have been the smoothest way out of Suitecrm. I was using curl in the command line to test all the links so I was already 50% of the way there for the integration. It just made sense for me to replicate the code I already had.

I was avoiding n8n as I wanted something free and utilising more open source projects.

Hiya, also a big fan. I’ve watched most of your videos on Suitecrm!

n8n was probably much easier to implement. I went with a custom code solution to utilise free/open source projects.

Look forward to the SMS video!

1 Like

You can install n8n on your own server, keep your data with you and you won’t have to pay license fees if you self host it.

2 Likes

I’ve been fiddling around in n8n for a few days now after you mentioned it was free to selfhost. I worked up a fabulous workflow to reply to new leads from a web form on a website. It sends out very personalised replies. Much better than a ā€œWe’ve received your message and will get back to youā€.

2 Likes

Could you explain in more detail what’s happening in your workflow?

1 Like

Sure thing!

  1. Still a development workflow, this will change to an automated workflow. Currently, click to run.
  2. Post request for API access token
  3. Get request for new leads
  4. Code to clean string data to a json array
  5. Loop over array
  6. Send Lead info to AI to interpret and respond
  7. Fields to clean response
  8. Send email to lead responding to their inquiry

All tested and it works fab. I’d like to eventually develop it to give the inquiry a score. It will respond itself to levels 1-5 of complexity. E.g. ā€œHow much is this?ā€ , ā€œCan I call you to discuss xxyyā€. All things that just need a quick, yep I’ll call you now or this is $xx.

Levels 6-10 inquiries will need to be sent to a holding area for me to respond to. Something complex like ā€œHi, I’d some more info about X and I don’t know if I need Y. But on your website it says you do Y and X as a package, if I take Z as well is there free delivery with X and Yā€. Etc etc. Too complex for a model to interpret (I assume)

Lots of scope to automate things here. A big chunk of my time is spent responding to simple enquiries but I feel like a personalised response is needed for engagement with my customers. So its a win win for this.

Following on from my SMS module. There is scope to have some kind of lead follow up by SMS if they don’t respond to the email. Another big pain point. My customers are definitely phone call/sms users and not so much of email. So an email can be first point of call and then if they don’t respond, I can reach out via SMS. All automated!

2 Likes

Hello SuiteCRM Community,

I am currently running SuiteCRM 8.8.1 and I am trying to integrate a landing page form that automatically creates Leads in SuiteCRM whenever a user submits the form.

Here’s what I have done so far:

  • I tried using the WebToLeadCapture entryPoint, but the submission sometimes reports success without actually creating a Lead.
  • I verified that assigned_user_id and other required fields are correctly set, but the leads are still not consistently created.
  • I attempted to add a redirect_url to redirect users to a thank-you page, but it always redirects back to index.php?entryPoint=WebToLeadCapture with a blank page.
  • I also checked server logs, but there is no clear error related to lead creation.

What I would like to achieve:

  1. A robust solution to reliably create Leads from a landing page form in SuiteCRM 8.8.1.
  2. Optional: guidance on using the SuiteCRM REST API with OAuth2 to create Leads from an external form.
  3. Best practices for logging and debugging lead creation issues in SuiteCRM 8.x.

I would greatly appreciate any guidance, example code, or configuration tips that can help me implement this correctly.

Thank you in advance for your help!

Best regards,

This may help, its for adding new WP users via V8 API to Suitecrm. It’s not hard to just instead trigger it by your form submission, whatever form you are using:

Also I have a more indepth overview of V8 API here:

Here is a script I built that will create a new lead from a contact form. It is based off of the SuiteCRM webtolead form with some captcha improvements. You’ll get loads of spam if you don’t implement it.

Logging - a lot of my scripts have logging after every line so you can easily debug. Just script in a well thought out manner to prevent errors. Copying code is great, but you’ve got to be tech savvy to ensure it works.

PHP Script:

<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // Start time to log total processing time
    $start_time = microtime(true);
    
    $recaptcha_secret = '';
    $recaptcha_response = $_POST['g-recaptcha-response'];
    $recaptcha_action = 'submit'; // This should match the action in your frontend

    // Verify the reCAPTCHA response with Google
    $recaptcha_start = microtime(true);
    $response = file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=$recaptcha_secret&response=$recaptcha_response");
    $recaptcha_end = microtime(true);

    // Log how long the reCAPTCHA verification took
    error_log("reCAPTCHA verification took " . ($recaptcha_end - $recaptcha_start) . " seconds.");
    
    $response_keys = json_decode($response, true);
    
    error_log("Full reCAPTCHA response: " . print_r($response_keys, true));
    error_log("Expected action: $recaptcha_action");
error_log("Hostname from siteverify: " . ($response_keys['hostname'] ?? 'none'));
error_log("Received g-recaptcha-response token length: " . strlen($recaptcha_response));
error_log("Received g-recaptcha-response token (first 50 chars): " . substr($recaptcha_response, 0, 50));

    // Log the reCAPTCHA score
    if (isset($response_keys['score'])) {
        error_log("reCAPTCHA score: " . $response_keys['score']);
    }

    // Check if reCAPTCHA verification was successful and if the score is high enough
    if ($response_keys["success"] && $response_keys['action'] == $recaptcha_action && $response_keys["score"] >= 0.2) {
        // Return a success response immediately for the AJAX call, including the redirect URL
        echo json_encode([
            "success" => true,
            "message" => "Form submitted successfully.",
        ]);

        // Prepare data to send to CRM
        $crm_url = 'xxxxxxxxxxxxxxxxx/public/index.php?entryPoint=WebToPersonCapture';

        $postData = [
            'first_name' => $_POST['first_name'],
            'last_name' => $_POST['last_name'],
            'email1' => $_POST['email1'],
            'phone_mobile' => $_POST['phone_mobile'],
            'description' => $_POST['description'],
            'campaign_id' => $_POST['campaign_id'],
            'assigned_user_id' => $_POST['assigned_user_id'],
            'moduleDir' => $_POST['moduleDir'],
            'lead_source' => $_POST['lead_source']
        ];

Contact form script:

  <form id="WebToLeadForm" action="\submit_form.php" method="POST" name="WebToLeadForm">

    <div class="email_check2">
        <label>Email</label>
        <input type="text" name="email" id="email" value="">
    </div>

    <!-- First Name / Last Name side by side -->
    <div class="row">
        <div class="col">
            <label>First Name: <span class="required">*</span></label>
            <input name="first_name" id="first_name" type="text" required />
        </div>
        <div class="col">
            <label>Last Name: <span class="required">*</span></label>
            <input name="last_name" id="last_name" type="text" required />
        </div>
    </div>

    <!-- Email / Phone side by side -->
    <div class="row">
        <div class="col">
            <label>Email Address: <span class="required">*</span></label>
            <input name="email1" id="email1" type="email" required />
        </div>
        <div class="col">
            <label>Phone Number: <span class="required">*</span></label>
            <input name="phone_mobile" id="phone_mobile" type="text" required />
        </div>
    </div>

    <!-- Message -->
    <div class="row">
        <div class="col">
            <label>Message: <span class="required">*</span></label>
            <textarea name="description" id="description" required></textarea>
        </div>
    </div>

    <input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response">

    <div class="small-grey-text" style="color: grey; font-size: 0.8em; text-align: center; margin-bottom: 1rem;">
        This site is protected by reCAPTCHA and the Google 
        <a href="https://policies.google.com/privacy" style="color: grey;">Privacy Policy</a> and 
        <a href="https://policies.google.com/terms" style="color: grey;">Terms of Service</a> apply.
    </div>

    <!-- Submit Button -->
    <div class="row center buttons">
        <input class="button" name="Submit" type="submit" value="Submit" onclick="return validateAndSubmit();" />
    </div>

    <div id="form-message"></div>

    <!-- Hidden fields -->
    <input name="campaign_id" id="campaign_id" type="hidden" value="81605a5a-cf1b-66f9-4565-67aa45088695" />
    <input name="assigned_user_id" id="assigned_user_id" type="hidden" value="1" />
    <input name="moduleDir" id="moduleDir" type="hidden" value="Leads" />
    <input name="lead_source" id="lead_source" type="hidden" value="Web Site" />

  </form>
</div>

<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
    grecaptcha.ready(function() {
        grecaptcha.execute('6LfUDtIqAAAAALIZquustzbfk-YqIiaK63H02PPO', {action: 'submit'}).then(function(token) {
            document.getElementById('g-recaptcha-response').value = token;
        });
    });

    function validateAndSubmit(event) {
        if(event) event.preventDefault();
        var messageDiv = document.getElementById('form-message');
        messageDiv.innerHTML = '';
        var formData = new FormData(document.getElementById('WebToLeadForm'));

        fetch('/submit_form.php', { method: 'POST', body: formData })
        .then(response => response.json())
        .then(data => {
            if(data.success){
                messageDiv.innerHTML = '<p style="color: green;">' + data.message + '</p>';
                setTimeout(() => { window.location.href = "https://xxxxxxxxxxxxxxxxxxx/redirect/"; }, 0);
            } else {
                messageDiv.innerHTML = '<p style="color: red;">' + data.message + '</p>';
                grecaptcha.reset();
            }
        })
        .catch(error => {
            console.error('Error:', error);
            messageDiv.innerHTML = '<p style="color: red;">An error occurred. Please try again.</p>';
        });

        return false;
    }

    document.getElementById('WebToLeadForm').addEventListener('submit', validateAndSubmit);
});
</script>

        // Use cURL to send data to CRM
        $crm_start = microtime(true); // Start time for CRM request
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $crm_url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        // Start the CRM request
        $crm_response = curl_exec($ch);
        $curl_error = curl_error($ch);
        curl_close($ch);
        $crm_end = microtime(true); // End time for CRM request
        
        // Log how long the CRM request took
        error_log("CRM request took " . ($crm_end - $crm_start) . " seconds.");

        // End timing the form processing
        $end_time = microtime(true);

        // Log total processing time and CRM response or error
        error_log("Total form processing time: " . ($end_time - $start_time) . " seconds.");
        if ($crm_response === false) {
            error_log("CRM request failed: " . $curl_error);
        } else {
            error_log("CRM response: " . $crm_response);
        }
    } else {
        // Log the failure and reCAPTCHA score for debugging
        error_log("CAPTCHA verification failed. Score: " . (isset($response_keys['score']) ? $response_keys['score'] : 'No score available'));
        
        // Return an error response for CAPTCHA failure
        echo json_encode([
            "success" => false, 
            "message" => "CAPTCHA verification failed or score too low. Please try again."
        ]);
    }
}
?>

@Duncantr nice, can you please share the code of your custom entry-point?

Hi @JTJT

Thanks for sharing the code. Your ā€method of sending SMS messages via a custom logic hook in SuiteCRM 8 is currently working and is structurally sound as a first implementation, particularly with the detailed logging that facilitates troubleshooting. Nevertheless, it can be enhanced for better maintainability, performance, and conformity with SuiteCRM 8’s modern Symfony-based framework.

Rather than having all the logic in the hook file, you might consider refactoring the SMS sending logic to a dedicated service class thereby the hook will be lightweight and it will only handle the trigger.

Configurations like API keys and URLs should be put in the Configurator or environment files instead of being hardcoded.

By using Symfony’s built-in HttpClient instead of raw cURL calls, your code will be simplified, and error handling will be more efficient. Similarly, logging can be done with SuiteCRM’s native logger or Monolog, thus it will be more convenient to handle logs throughout the system.

In the case of bulk SMS, it would be better to use SuiteCRM’s scheduler and job queue system to queue the messages, hence the user interface will not be slowed down as synchronous sending will not be needed.

At last, incorporating phone number validation, API response error handling, and correct separation of configuration and logic will render your solution more secure, scalable, and easier to maintain.