Guide · AWS API Gateway · Custom Domains

AWS API Gateway Custom Domains for MCP Servers — ACM Certificates, Base Path Mappings, and Route53

API Gateway custom domains replace the default execute-api.amazonaws.com hostname with your own domain — api.yourmcp.com — using an ACM-provisioned TLS certificate, a Route53 alias record, and base path mappings that route path prefixes to specific API stages. For MCP server operators, a custom domain is not just cosmetic: it decouples your client-facing URL from the API ID (which changes when you recreate the API), enables version routing without client reconfiguration (/v1 and /v2 can point to different stages), and is a prerequisite for mutual TLS authentication with enterprise clients that need to present client certificates. No CloudFront distribution is required for regional endpoints — API Gateway handles TLS termination natively. For traffic control across deployments, combine custom domains with canary deployments; for per-client rate limits on the domain, see usage plans; for the underlying API configuration, see HTTP API Gateway for MCP servers.

TL;DR

Request an ACM certificate for your domain in the same region as your API (for regional endpoints) or in us-east-1 (for edge-optimized endpoints — CloudFront requirement). Create a custom domain name with aws apigateway create-domain-name --domain-name api.yourmcp.com --regional-certificate-arn $CERT_ARN --endpoint-configuration types=REGIONAL. The output gives you a regionalDomainName — use this (not the execute-api URL) as the alias target in Route53. Create a base path mapping linking the domain, a path prefix, and an API stage. DNS propagation takes 1–5 minutes; TLS works immediately because ACM is pre-provisioned before the domain is created.

Regional vs edge-optimized endpoints

API Gateway offers two endpoint types for custom domains, and the choice affects certificate region requirements, latency characteristics, and operational flexibility.

AttributeRegional EndpointEdge-Optimized Endpoint
TLS termination locationAWS region where the API is deployed (e.g., us-east-1)CloudFront edge PoP closest to the client (global)
ACM certificate regionSame region as the API deploymentMust be in us-east-1 (CloudFront requirement regardless of API region)
DNS record typeA record alias to regionalDomainNameCNAME or A alias to distributionDomainName (CloudFront)
CloudFront prefix in domainNone — your own domain goes directly to API GatewayAPI Gateway creates a CloudFront distribution; you get a .cloudfront.net domain as the alias target
Custom request headersFull support — headers pass through unchangedSome CloudFront header restrictions apply; CloudFront may strip or normalize certain headers
Latency for global clientsBest for single-region or co-located clients; add your own CloudFront if neededAutomatically routes to nearest edge; better for geographically distributed clients without manual CloudFront setup
mTLS supportSupportedNot supported
Recommended for MCP serversYes — use regional + manual CloudFront if neededAvoid — us-east-1 certificate constraint and no mTLS support

For MCP servers, the regional endpoint is almost always the right choice. It keeps your certificate in the same region as your API (simpler), supports mutual TLS for enterprise clients, and gives you full control if you later want to add CloudFront in front of API Gateway with custom cache behaviors. Edge-optimized is a legacy default that predates regional endpoints — AWS's own documentation now recommends regional endpoints for new deployments.

Creating a regional custom domain

The workflow has four phases: request and validate the ACM certificate, create the API Gateway custom domain, create the base path mapping, and create the Route53 DNS record.

Phase 1: Request and validate the ACM certificate

# Request the certificate — DNS validation is recommended over email validation
# (DNS validation allows programmatic renewal without human action)
CERT_ARN=$(aws acm request-certificate \
  --domain-name api.yourmcp.com \
  --validation-method DNS \
  --region us-east-1 \
  --query CertificateArn --output text)

echo "Certificate ARN: $CERT_ARN"

# Get the DNS CNAME record you need to add to Route53 for validation
aws acm describe-certificate \
  --certificate-arn $CERT_ARN \
  --region us-east-1 \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord"

# Output:
# {
#   "Name": "_abc123def456.api.yourmcp.com.",
#   "Type": "CNAME",
#   "Value": "_xyz789abc.acm-validations.aws."
# }

# Create the validation CNAME in Route53
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name \
  --dns-name yourmcp.com \
  --query "HostedZones[0].Id" --output text | cut -d/ -f3)

aws route53 change-resource-record-sets \
  --hosted-zone-id $HOSTED_ZONE_ID \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "_abc123def456.api.yourmcp.com.",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [{"Value": "_xyz789abc.acm-validations.aws."}]
      }
    }]
  }'

