Adding custom domains to a Vercel-hosted app uses Vercel's Domains API: you attach the customer hostname to your project, Vercel issues the certificate, and your app routes by the Host header. It works well for a handful of domains. Here is the complete flow — and the honest limits when you have thousands.
# Add a customer domain to your Vercel project
curl -X POST \
"https://api.vercel.com/v10/projects/$PROJECT_ID/domains?teamId=$TEAM_ID" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"app.customer.com"}'Step 1 — Add the domain, then show DNS records
Once the domain is on the project, tell the customer what to set. For a subdomain that is a CNAME; for a bare domain it is an A record (Vercel hits the CNAME-at-apex problem like everyone else):
# subdomain
app.customer.com. CNAME cname.vercel-dns.com.
# apex (verify the current IP in Vercel's docs)
customer.com. A 76.76.21.21If the domain is already serving elsewhere, Vercel also returns a verification TXT record you must display. Check propagation with the DNS propagation checker.
Step 2 — Poll verification status
curl "https://api.vercel.com/v9/projects/$PROJECT_ID/domains/app.customer.com?teamId=$TEAM_ID" \
-H "Authorization: Bearer $VERCEL_TOKEN"
# -> { "verified": true, ... } once DNS resolves and the cert is issuedAs always, the slow part is the customer's DNS propagating, not Vercel issuing the certificate.
Step 3 — Route by host in your app
Vercel functions and Next.js middleware see the customer hostname via the forwarded host header. Resolve it to a tenant and fail closed:
const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? '';
const tenant = await getTenantByDomain(host);
if (!tenant) return new Response('Unknown domain', { status: 404 });If your app is Next.js, the routing details are in how to add custom domains to your Next.js app. The client IP and geo arrive in x-forwarded-for / Vercel's geo headers.
Where Vercel stops
For a few dozen domains this is fine. At real multi-tenant scale the friction shows up: per-project domain limits, plan-gated wildcards, no per-tenant header/redirect policies, and everything tied to Vercel. That is the same trade-off we cover for the CDN-native option in the Cloudflare for SaaS alternative.
The one-API-call way
Front your Vercel app (or any origin) with cnames.dev and register domains without per-project limits or CDN lock-in:
curl -X POST https://api.cnames.dev/v1/domains \
-H "Authorization: Bearer sk_live_..." \
-d '{"hostname":"app.customer.com","origin":"your-app.vercel.app"}'SSL, renewals, wildcards, and per-tenant policies are handled at the edge. This is a common setup for AI app builders shipping user apps. Full architecture in the complete guide.