Intermediate Certificates Explained — The Silent Cause of API Integration Failures
Your API works in every browser. An engineer at a partner company says their webhook delivery is failing with x509: certificate signed by unknown authority. You visit the URL yourself — padlock, green, fine. What is different between the browser and their webhook? The intermediate certificate.
The Cast of Characters
Every TLS certificate is part of a chain of trust. Three types of certs live on that chain:
- Leaf certificate — the one for your domain. Has a short lifetime (90 days for Let's Encrypt, 1 year max for commercial CAs).
- Intermediate certificate — the CA's working cert. Signs your leaf. Rotates every few years. Lives on your server.
- Root certificate — the CA's offline, locked-in-a-vault identity. Preinstalled in browsers and OS trust stores. Never lives on your server.
The trust path is: the client has the root; your server sends the leaf plus intermediate; the client uses the intermediate's signature to connect your leaf to the trusted root. If the intermediate is missing from what your server sends, the client cannot bridge the gap.
Why Intermediates Exist at All
The root CA is too valuable to expose. If a root's private key is ever compromised, every cert it ever signed becomes untrusted overnight — and every browser has to push a root-store update, which takes years to propagate. So CAs keep roots offline and use an intermediate (signed by the root) for actual day-to-day signing.
If an intermediate is compromised, the CA revokes just that intermediate, issues a new one, and re-signs new leaves. Customers re-fetch the new intermediate bundle on their next renewal. The root stays untouched. This is why the chain has to include the intermediate — the root has never seen your leaf directly.
Browsers Cheat. Everything Else Does Not.
Here is why your site "works" even when the intermediate is missing from your server config: modern browsers maintain a cache of intermediates they have seen on other sites you have visited. Chrome calls it the AIA (Authority Information Access) fetcher; Firefox has its own cache. When they encounter a handshake with a missing intermediate, they:
- Check the leaf's AIA extension for an intermediate URL.
- Silently fetch it over HTTP.
- Complete the chain.
- Show the padlock as if nothing was wrong.
No other client does this. Not cURL. Not Python's requests. Not Go's net/http. Not Java. Not Node. Not iOS. Not Android. Not any webhook receiver. They all trust only what arrives in the TLS handshake.
The AIA fetch is why your dev sees a padlock on a broken server. It is actively hiding your bug.
The Pattern to Recognize
"My API works in Postman or the browser but fails in..."
- ...Stripe's webhook delivery
- ...GitHub's webhook delivery
- ...my customer's iOS app
- ...our CI build
- ...the Python script on the server
- ...cURL from the command line
That pattern is a missing intermediate. The fix is almost never in the client — it is in your server's TLS config.
Common Ways Chains Break
- Nginx or Apache configured to use the leaf file instead of the fullchain file. Let's Encrypt gives you both. Use
fullchain.pem, always. - CA rotated its intermediate and you did not re-download. Common with self-renewing certs where the automation only touches the leaf.
- Your load balancer terminates TLS and needs its own intermediate upload. AWS ALB, NLB with TLS, Cloudflare with Advanced Certificate Manager, GCP LB — each has its own place to paste the intermediate bundle. The origin server's config does not help.
- Copy-pasted the cert from a browser Export dialog. That usually exports just the leaf.
- Used the wrong bundle file. CAs publish per-CA, per-year bundle files. Pick the wrong one and the chain will not verify.
How to Spot It in 30 Seconds
curl -v https://your-api.example.com 2>&1 | grep -i "certificate\|verify"
If you see unable to get local issuer certificate, your chain is broken. Fix it on the server; every programmatic client will start working again.
Or paste the domain into our Certificate Chain Checker — it walks every cert in the chain, names the specific intermediate that is missing if one is, and confirms CRL status plus expiration for each.
The Fix, in One Command
# Nginx / Apache: point ssl_certificate at fullchain (leaf + intermediates)
# If you only have them as separate files:
cat leaf.crt intermediate.crt > fullchain.crt
# Reload the server, re-run curl -v to confirm.
Order matters: leaf first, then intermediate(s), never the root. Clients build upward from the leaf.
If a webhook from a major service fails mysteriously and the provider's debug log says SSL error — before you blame them, check your chain. It is the single most common cause of silent API integration failures on the receiving side.