Skip to content

A default HTTP API on AWS exposes an unusable URL such as a1b2c3d4.execute-api.eu-central-1.amazonaws.com.
In multi-tenant environments you want stable, human-readable, tenant-specific URLs that integrate cleanly into your product.
This guide shows how to build exactly that using a wildcard certificate, a custom domain, Route53 alias records and SSM-based account identity.

For context and the full webhook implementation, see the related article:
Reliable Serverless Webhooks on AWS with HTTP API, Lambda, and DynamoDB


Why the default setup is insufficient

The default execute-api domain has several drawbacks:

  • It leaks AWS internals.
  • It changes when you create new APIs or stages.
  • It doesn't scale across tenants.
  • It cannot be controlled or branded.
  • It complicates documentation and automation.

A clean multi-tenant structure uses predictable URLs:

https://api.<accountSlug>.pantarey.io/

Each AWS account hosts one isolated environment.
The account identity (slug + HostedZoneId) comes from SSM so the CloudFormation template stays generic. This has to be filled in the onboarding step of a new tenant. In this way we can transport information from onboarding to application-deployment.


Architecture Overview

The solution consists of:

  1. SSM Parameter Store
    Tenant-specific configuration (accountSlug, hostedZoneId).
  2. HTTP API
    Lightweight, fast API Gateway v2.
  3. Lambda integration
    Standard AWS_PROXY setup.
  4. Wildcard ACM certificate
    *.${accountSlug}.pantarey.io
  5. Custom Domain for the HTTP API
  6. API Mapping
    Domain → API → Stage
  7. Route53 Alias Record
  8. A clean Base URL output

All resources are deployed via a single CloudFormation stack.


1. Store tenant identity in SSM

Each tenant account defines:

  • /accountSlug
  • /hostedZoneId (Route53 zone of *.pantarey.io)

Example:

accountSlug = "labs"
hostedZoneId = "Z0ABCDEFG12345"

These values never change during deployment and isolate all tenants.


2. CloudFormation parameters

{
  "Parameters": {
    "env": { "Type": "String" },
    "functionwebhookEntryArn": {
      "Type": "String",
      "Description": "Lambda ARN used by the HTTP API"
    },
    "accountSlug": {
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Default": "accountSlug"
    },
    "hostedZoneId": {
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Default": "hostedZoneId"
    }
  }
}

CloudFormation resolves them directly from SSM at deploy time.


3. HTTP API and Lambda integration

{
  "UserApiHttp": {
    "Type": "AWS::ApiGatewayV2::Api",
    "Properties": {
      "Name": { "Fn::Sub": "pantarey-user-api-${env}" },
      "ProtocolType": "HTTP",
      "CorsConfiguration": {
        "AllowOrigins": ["*"],
        "AllowHeaders": ["content-type", "authorization"],
        "AllowMethods": ["OPTIONS", "POST"]
      }
    }
  },

  "WebhookIntegration": {
    "Type": "AWS::ApiGatewayV2::Integration",
    "Properties": {
      "ApiId": { "Ref": "UserApiHttp" },
      "IntegrationType": "AWS_PROXY",
      "IntegrationMethod": "POST",
      "PayloadFormatVersion": "2.0",
      "IntegrationUri": { "Ref": "functionwebhookEntryArn" }
    }
  },

  "WebhookRoute": {
    "Type": "AWS::ApiGatewayV2::Route",
    "Properties": {
      "ApiId": { "Ref": "UserApiHttp" },
      "RouteKey": "POST /v1/user-api/{tenant}/webhook/{uuid}/{token}",
      "Target": { "Fn::Sub": "integrations/${WebhookIntegration}" }
    }
  },

  "WebhookStage": {
    "Type": "AWS::ApiGatewayV2::Stage",
    "Properties": {
      "ApiId": { "Ref": "UserApiHttp" },
      "StageName": "prod",
      "AutoDeploy": true
    }
  }
}

Lambda permissions:

{
  "WebhookLambdaPermission": {
    "Type": "AWS::Lambda::Permission",
    "Properties": {
      "FunctionName": { "Ref": "functionwebhookEntryArn" },
      "Action": "lambda:InvokeFunction",
      "Principal": "apigateway.amazonaws.com",
      "SourceArn": {
        "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${UserApiHttp}/*/*/v1/user-api/*/webhook/*/*"
      }
    }
  }
}

4. Wildcard certificate (ACM)

{
  "UserApiWildcardCertificate": {
    "Type": "AWS::CertificateManager::Certificate",
    "Properties": {
      "DomainName": { "Fn::Sub": "*.${accountSlug}.pantarey.io" },
      "ValidationMethod": "DNS",
      "DomainValidationOptions": [
        {
          "DomainName": { "Fn::Sub": "*.${accountSlug}.pantarey.io" },
          "HostedZoneId": { "Ref": "hostedZoneId" }
        }
      ]
    }
  }
}

ACM automatically creates DNS validation records in the tenant zone.


5. Custom Domain for API Gateway

{
  "UserApiDomainName": {
    "Type": "AWS::ApiGatewayV2::DomainName",
    "Properties": {
      "DomainName": { "Fn::Sub": "api.${accountSlug}.pantarey.io" },
      "DomainNameConfigurations": [
        {
          "CertificateArn": { "Ref": "UserApiWildcardCertificate" },
          "EndpointType": "REGIONAL"
        }
      ]
    }
  }
}

6. API Mapping

{
  "UserApiApiMapping": {
    "Type": "AWS::ApiGatewayV2::ApiMapping",
    "Properties": {
      "ApiId": { "Ref": "UserApiHttp" },
      "DomainName": { "Ref": "UserApiDomainName" },
      "Stage": "prod"
    }
  }
}

7. Route53 alias record

{
  "UserApiDnsRecord": {
    "Type": "AWS::Route53::RecordSet",
    "Properties": {
      "HostedZoneId": { "Ref": "hostedZoneId" },
      "Name": { "Fn::Sub": "api.${accountSlug}.pantarey.io." },
      "Type": "A",
      "AliasTarget": {
        "DNSName": {
          "Fn::GetAtt": ["UserApiDomainName", "RegionalDomainName"]
        },
        "HostedZoneId": {
          "Fn::GetAtt": ["UserApiDomainName", "RegionalHostedZoneId"]
        }
      }
    }
  }
}

The API Gateway alias zone is not your own HostedZoneId; it comes from RegionalHostedZoneId.


8. Output: clean Base URL

{
  "Outputs": {
    "UserApiBaseUrl": {
      "Value": {
        "Fn::Sub": "https://api.${accountSlug}.pantarey.io/v1/user-api"
      }
    }
  }
}

Applications can use this URL to generate webhook endpoints or user-facing links.


Advantages of this setup

  • Clean, stable tenant URLs.
  • Automated TLS with wildcard certificates.
  • Strict tenant isolation at the AWS account level.
  • No hardcoded environment-specific values.
  • Reusable CloudFormation template across all tenants.
  • Works for any future API namespace under the same domain.

Conclusion

This pattern provides predictable, branded tenant URLs while staying fully serverless and automated.
It is the same setup used internally at Pantarey.io for multi-tenant webhook and API operations.