Mahbubur Riad
Back to blog
Hosting & Server 6 min read

End-to-End Guide: Automating SSL Certificates in WHMCS with Let’s Encrypt & ACME.sh

Jun 17, 2026 · Mahbubur Riad

Automate Let’s Encrypt SSL issuance and renewal in WHMCS using ACME.sh—no manual work, no expiry surprises. Real steps, pitfalls, and fixes.

On this page

End-to-End Guide: Automating SSL Certificate Issuance and Renewal in WHMCS with Let’s Encrypt and ACME.sh

If you’re a hosting provider managing dozens—or hundreds—of client domains, manually handling SSL certificates is a time sink and a ticking time bomb. Certificates expire. Clients forget. Renewals slip through cracks. And when a client’s site goes down because of a missing https://, you are the one getting the 2 a.m. call.

The good news? You don’t have to do it manually.

In this guide, I’ll walk you through how to automate Let’s Encrypt SSL certificate issuance and renewal inside WHMCS, using ACME.sh—a lightweight, dependency-free ACME protocol client—paired with WHMCS automation hooks and cron jobs.

No fluff. No theory. Just what works in production, what broke for me, and how I fixed it.


Why Automate SSL in WHMCS?

WHMCS has built-in support for selling SSL certificates—especially through partners like cPanel AutoSSL, Comodo, DigiCert, etc. But those are commercial. Let’s Encrypt is free, trusted, and machine-readable (via ACME). That makes it ideal for automation.

But WHMCS doesn’t natively issue Let’s Encrypt certs via ACME. So we fill the gap.

We use:

  • ACME.sh — the ACME client (shell-based, no deps, supports DNS/API challenges)
  • WHMCS Automation Hooks — to trigger issuance/renewal on domain provisioning, upgrade, or renewal
  • Cron + CLI — to batch-check expiries and re-issue if needed

The result? Clients get HTTPS on new domains within minutes of provisioning, and renewals happen silently—no tickets, no panic.


Prerequisites

Before you begin:

  • WHMCS ≥ 8.0 (tested on 8.5 and 8.6)
  • Root SSH access to the WHMCS server (or at least the WHM server if using cPanel)
  • Let’s Encrypt DNS/API challenge credentials (e.g., Cloudflare API token, Route53 keys, or cPanel API access)
  • Basic shell scripting familiarity

⚠️ Important: Never run ACME.sh as root unless absolutely necessary. Create a dedicated acme user with sudo privileges.


Step 1: Install ACME.sh on the WHMCS Server

We’ll install ACME.sh in /opt/acme.sh and use a non-root service user.

Bash
# Create service user
useradd -r -s /bin/false acme

# Install ACME.sh as the acme user
sudo -u acme mkdir -p /home/acme/.acme.sh
sudo -u acme git clone https://github.com/acmesh-project/acme.sh.git /opt/acme.sh

# Install to user's home (sets up aliases)
sudo -u acme /opt/acme.sh/acme.sh --install \
  -m /opt/acme.sh \
  -h /home/acme/.acme.sh/acme.sh \
  -d /home/acme/.acme.sh \
  --home /home/acme/.acme.sh

Pro Tip: Use --home to avoid polluting /root/.acme.sh. Keep it clean.


Step 2: Configure ACME.sh Defaults for WHMCS

ACME.sh supports multiple DNS/API challenges. Let’s pick the most common one: Cloudflare.

Set global defaults (as acme user):

Bash
sudo -u acme /opt/acme.sh/acme.sh --set-default-ca --server letsencrypt
sudo -u acme /opt/acme.sh/acme.sh --set-default-flags --server-letsencrypt
sudo -u acme /opt/acme.sh/acme.sh --set-default-flags --no-deps

Now store Cloudflare credentials securely:

Bash
export CF_Key="your_cloudflare_api_key"
export CF_Email="[email protected]"

🔒 Security note: Store these in /home/acme/.acme.sh/cloudflare.env (mode 600) and source it in scripts—not in .bashrc.


Step 3: Build a WHMCS Automation Hook

WHMCS hooks let us run custom PHP code on events like ProductSetup, DomainRegister, or InvoicePaid. We’ll use ProductSetup to trigger SSL issuance when a client purchases a hosting plan and selects an SSL product.

Create: modules/hooks/acme_ssl_automation.php

PHP
<?php
use WHMCS\Database\Capsule;