# Wait for certificate validation (typically 1–5 minutes)
aws acm wait certificate-validated \
  --certificate-arn $CERT_ARN \
  --region us-east-1

echo "Certificate validated."

Phase 2: Create the API Gateway custom domain

# Create the regional custom domain name
DOMAIN_OUTPUT=$(aws apigateway create-domain-name \
  --domain-name api.yourmcp.com \
  --endpoint-configuration types=REGIONAL \
  --regional-certificate-arn $CERT_ARN \
  --security-policy TLS_1_2)

# Extract the regional domain name — THIS is what you point Route53 at
# NOT the execute-api.amazonaws.com URL
REGIONAL_DOMAIN=$(echo $DOMAIN_OUTPUT | jq -r '.regionalDomainName')
REGIONAL_HOSTED_ZONE=$(echo $DOMAIN_OUTPUT | jq -r '.regionalHostedZoneId')

echo "Regional domain name: $REGIONAL_DOMAIN"
echo "Regional hosted zone ID: $REGIONAL_HOSTED_ZONE"
# Example output:
# Regional domain name: d-abc123def.execute-api.us-east-1.amazonaws.com
# Regional hosted zone ID: Z1UJRXOUMOOFQ8

Phase 3: Create a base path mapping

# Map /v1 path prefix to the prod stage of your API
aws apigateway create-base-path-mapping \
  --domain-name api.yourmcp.com \
  --rest-api-id abc123def \
  --stage prod \
  --base-path v1

# Map /v2 path prefix to the v2 stage
aws apigateway create-base-path-mapping \
  --domain-name api.yourmcp.com \
  --rest-api-id xyz789ghi \
  --stage prod \
  --base-path v2

# Map the root path (no prefix) to the current stable API
# base-path "(none)" in CLI means no path prefix = root mapping
aws apigateway create-base-path-mapping \
  --domain-name api.yourmcp.com \
  --rest-api-id abc123def \
  --stage prod
  # omitting --base-path creates the root (empty) mapping

Phase 4: Create the Route53 alias A record

# Create an A record alias pointing to the API Gateway regional domain
# IMPORTANT: use the regionalDomainName from create-domain-name output,
# not the execute-api.amazonaws.com URL — they are different values
aws route53 change-resource-record-sets \
  --hosted-zone-id $HOSTED_ZONE_ID \
  --change-batch "{
    \"Changes\": [{
      \"Action\": \"CREATE\",
      \"ResourceRecordSet\": {
        \"Name\": \"api.yourmcp.com\",
        \"Type\": \"A\",
        \"AliasTarget\": {
          \"HostedZoneId\": \"$REGIONAL_HOSTED_ZONE\",
          \"DNSName\": \"$REGIONAL_DOMAIN\",
          \"EvaluateTargetHealth\": false
        }
      }
    }]
  }"

echo "DNS record created. Allow 1-5 minutes for propagation."

# Verify after propagation
curl -v https://api.yourmcp.com/v1/status

Base path mappings

Base path mappings are the routing layer between a custom domain and your API stages. A single custom domain can have one empty-path (root) mapping and any number of prefixed mappings. This enables several important patterns for MCP server versioning and organization:

When a request arrives at api.yourmcp.com/v1/mcp/tools/call, API Gateway strips the base path (v1) before forwarding to the mapped API stage. Your Lambda handler receives the path as /mcp/tools/call, not /v1/mcp/tools/call. This is important to account for when designing your API routes — the path your Lambda sees is not the same as the path the client sends.

# List existing base path mappings for a domain
aws apigateway get-base-path-mappings \
  --domain-name api.yourmcp.com

# Update a base path mapping to point to a new stage (e.g., after promoting a deployment)
aws apigateway update-base-path-mapping \
  --domain-name api.yourmcp.com \
  --base-path v1 \
  --patch-operations \
    op=replace,path=/stage,value=prod-v2

# Delete a base path mapping (removes that path prefix routing rule)
aws apigateway delete-base-path-mapping \
  --domain-name api.yourmcp.com \
  --base-path v1

Note: base path string must not include a leading slash. The correct value is v1, not /v1. Passing a leading slash causes an error at creation time and is a common mistake when migrating from other API gateway products that use path-prefix format with slashes.

