If you're building a Shopify Payment App Extension, there's a good chance you already got the hard part right. The extension is built, the payment flow works in testing, and Shopify has approved the app itself.
Then setup stalls. Requests from Shopify start failing with handshake errors. Certificate warnings show up in logs. The DevOps team says the server is fine, and the app team says the code is fine, and nobody can tell where the actual problem is.
Almost every time, the answer is the same: mTLS was never configured correctly.
We've worked with a lot of teams where the extension itself was built well, but the deployment side sat with an internal DevOps team that had never set up mTLS before. One Shopify agency we worked with had their extension fully approved and ready to launch.
Then it sat stuck for weeks: every request from Shopify came back as a handshake failure, and their DevOps team kept assuming it was a firewall or security group issue. It turned out their trust store only had Shopify's leaf certificate, not the full chain, so verification failed the moment Shopify's request came through an intermediate CA. Once that was fixed, the app was live within two days.
It's a small piece of the puzzle, but it blocks the whole payment app from going live until it's right. If that's where you're stuck right now, this post walks through exactly what's going on and how to fix it.
What mTLS actually is
Regular TLS (the "https" you see every day) only proves one thing: that the server you're talking to is really the server it claims to be. Your browser checks the server's certificate, and if it's valid, the connection goes ahead. The server never checks who you are.
mTLS, short for mutual TLS, adds the missing half. Both sides present a certificate. The server proves its identity to the client, and the client proves its identity to the server. Neither side can talk to the other without a certificate the other side trusts.
That's the whole idea. One-way trust becomes two-way trust.
Why Shopify requires this for Payment Apps specifically
Here's the part that confuses a lot of developers at first: for most of your app's life, your app is the one calling Shopify (using an OAuth access token, no mTLS involved at all).
But for payment actions, that flips. When a customer pays, Shopify calls your server to trigger payment, refund, capture, and void requests. In this direction, Shopify is the client and your server is the server.
Since money is moving, Shopify isn't willing to let just anyone hit your payment endpoint and doesn't want your server accepting requests from anyone claiming to be Shopify. So Shopify signs its requests with its own client certificate, and your server needs to trust Shopify's provided CA to verify that certificate is genuinely theirs. That confirms the request really came from Shopify.
And because mTLS is mutual, your app also needs to present its own certificate for Shopify to validate. For this one, you need a certificate signed by a standard trusted CA, not Shopify's self-signed CA. Your side uses a normal, publicly trusted certificate. Shopify's side uses their own private CA. Two different trust chains, one mutual handshake.
One clarification that trips people up: mTLS isn't used for retrieving your OAuth access token. Every request that uses Shopify's GraphQL mutations is authenticated with OAuth, not mTLS. mTLS only applies to the direct server-to-server calls Shopify makes to your payment endpoints, not to your app's own calls back to Shopify.