add_hook('ProductSetup', 1, function($vars) {
    $pid = $vars['service']['productid'];
    $domain = $vars['domain'];
    $serviceid = $vars['service']['id'];

    // Only trigger for hosting plans that opted for SSL
    $service = Capsule::table('tblhosting')
        ->where('id', $serviceid)
        ->where('domainstatus', 'Active')
        ->first();

    if (!$service) return;

    $sslOption = Capsule::table('tblproductconfiglinks')
        ->where('pid', $pid)
        ->where('optionid', $service['configoption1'])
        ->first();

    // Assuming config option ID 1 = "Free Let's Encrypt SSL"
    if ($service['configoption1'] != 1) return;

    // Trigger ACME.sh issuance
    exec("sudo -u acme /opt/acme.sh/acme.sh --issue -d {$domain} --dns dns_cf --home /home/acme/.acme.sh --log /var/log/acme_{$domain}.log 2>&1", $output, $return);

    if ($return === 0) {
        // Mark SSL as installed in WHMCS
        Capsule::table('tblhosting')
            ->where('id', $serviceid)
            ->update(['sslinstalled' => 1, 'sslstatus' => 'Valid']);
    } else {
        logActivity("ACME SSL issuance failed for domain {$domain}: " . implode("\n", $output));
    }
});

⚠️ Security reminder: sudo must be configured to allow the acme user to run /opt/acme.sh/acme.sh without password. Edit /etc/sudoers.d/acme:

Text
acme ALL=(ALL) NOPASSWD: /opt/acme.sh/acme.sh

Step 4: Handle Domain Renewal & Automatic Renewal

Domains auto-renew in WHMCS. But Let’s Encrypt certs only last 90 days—so we need to trigger renewal before expiry.

We use a cron job that runs daily:

Create /usr/local/bin/acme_renewal_check.sh:

Bash
#!/bin/bash
set -euo pipefail

# Load Cloudflare credentials
source /home/acme/.acme.sh/cloudflare.env

# Get all domains with SSL enabled in WHMCS
domains=$(mysql -u root -p'YOUR_DB_PASS' -N -e "SELECT domain FROM tblhosting WHERE sslinstalled = 1;")

for domain in $domains; do
  # Check expiry (days remaining)
  expiry=$(sudo -u acme /opt/acme.sh/acme.sh --list -d "$domain" 2>/dev/null | awk '/Expire/ {print $3}')
  if [[ -z "$expiry" ]]; then
    logger -t acme-renew "No cert found for $domain"
    continue
  fi

  # Convert expiry date to days left
  expiry_epoch=$(date -d "$expiry" +%s)
  now_epoch=$(date +%s)
  days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

  if [[ $days_left -lt 14 ]]; then
    logger -t acme-renew "Renewing $domain ($days_left days left)"
    sudo -u acme /opt/acme.sh/acme.sh --renew -d "$domain" --dns dns_cf --home /home/acme/.acme.sh --log /var/log/acme_renewal.log
  fi
done

Then add to root’s crontab (sudo crontab -e):

CRON
0 2 * * * /usr/local/bin/acme_renewal_check.sh >/dev/null 2>&1

Pro Tip: Use logger for syslog integration—journalctl -t acme-renew helps debug.


Step 5: Update WHMCS SSL Status Automatically

Let’s say you’ve issued a cert via ACME.sh, but WHMCS still shows “SSL Not Installed.” That breaks billing and client expectations.

We’ll add a hook that syncs real cert status to WHMCS every night.

Create: modules/hooks/sync_ssl_status.php

PHP
<?php
use WHMCS\Database\Capsule;

add_hook('DailyCronJob', 1, function() {
    $domains = Capsule::table('tblhosting')
        ->where('sslinstalled', 1)
        ->where('domainstatus', 'Active')
        ->pluck('domain');

    foreach ($domains as $domain) {
        $cmd = "sudo -u acme /opt/acme.sh/acme.sh --list -d {$domain} 2>/dev/null";
        exec($cmd, $output, $return);

        if ($return !== 0) {
            // Cert missing → mark as uninstalled
            Capsule::table('tblhosting')
                ->where('domain', $domain)
                ->update(['sslinstalled' => 0, 'sslstatus' => 'Expired']);
            continue;
        }

        // Parse output: "Expire: 2025-04-15"
        $expire_line = array_filter($output, function($line) { return strpos($line, 'Expire') !== false; });
        $expire_date = trim(explode(':', array_shift($expire_line))[1] ?? '');

        if (!$expire_date) continue;

        $days_left = (int)floor((strtotime($expire_date) - time()) / 86400);
        $status = $days_left > 0 ? 'Valid' : 'Expired';

        Capsule::table('tblhosting')
            ->where('domain', $domain)
            ->update(['sslstatus' => $status, 'sslinstalled' => $days_left > 0 ? 1 : 0]);
    }
});