Mutual TLS (mTLS) for enterprise MCP clients

Enterprise MCP clients — particularly internal tooling in financial services, healthcare, or government environments — often require mutual TLS: not only does the server prove its identity with a certificate, but the client must also present a valid certificate signed by a trusted CA. API Gateway supports mTLS on regional custom domains via a truststore bundle stored in S3.

The mTLS configuration is part of the custom domain, not the API stage. All APIs mapped to a domain with mTLS enabled must present a valid client certificate. You cannot enable mTLS for some base path mappings and disable it for others on the same domain — use separate domains if you need mixed mTLS and non-mTLS access.

# Step 1: Create the truststore bundle (PEM-encoded CA certificates)
# This file contains one or more CA certificates that are trusted to sign client certs
cat ca1.pem ca2.pem > truststore.pem

# Step 2: Upload to S3 (versioning must be enabled on the bucket)
aws s3api create-bucket \
  --bucket yourmcp-mtls-truststore \
  --region us-east-1

aws s3api put-bucket-versioning \
  --bucket yourmcp-mtls-truststore \
  --versioning-configuration Status=Enabled

aws s3 cp truststore.pem s3://yourmcp-mtls-truststore/truststore.pem

# Get the version ID of the uploaded object
TRUSTSTORE_VERSION=$(aws s3api head-object \
  --bucket yourmcp-mtls-truststore \
  --key truststore.pem \
  --query VersionId --output text)

# Step 3: Create the custom domain with mTLS enabled
aws apigateway create-domain-name \
  --domain-name enterprise.yourmcp.com \
  --endpoint-configuration types=REGIONAL \
  --regional-certificate-arn $CERT_ARN \
  --security-policy TLS_1_2 \
  --mutual-tls-authentication \
    truststoreUri=s3://yourmcp-mtls-truststore/truststore.pem,\
    truststoreVersion=$TRUSTSTORE_VERSION

# Step 4: After updating the truststore bundle (e.g., adding a new CA),
# re-upload to S3 and call update-domain-name to trigger re-ingestion
# API Gateway does NOT poll S3 automatically — you must trigger the update
NEW_VERSION=$(aws s3api head-object \
  --bucket yourmcp-mtls-truststore \
  --key truststore.pem \
  --query VersionId --output text)

aws apigateway update-domain-name \
  --domain-name enterprise.yourmcp.com \
  --patch-operations \
    op=replace,path=/mutualTlsAuthentication/truststoreVersion,value=$NEW_VERSION

mTLS and usage plan API keys can be used together on the same domain. The request must pass both the mTLS client certificate validation (enforced at the domain level) and provide a valid API key (enforced at the stage level). This gives you two independent layers of access control: network identity (who is allowed to connect at all) and API key (which tier of service they have access to).

When a client certificate fails mTLS validation, API Gateway returns a 403 with no Access-Control-Allow-Origin header — browser-based MCP clients will see a CORS error rather than the underlying authentication failure. Add a GatewayResponse for UNAUTHORIZED type to include CORS headers in the mTLS rejection response.

CDK setup for custom domains

import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53targets from 'aws-cdk-lib/aws-route53-targets';
import * as cdk from 'aws-cdk-lib';

// Look up the hosted zone for your domain
const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
  domainName: 'yourmcp.com',
});

// Request an ACM certificate with DNS validation
// CDK automatically creates the validation CNAME records in Route53
const certificate = new acm.Certificate(this, 'ApiCertificate', {
  domainName: 'api.yourmcp.com',
  validation: acm.CertificateValidation.fromDns(hostedZone),
});

// Create the REST API
const api = new apigateway.RestApi(this, 'McpServerApi', {
  restApiName: 'mcp-server-api',
  domainName: {
    domainName: 'api.yourmcp.com',
    certificate,
    endpointType: apigateway.EndpointType.REGIONAL,
    securityPolicy: apigateway.SecurityPolicy.TLS_1_2,
    // Optional: add a base path mapping inline
    basePath: 'v1',
  },
});

// Create a Route53 A record alias pointing to the API Gateway regional domain
new route53.ARecord(this, 'ApiAliasRecord', {
  zone: hostedZone,
  recordName: 'api',
  target: route53.RecordTarget.fromAlias(
    new route53targets.ApiGatewayDomain(api.domainName!)
  ),
});