Setting it up in nginx
At a high level, nginx needs to do three things: trust Shopify's CA so it can verify Shopify's client certificate, present your own trusted certificate back to Shopify, and require a valid client certificate before it lets a request through.
A minimal server block looks like this:
server {
listen 443 ssl;
server_name your-payment-app.example.com;
# Your server's own certificate, signed by a public trusted CA
ssl_certificate /etc/nginx/ssl/your-domain-cert.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain-key.pem;
# Shopify's CA chain, used to verify Shopify's client certificate
ssl_client_certificate /etc/nginx/ssl/shopify-ca-chain.pem;
ssl_verify_client on;
ssl_verify_depth 2;
location / {
# Pass the verification result to your backend so your app
# can double check it if needed
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_pass http://your_backend_app;
}
}
A few details that matter more than they look:
- shopify-ca-chain.pem should contain Shopify's Root and Intermediate CA certificates concatenated together, not just the leaf certificate. If your setup only accepts a single CA and not a chain (some managed services work this way), you need both certificates stored together.
- ssl_verify_depth 2 matters because Shopify's chain has a root and an intermediate. If you leave the default depth of 1, nginx will reject a valid Shopify request because it can't walk far enough up the chain.
- ssl_verify_client on rejects any request without a valid client certificate outright. Use optional only temporarily while testing, and switch it back to on before going live, since optional means unauthenticated requests still reach your app.
- Your own certificate (your-domain-cert.pem) needs to come from a normal trusted CA (anything like Let's Encrypt, DigiCert, etc. works). Don't reuse Shopify's CA for your own certificate. That's the single most common setup mistake we see: teams assume "mTLS with Shopify" means using Shopify's certificate on both sides, and it doesn't.
nginx isn't the only option
Everything above uses nginx as the example, since it's the most common setup we see. But mTLS isn't nginx-specific. If your infrastructure runs through something else, the same idea (trust Shopify's CA, present your own trusted certificate, reject anything unverified) applies, just configured differently.
Cloudflare. If Cloudflare sits in front of your server, you can enforce mTLS at the edge instead of on your origin server, using Cloudflare's API Shield mTLS feature. You upload Shopify's CA certificate as a custom CA (Cloudflare calls this "bring your own CA"), then create an mTLS rule for the hostname your payment app uses. Cloudflare blocks any request without a valid client certificate before it even reaches your server. This works well if you'd rather manage certificate trust in Cloudflare's dashboard than in server config files, but keep in mind your origin server still needs its own valid, trusted certificate for the connection between Cloudflare and your origin.
AWS API Gateway. API Gateway supports mTLS directly on a custom domain name, for both REST and HTTP APIs. You build a truststore file (Shopify's CA chain, the same PEM file you'd use with nginx), upload it to an S3 bucket, and then point your API Gateway custom domain at that truststore. Once enabled, API Gateway rejects any request that doesn't present a certificate matching that trust store, before the request reaches your Lambda function or backend integration. One thing to watch: API Gateway warns you about invalid certificates only when you update the domain, not when a certificate later expires, so you need your own reminder for certificate expiry.
Apache. If your payment app runs behind Apache instead of nginx, mod_ssl gives you the same three pieces: your server's own certificate and key (SSLCertificateFile / SSLCertificateKeyFile), Shopify's CA chain for verifying incoming client certificates (SSLCACertificateFile), and an enforcement directive (SSLVerifyClient require, with SSLVerifyDepth set high enough to walk Shopify's full chain). The logic is identical to the nginx config above, just spelled differently.
Whichever layer you enforce mTLS at, the same rule from the certificate rotation section below still applies: trust the CA chain, not one specific certificate, or you'll break at the next rotation regardless of which platform you're on.
The certificate rotation trap
Shopify rotated its mTLS client certificate in mid-June 2026. During the transition window, requests could arrive signed by either the old or new certificate, and Shopify expected apps to trust both until the rotation finished.
Teams that had hardcoded a single leaf certificate instead of trusting Shopify's CA chain broke the moment the rotation happened. Requests started failing with no code change on their end at all, which made it a confusing incident to diagnose.
The fix is architectural, not a one-time patch: trust the CA chain, not a specific certificate. If your nginx config trusts the actual Shopify CA (as shown above), you don't need to touch anything the next time Shopify rotates a leaf certificate. If your config pins a specific certificate fingerprint, you will break again at the next rotation.
Common mistakes we keep seeing
- Using Shopify's self-signed CA for your own server certificate. It only exists to verify Shopify's identity, not to identify your server. Your certificate needs to come from a public trusted CA.
- Missing the intermediate certificate, so verification works in testing (where some tools are more forgiving) but fails against real Shopify traffic. This is exactly what happened to the agency from the intro: their trust store had Shopify's root but not the intermediate, so every real request from Shopify failed while local tests kept passing.
- Confusing mTLS scope with OAuth scope, and assuming a working OAuth integration means mTLS is already handled. They're separate mechanisms covering separate request directions.
- Pinning a specific Shopify certificate instead of trusting the CA chain, which breaks at the next rotation.
- Leaving ssl_verify_client set to optional after testing, which quietly leaves the endpoint open to unauthenticated requests.
Why this is worth getting right the first time
A broken mTLS setup doesn't just delay your launch. It's usually the difference between an extension that passes Shopify's review smoothly and one that gets stuck bouncing between your DevOps team and Shopify support. Getting it right the first time means:
- Faster approval, since Shopify's review includes checking that your payment endpoints actually respond correctly under mTLS.
- No processing downtime once merchants start using your app for real transactions.
- One less thing for merchants and payment gateways to worry about when they're deciding whether to trust your extension with real money.
If your team is stuck on this right now
If you're a Shopify app developer, an agency, or a payment gateway trying to get a Payment App Extension live, and mTLS setup is the thing standing between you and launch, this is exactly the kind of problem we help teams solve at Lucent Innovation, whether that's building the extension itself or getting your existing DevOps setup configured correctly against Shopify's requirements.
Reach out here and tell us where you're stuck. We'll get back to you directly.