This ensures the WHMCS client portal always shows accurate SSL status—no more “SSL Expired” surprises.


Common Pitfalls & Fixes

❌ “ACME: Invalid domain” or “DNS error”

Most often, it’s a typo in the domain name or misconfigured DNS credentials. ACME.sh logs to /var/log/acme_*.log. Always check those first.

❌ “Permission denied” when running ACME.sh via PHP

PHP (Apache/Nginx) runs as www-data, but ACME.sh is owned by acme. Use sudo -u acme and configure /etc/sudoers.d/acme as above.

❌ WHMCS shows “SSL Installed” but site is still HTTP

You issued the cert, but didn’t configure the web server (Apache/Nginx) to use it. For cPanel hosts, use whmapi1 sslinstall after ACME.sh succeeds.

Add this to your ProductSetup hook:

PHP
if ($return === 0) {
    $cert_path = "/home/acme/.acme.sh/{$domain}/{$domain}.crt";
    $key_path = "/home/acme/.acme.sh/{$domain}/{$domain}.key";
    $ca_path = "/home/acme/.acme.sh/{$domain}/ca.cer";

    // For cPanel/WHM servers
    exec("whmapi1 sslinstall domain={$domain} certificate=$(cat {$cert_path}) key=$(cat {$key_path}) cabundle=$(cat {$ca_path})");
}

🛠️ Test first: Run whmapi1 sslinstall ... manually before automating.


Let’s Encrypt vs. Commercial SSL in WHMCS: Quick Comparison

Feature Let’s Encrypt (via ACME.sh) Commercial (e.g., Comodo, DigiCert)
Cost Free $5–$100+ per cert
Automation ✅ Full control (CLI, hooks) ✅ (via WHMCS modules, but vendor-dependent)
Validation DNS/API (fast), HTTP (slower) DV (fast), OV/EV (manual, slow)
Coverage Single domain + 100 SANs (with -d) Single domain or multi-domain
Renewal Auto (90-day cycle) Auto (1–2 years), but vendor alerts needed
Trust ✅ All modern browsers
Support Community-driven Paid support (if purchased)

💡 Verdict: Let’s Encrypt + ACME.sh is ideal for most hosting providers. Use commercial certs only for EV or niche requirements.


FAQ: SSL Automation in WHMCS

Q1: Can I use ACME.sh with non-cPanel servers (e.g., Plesk, custom Nginx)?

Yes. ACME.sh handles the cert generation. You just need to install the cert on your web server after issuance. Use --install-cert in ACME.sh to point to your custom paths:

Bash
acme.sh --install-cert -d example.com \
  --cert-file /etc/ssl/certs/example.crt \
  --key-file /etc/ssl/private/example.key \
  --fullchain-file /etc/ssl/certs/example-fullchain.crt \
  --reloadcmd "systemctl reload nginx"

Q2: What if a domain fails DNS challenge due to propagation delay?

ACME.sh waits up to 60 seconds by default. Increase it:

Bash
export CF_Propagation_Timeout=180

Or switch to HTTP challenge if DNS is flaky (but requires port 80 open and no redirects).

Q3: Do I need a separate ACME account for each domain?

No. ACME.sh reuses the same account key unless you specify --accountkeyfile. One acme user + one account key is fine—even for 1,000 domains.

Q4: How do I handle wildcard SSL (*.example.com)?

ACME.sh supports DNS-01 challenges for wildcards. Use:

Bash
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf

But note: WHMCS doesn’t natively support SAN/wildcard billing—so you’ll need custom product configuration.

Q5: Will this work in a multi-server environment (e.g., load-balanced web or separate DB)?

Yes—but store certs in a shared directory (e.g., NFS, S3 + sync), or run ACME.sh on each web node and sync certs via rsync or ansible. Don’t run ACME.sh from the DB server.


Final Thoughts

Automating SSL in WHMCS isn’t just about saving time—it’s about reliability. Clients trust your hosting when HTTPS just works, without manual tickets or downtime.

ACME.sh + WHMCS hooks is a lightweight, transparent, and controllable stack. You’re not locked into a vendor. You control the logs, the errors, and the rollback.

Start small: automate one domain. Then scale.

And if it breaks? You’ll know exactly where—because the logs are right there, in /var/log/acme_*.log, and the code is in your hooks.

If you’re building a hosting automation workflow, mahbuburriad.com has more deep dives on WHMCS internals, billing automation, and infrastructure-as-code.

Happy automating.

Related

Related posts