// Add additional base path mappings for other API versions
const apiV2 = new apigateway.RestApi(this, 'McpServerApiV2', {
  restApiName: 'mcp-server-api-v2',
});

// addBasePathMapping creates an additional routing rule on the same domain
api.domainName!.addBasePathMapping(apiV2, {
  basePath: 'v2',
});

// Root mapping (no base path) — maps api.yourmcp.com/ to this API
api.domainName!.addBasePathMapping(api, {
  basePath: '',  // empty string = root mapping
});

The CDK route53targets.ApiGatewayDomain target automatically uses the regionalDomainName and regionalHostedZoneId from the custom domain resource — the values that must be used for the alias record, not the execute-api URL. This is one area where CDK handles the complexity correctly and is worth using over raw CLI commands.

Failure modes

SymptomCauseFix
HTTPS connection fails with TLS error "certificate unknown" or "SSL handshake failed" The ACM certificate is in the wrong region for the endpoint type. Edge-optimized endpoints require the certificate in us-east-1 (because CloudFront serves the TLS and CloudFront only supports us-east-1 certificates). Regional endpoints require the certificate in the same region as the API Gateway deployment Identify your endpoint type with aws apigateway get-domain-name --domain-name api.yourmcp.com --query endpointConfiguration. If REGIONAL, request a new ACM certificate in the API's region. If EDGE, request in us-east-1. Then update the domain name to use the new certificate ARN with update-domain-name --patch-operations op=replace,path=/regionalCertificateArn,value=$NEW_CERT_ARN
Custom domain created successfully but DNS does not resolve — dig api.yourmcp.com returns NXDOMAIN or wrong IP The Route53 A record alias was pointed at the execute-api.amazonaws.com URL (the API execution endpoint) instead of the regionalDomainName from the create-domain-name output. These are two completely different endpoints — the execute-api URL serves the API directly but does not serve your custom domain TLS certificate Get the correct alias target: aws apigateway get-domain-name --domain-name api.yourmcp.com --query regionalDomainName. The value looks like d-abc123.execute-api.us-east-1.amazonaws.com (note the d- prefix). Update the Route53 A record to point to this value with the corresponding regionalHostedZoneId
Base path mapping returns 403 Forbidden immediately after creation, even though the API and stage are correct Base path mappings take 1–3 minutes to propagate after creation — the mapping exists in the API Gateway control plane but has not yet propagated to all regional edge nodes. Also verify the base path string has no leading slash: the correct value is v1, not /v1. A leading slash causes the mapping to be created but never match incoming requests Wait 2–3 minutes after create-base-path-mapping before testing. Check the base path: aws apigateway get-base-path-mappings --domain-name api.yourmcp.com — the basePath field should be v1 with no slash. If it was created with a slash, delete and recreate it
mTLS rejecting valid client certificates after updating the truststore with new CA certificates API Gateway does not poll the S3 truststore object for changes. After uploading a new version of the truststore PEM to S3, API Gateway continues using the previously ingested bundle until you explicitly trigger re-ingestion by calling update-domain-name with the new S3 object version ID After every truststore update: (1) upload the new PEM to S3, (2) retrieve the new S3 object version ID, (3) call aws apigateway update-domain-name --domain-name enterprise.yourmcp.com --patch-operations op=replace,path=/mutualTlsAuthentication/truststoreVersion,value=$NEW_VERSION. The update takes 1–2 minutes to propagate. Verify with get-domain-name --query mutualTlsAuthentication
Edge-optimized domain not serving the renewed ACM certificate — clients still see the old certificate after ACM auto-renewal Edge-optimized endpoints use a CloudFront distribution managed by API Gateway. When ACM renews the certificate, the new certificate must be propagated to the CloudFront distribution. This propagation is automatic but takes approximately 5 minutes after ACM renewal completes. In some cases, CloudFront does not pick up the renewal and the old certificate continues to be served If the CloudFront distribution is still serving the old certificate more than 15 minutes after ACM shows the new certificate as valid, call aws apigateway update-domain-name --domain-name api.yourmcp.com --patch-operations op=replace,path=/certificateArn,value=$CERT_ARN with the same ARN to trigger a CloudFront distribution update. This is why regional endpoints are preferred — certificate renewal is handled by API Gateway directly and propagates in under 2 minutes