Paystack Apple Pay Domain Verification Failing
Apple Pay domain verification fails when Apple cannot access the file at https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association. The fix is to download the verification file from your Paystack Dashboard, place it at that exact path on your server, and ensure it is served without redirects, with the correct content type, and is accessible from the public internet. Common causes of failure are 301 redirects (trailing slash, www/non-www), incorrect MIME type, the file being blocked by your hosting platform, or the file not being deployed to production.
How Apple Pay Domain Verification Works
Before Apple Pay can be offered as a payment option on your website through Paystack, Apple needs to confirm that you control the domain. The verification process works like this:
- You enable Apple Pay in your Paystack Dashboard and enter your domain name.
- Paystack gives you a verification file to download (or in some configurations, Paystack registers the domain directly with Apple).
- You place the file on your web server at the path
/.well-known/apple-developer-merchantid-domain-association. - Apple's servers send a request to
https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association. - If Apple receives the correct file content with a 200 HTTP status, the domain is verified and Apple Pay becomes available.
The file itself is a text file containing a long token string. It has no file extension. The .well-known directory is a standard convention (RFC 8615) for hosting metadata files that services can discover automatically.
When verification fails, the Paystack Dashboard shows the domain as "unverified" or "pending." Apple Pay will not appear as a payment option for customers on that domain. The error is silent on the customer side: they simply do not see the Apple Pay button.
Common Causes of Verification Failure
Cause 1: Redirect instead of direct response. This is the most frequent culprit. Your server redirects the request before serving the file. Typical redirects that break verification:
http://tohttps://redirect (Apple requests HTTPS, so this is only an issue if your HTTPS itself redirects)example.comtowww.example.com(or vice versa)- Trailing slash redirect:
/.well-known/apple-developer-merchantid-domain-associationto/.well-known/apple-developer-merchantid-domain-association/
Cause 2: File not found (404). The file is not deployed to the correct path. This happens when the file is in your local development environment but was not included in the production build, or when your build process ignores dotfiles and directories starting with a period (like .well-known).
Cause 3: Wrong content type. Some servers auto-detect the content type and serve the file as application/octet-stream or another type. Apple expects the file to be served with a content type that returns the file body as text. text/plain or application/octet-stream both work, but ensure no middleware is transforming the response body.
Cause 4: Hosting platform blocks dotfiles. Some platforms block access to directories starting with a dot for security reasons. The .well-known directory needs an explicit exception.
Cause 5: CDN or caching layer interference. A CDN in front of your origin server might cache a 404 for the path, or might transform the response in a way Apple does not expect. Ensure the CDN passes through requests to /.well-known/ without modification.
Fix for Vercel (Next.js, React, etc.)
On Vercel, place the verification file in your public directory. Files in public are served statically at the root path.
# Create the directory and place the file
mkdir -p public/.well-known
cp apple-developer-merchantid-domain-association public/.well-known/
If Vercel is stripping the .well-known directory during build, add a configuration to your vercel.json:
// vercel.json
{
"rewrites": [
{
"source": "/.well-known/apple-developer-merchantid-domain-association",
"destination": "/api/apple-pay-verification"
}
]
}
Then create an API route that serves the file content:
// pages/api/apple-pay-verification.ts (Pages Router)
// or app/api/apple-pay-verification/route.ts (App Router)
import { NextResponse } from 'next/server';
const VERIFICATION_CONTENT = 'paste-your-verification-token-here';
export async function GET() {
return new NextResponse(VERIFICATION_CONTENT, {
headers: { 'Content-Type': 'text/plain' },
});
}
The static file approach (public/.well-known/) is simpler and should be your first attempt. Use the API route approach only if the static file is not being served correctly.
After deploying, verify with curl:
curl -I https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association
You should see a 200 status with no redirects in the response chain.
Fix for Netlify
On Netlify, place the verification file in your publish directory (usually public, dist, or build depending on your framework).
# For a React/Vue/Svelte app built with Vite
mkdir -p public/.well-known
cp apple-developer-merchantid-domain-association public/.well-known/
Netlify sometimes has issues serving files without extensions from dotfile directories. If the file is not being served, add a redirect rule in your netlify.toml or _redirects file:
# netlify.toml
[[redirects]]
from = "/.well-known/apple-developer-merchantid-domain-association"
to = "/.well-known/apple-developer-merchantid-domain-association"
status = 200
force = true
The force = true flag ensures this rule takes priority over any other redirect rules (like a SPA catch-all redirect). Without it, a rule like /* /index.html 200 for single-page apps might intercept the request and serve your HTML page instead of the verification file.
If you have a _redirects file, add this line before your SPA catch-all:
/.well-known/apple-developer-merchantid-domain-association /.well-known/apple-developer-merchantid-domain-association 200!
The exclamation mark (!) is Netlify's syntax for forced redirects, equivalent to force = true in the TOML format.
Fix for nginx
On nginx, the .well-known directory needs an explicit location block because nginx may block dotfile access by default.
# nginx.conf or your site configuration
server {
# ... your existing configuration ...
# Serve Apple Pay verification file
location = /.well-known/apple-developer-merchantid-domain-association {
alias /var/www/html/.well-known/apple-developer-merchantid-domain-association;
default_type text/plain;
}
# If you have a blanket block on dotfiles, add an exception
location ~ /\.well-known {
allow all;
}
# Common dotfile block that might interfere
# Make sure this does NOT match .well-known
location ~ /\.(?!well-known) {
deny all;
}
}
The key details:
- Use
location =(exact match) rather than a prefix match. This is more efficient and avoids unintended matches. - Set
default_type text/plainbecause nginx cannot auto-detect the type of a file without an extension. - If you have a blanket
deny allfor dotfiles, modify it to exclude.well-known. The regex/\.(?!well-known)uses a negative lookahead to block all dotfiles except.well-known.
After updating the configuration:
# Test configuration syntax
sudo nginx -t
# Reload nginx
sudo systemctl reload nginx
# Verify the file is accessible
curl -v https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association
Fix for Other Platforms (Apache, Express, Django)
Apache. Place the file in your document root under .well-known/. If Apache blocks dotfiles, add an exception in your .htaccess:
# .htaccess
RewriteEngine On
# Allow .well-known directory
RewriteRule ^.well-known/ - [L]
Express.js. Serve the file directly from a route:
const fs = require('fs');
const path = require('path');
app.get('/.well-known/apple-developer-merchantid-domain-association', (req, res) => {
const filePath = path.join(__dirname, '.well-known', 'apple-developer-merchantid-domain-association');
res.type('text/plain').sendFile(filePath);
});
Django. Add a URL pattern that serves the file:
# urls.py
from django.http import HttpResponse
from django.urls import path
def apple_pay_verification(request):
with open('.well-known/apple-developer-merchantid-domain-association') as f:
return HttpResponse(f.read(), content_type='text/plain')
urlpatterns = [
path('.well-known/apple-developer-merchantid-domain-association', apple_pay_verification),
# ... other urls
]
Laravel. Return the file content from a route:
// routes/web.php
Route::get('.well-known/apple-developer-merchantid-domain-association', function () {
return response(
file_get_contents(public_path('.well-known/apple-developer-merchantid-domain-association')),
200,
['Content-Type' => 'text/plain']
);
});
Testing Before Triggering Verification
Before telling Paystack to verify your domain, confirm the file is accessible. Run these checks from your local machine (or any machine outside your network):
# Check 1: Verify HTTP status is 200 (not 301, 302, or 404)
curl -o /dev/null -s -w "%{http_code}" https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association
# Expected output: 200
# Check 2: Verify no redirects occurred
curl -L -v https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association 2>&1 | grep "< HTTP"
# Expected: single "HTTP/2 200" line. Multiple HTTP status lines mean redirects.
# Check 3: Verify the content is correct (not your SPA index.html)
curl https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association
# Expected: the verification token string, NOT HTML content
The third check catches a subtle bug: if your single-page application's catch-all route is serving index.html for the verification path, curl will return a 200 status (looks correct) but the content will be HTML instead of the verification token. Apple will see the wrong content and reject verification.
Only trigger the domain verification from Paystack Dashboard after all three checks pass.
Verifying Apple Pay Works After Domain Verification
After domain verification succeeds in the Paystack Dashboard, confirm Apple Pay works end-to-end:
- Check Dashboard status. The domain should show as "verified" in your Paystack Apple Pay settings.
- Test on Safari. Open your checkout page in Safari on a Mac or iOS device. Apple Pay only appears on Apple devices using Safari. It will not show on Chrome, Firefox, or non-Apple devices.
- Check the Paystack checkout. If you use Paystack Popup or Inline, the Apple Pay option should appear automatically for eligible customers. If you use a custom checkout with the Charge API, you need to implement Apple Pay separately.
- Complete a test payment. In test mode, initiate a payment and select Apple Pay. Confirm the payment completes and your webhook receives the charge.success event.
If the Apple Pay button does not appear after verification, check that: you are testing on an Apple device with Safari, the device has Apple Pay set up with at least one card, your Paystack account has Apple Pay enabled (not just domain-verified), and your checkout code includes the card payment channel (Apple Pay is presented as a card payment option).
Domain verification is per-environment. If you verified your production domain, your staging domain is still unverified. Verify each domain separately.
Key Takeaways
- ✓The verification file must be accessible at /.well-known/apple-developer-merchantid-domain-association with no file extension, no redirect, and an appropriate content type.
- ✓The most common cause of failure is a redirect. If your server redirects from non-www to www (or adds a trailing slash), Apple sees a 301 instead of the file and verification fails.
- ✓On Vercel, you need a vercel.json rewrite or a file in the public directory. On Netlify, you need a _redirects rule or a file in the static directory. On nginx, you need a specific location block.
- ✓After placing the file, test it yourself by visiting the URL in your browser and using curl. If you see the file content with a 200 status and no redirects, Apple will too.
- ✓Domain verification must be done for every domain and subdomain where Apple Pay checkout is used. If you accept payments on both example.com and shop.example.com, verify both.
- ✓Verification can take up to 24 hours after the file is correctly placed, though it usually completes within minutes.
Frequently Asked Questions
- Do I need to re-verify if I change hosting providers?
- If your domain stays the same and the verification file remains accessible at the same path, you do not need to re-verify. But if the new hosting provider does not serve the file correctly (due to different redirect rules or dotfile handling), verification may break. Test the file URL after any hosting migration.
- Can I verify a subdomain for Apple Pay?
- Yes. Each subdomain needs its own verification. If you accept payments on shop.example.com, verify shop.example.com specifically. Verifying example.com does not automatically verify subdomains.
- Does the verification file ever change?
- Paystack may update the verification file content when they rotate their merchant credentials with Apple. Check your Paystack Dashboard periodically and re-download the file if prompted. If Apple Pay stops working unexpectedly, a stale verification file is a likely cause.
- Why does Apple Pay work on my staging domain but not production?
- The most common cause is a CDN or load balancer in front of production that is not in front of staging. Check for caching layers, WAF rules, or reverse proxies that might intercept the verification file request. Test with curl from outside your network to see exactly what Apple sees.
- Can I use Apple Pay on Paystack without domain verification?
- No. Domain verification is mandatory. Apple requires it to ensure that only authorized merchants present the Apple Pay button on their websites. Without verification, the Apple Pay option will not appear in Paystack checkout for your domain.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon