Fix PHP cURL Error 60: SSL Certificate Problem Unable to Get Local Issuer Certificate

intermediate๐Ÿ˜ PHP2026-05-10| PHP 7.x/8.x on Windows (XAMPP/Laragon/WAMP) and Linux (Ubuntu/CentOS/Debian), any framework using Guzzle or direct curl_exec()

Error Message

cURL error 60: SSL certificate problem: unable to get local issuer certificate
#php#curl#ssl#https#certificate#api

The Situation

You make an HTTPS request to an external API โ€” Stripe, PayPal, some webhook endpoint โ€” and PHP throws this:

cURL error 60: SSL certificate problem: unable to get local issuer certificate

Postman works. The browser works. PHP refuses. Every time. This almost always happens on Windows dev machines (XAMPP, Laragon, WAMP) or freshly provisioned Linux servers where the CA bundle is missing or points nowhere useful.

Why It Happens

cURL verifies a server's SSL certificate against a list of trusted Certificate Authorities (CAs). On Linux, that bundle lives at /etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu) or /etc/pki/tls/certs/ca-bundle.crt (CentOS/RHEL). Two things go wrong: either PHP's bundled cURL can't find the file at all, or the system CA bundle is so outdated it doesn't include the issuer for the cert you're hitting.

Windows is a different animal. cURL doesn't read the Windows certificate store by default โ€” it needs a separate cacert.pem pointed to explicitly in php.ini. Out of the box, XAMPP ships with no cainfo setting configured, so every HTTPS call to an external API fails with error 60.

Quick Fix (Dev Only โ€” Don't Ship This)

Need to unblock yourself right now? Disable SSL verification. Never do this in production.

$ch = curl_init('https://api.example.com/endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // dev only
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // dev only
$response = curl_exec($ch);
curl_close($ch);

With Guzzle:

$client = new \GuzzleHttp\Client(['verify' => false]); // dev only
$response = $client->get('https://api.example.com/endpoint');

Got a response? Good. That confirms the SSL cert check is the culprit. Now fix it properly.

Permanent Fix โ€” Point cURL at a Valid CA Bundle

Step 1: Download the Mozilla CA Bundle

The curl project maintains an official cacert.pem sourced directly from Mozilla. It's updated several times a year and covers all major CAs:

# Linux
wget -O /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem

# Or using curl itself
curl -o /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem

On Windows, download it manually and drop it somewhere stable โ€” C:\php\extras\cacert.pem works well. Avoid paths with spaces.

Step 2: Tell PHP to Use It

Edit php.ini (find the right file with php --ini or check phpinfo() in a browser):

[curl]
curl.cainfo = /etc/ssl/certs/cacert.pem

[openssl]
openssl.cafile = /etc/ssl/certs/cacert.pem

On Windows with XAMPP, use Windows-style paths with quotes:

[curl]
curl.cainfo = "C:\php\extras\cacert.pem"

[openssl]
openssl.cafile = "C:\php\extras\cacert.pem"

Restart your server after saving:

# Linux systemd
sudo systemctl restart php8.2-fpm
sudo systemctl restart apache2

# XAMPP on Windows โ€” restart via the control panel

Step 3: Set It Per-Request (Shared Hosting Fallback)

No access to php.ini? Pass the CA bundle path directly in the cURL call:

$ch = curl_init('https://api.example.com/endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/cacert.pem');
$response = curl_exec($ch);
if ($response === false) {
    echo curl_error($ch);
}
curl_close($ch);

Same idea with Guzzle:

$client = new \GuzzleHttp\Client([
    'verify' => '/etc/ssl/certs/cacert.pem'
]);
$response = $client->get('https://api.example.com/endpoint');

Linux: Update the System CA Store Instead

Debian and Ubuntu make this even simpler โ€” just reinstall the CA certificates package and regenerate the bundle:

sudo apt-get update
sudo apt-get install --reinstall ca-certificates
sudo update-ca-certificates

CentOS/RHEL equivalent:

sudo yum reinstall ca-certificates
sudo update-ca-trust

After this, PHP's cURL (if compiled to look at the system bundle) picks up the changes automatically. No php.ini edits needed.

Verify the Fix Worked

Test from the CLI before touching your app code:

# Test with curl directly
curl -v https://api.example.com/endpoint

# Test PHP's cURL specifically
php -r "
\$ch = curl_init('https://api.example.com/endpoint');
curl_setopt(\$ch, CURLOPT_RETURNTRANSFER, true);
\$out = curl_exec(\$ch);
\$err = curl_error(\$ch);
curl_close(\$ch);
echo \$err ? 'ERROR: ' . \$err : 'OK โ€” got ' . strlen(\$out) . ' bytes';
"

You want to see OK โ€” got N bytes. Still failing? Run php --ini and confirm it's loading the right php.ini. Then open phpinfo() in a browser and search for curl.cainfo โ€” the path shown there is what PHP is actually using, not necessarily what you edited.

Self-Signed or Internal CA Certificates

Calling an internal API behind a self-signed cert? Don't disable verification โ€” that trades one problem for a worse one. Append your internal CA cert to the bundle instead:

# Append your internal CA cert to the cacert.pem bundle
cat /path/to/your-internal-ca.crt >> /etc/ssl/certs/cacert.pem

Or point cURL at it directly per request:

curl_setopt($ch, CURLOPT_CAINFO, '/path/to/your-internal-ca.crt');

Quick Reference

  • Windows dev machine (XAMPP/Laragon): Download cacert.pem, set curl.cainfo in php.ini
  • Linux prod server: Run update-ca-certificates or set curl.cainfo in php.ini
  • Self-signed cert: Append your CA to the bundle โ€” never disable verification
  • No access to php.ini: Use CURLOPT_CAINFO per request, or 'verify' => '/path/to/cacert.pem' in Guzzle
  • Never in production: CURLOPT_SSL_VERIFYPEER = false

Related Error Notes