Custom Hostnames (Cloudflare for SaaS)
Custom Hostnames enable SaaS providers to extend Cloudflare's security and performance benefits to their customers' vanity domains. This is the foundation of Cloudflare for SaaS.
Overview
Access the Custom Hostnames API through cf.Zones.CustomHostnames:
public class SaasService(ICloudflareApiClient cf)
{
public async Task OnboardCustomerAsync(string zoneId, string customerDomain)
{
var hostname = await cf.Zones.CustomHostnames.CreateAsync(zoneId,
new CreateCustomHostnameRequest(
Hostname: customerDomain,
Ssl: new SslConfiguration(Method: DcvMethod.Txt)
));
Console.WriteLine($"Status: {hostname.Status}");
}
}
Creating Custom Hostnames
Basic Creation
var hostname = await cf.Zones.CustomHostnames.CreateAsync(zoneId,
new CreateCustomHostnameRequest(
Hostname: "app.customer.com",
Ssl: new SslConfiguration(Method: DcvMethod.Txt)
));
With Custom Origin Server
Route traffic to a specific origin for this hostname:
var hostname = await cf.Zones.CustomHostnames.CreateAsync(zoneId,
new CreateCustomHostnameRequest(
Hostname: "app.customer.com",
Ssl: new SslConfiguration(Method: DcvMethod.Http),
CustomOriginServer: "customer-123.origin.example.com"
));
With Custom Metadata
Attach arbitrary metadata to the hostname:
var metadata = JsonSerializer.SerializeToElement(new
{
customerId = "cust_123",
plan = "enterprise",
region = "us-east"
});
var hostname = await cf.Zones.CustomHostnames.CreateAsync(zoneId,
new CreateCustomHostnameRequest(
Hostname: "app.customer.com",
Ssl: new SslConfiguration(Method: DcvMethod.Txt),
CustomMetadata: metadata
));
Domain Validation
Custom hostnames require domain ownership verification before SSL certificates are issued.
TXT Validation
The customer adds a TXT record to their DNS:
var hostname = await cf.Zones.CustomHostnames.GetAsync(zoneId, hostnameId);
if (hostname.OwnershipVerification is { } verification)
{
Console.WriteLine($"Add TXT record:");
Console.WriteLine($" Name: {verification.Name}");
Console.WriteLine($" Value: {verification.Value}");
}
HTTP Validation
The customer serves a token at a specific URL:
if (hostname.OwnershipVerificationHttp is { } httpVerification)
{
Console.WriteLine($"Serve content at: {httpVerification.HttpUrl}");
Console.WriteLine($"Content: {httpVerification.HttpBody}");
}
Checking Validation Errors
if (hostname.VerificationErrors?.Count > 0)
{
foreach (var error in hostname.VerificationErrors)
{
Console.WriteLine($"Error: {error}");
}
}
Listing Custom Hostnames
List with Pagination
var page = await cf.Zones.CustomHostnames.ListAsync(zoneId,
new ListCustomHostnamesFilters
{
Page = 1,
PerPage = 50
});
foreach (var hostname in page.Items)
{
Console.WriteLine($"{hostname.Hostname}: {hostname.Status}");
}
List All Hostnames
await foreach (var hostname in cf.Zones.CustomHostnames.ListAllAsync(zoneId))
{
Console.WriteLine($"{hostname.Hostname}: {hostname.Status}, SSL: {hostname.Ssl.Status}");
}
Filter by Hostname
var filters = new ListCustomHostnamesFilters
{
Hostname = "app.customer.com"
};
await foreach (var hostname in cf.Zones.CustomHostnames.ListAllAsync(zoneId, filters))
{
// Process matching hostnames
}
Updating Custom Hostnames
Update hostname configuration (PATCH operation):
var updated = await cf.Zones.CustomHostnames.UpdateAsync(zoneId, hostnameId,
new UpdateCustomHostnameRequest(
CustomOriginServer: "new-origin.example.com"
));
Refresh Validation Status
Send an empty update to refresh the validation status:
var refreshed = await cf.Zones.CustomHostnames.UpdateAsync(zoneId, hostnameId,
new UpdateCustomHostnameRequest());
Deleting Custom Hostnames
await cf.Zones.CustomHostnames.DeleteAsync(zoneId, hostnameId);
Fallback Origin
The fallback origin is used when a custom hostname doesn't have a specific CustomOriginServer configured.
Get Fallback Origin
var fallback = await cf.Zones.CustomHostnames.GetFallbackOriginAsync(zoneId);
Console.WriteLine($"Fallback origin: {fallback.Origin}");
Set Fallback Origin
var fallback = await cf.Zones.CustomHostnames.UpdateFallbackOriginAsync(zoneId,
new UpdateFallbackOriginRequest(Origin: "fallback.origin.example.com"));
Delete Fallback Origin
await cf.Zones.CustomHostnames.DeleteFallbackOriginAsync(zoneId);
Models Reference
CustomHostname
| Property | Type | Description |
|---|---|---|
Id |
string |
Unique identifier |
Hostname |
string |
The custom hostname (e.g., app.customer.com) |
Status |
CustomHostnameStatus |
Current status (see enum below) |
Ssl |
SslResponse |
SSL certificate status and configuration |
OwnershipVerification |
OwnershipVerification? |
TXT validation details |
OwnershipVerificationHttp |
OwnershipVerificationHttp? |
HTTP validation details |
VerificationErrors |
IReadOnlyList<string>? |
Validation error messages |
CustomMetadata |
JsonElement? |
Arbitrary JSON metadata |
CustomOriginServer |
string? |
Custom origin hostname |
CustomOriginSni |
string? |
SNI for origin TLS handshake |
CreatedAt |
DateTimeOffset? |
Creation timestamp |
CustomHostnameStatus
| Value | Description |
|---|---|
Pending |
Pending initial validation |
Active |
Active and serving traffic |
ActiveRedeploying |
Active, changes being redeployed |
Moved |
Moved to another zone |
PendingDeletion |
Pending deletion |
Deleted |
Deleted |
Blocked |
Blocked due to policy violation |
SslStatus
| Value | Description |
|---|---|
Initializing |
Certificate being initialized |
PendingValidation |
Pending domain control validation |
PendingIssuance |
Pending CA issuance |
PendingDeployment |
Pending edge deployment |
Active |
Active and serving traffic |
Expired |
Certificate expired |
DcvMethod (Domain Control Validation)
| Value | Description |
|---|---|
Http |
Serve token at specific URL |
Txt |
Add TXT record to DNS |
Email |
Email approval to domain contacts |
CertificateAuthority
| Value | Description |
|---|---|
Digicert |
DigiCert CA |
Google |
Google Trust Services |
LetsEncrypt |
Let's Encrypt |
SslCom |
SSL.com |
Common Patterns
Complete Onboarding Flow
public class CustomerOnboardingService(ICloudflareApiClient cf)
{
public async Task<OnboardingResult> OnboardAsync(
string zoneId,
string customerDomain,
string customerId)
{
// Create hostname with metadata
var metadata = JsonSerializer.SerializeToElement(new { customerId });
var hostname = await cf.Zones.CustomHostnames.CreateAsync(zoneId,
new CreateCustomHostnameRequest(
Hostname: customerDomain,
Ssl: new SslConfiguration(Method: DcvMethod.Txt),
CustomMetadata: metadata
));
return new OnboardingResult
{
HostnameId = hostname.Id,
Status = hostname.Status,
TxtName = hostname.OwnershipVerification?.Name,
TxtValue = hostname.OwnershipVerification?.Value
};
}
public async Task<bool> CheckValidationAsync(string zoneId, string hostnameId)
{
var hostname = await cf.Zones.CustomHostnames.GetAsync(zoneId, hostnameId);
return hostname.Status == CustomHostnameStatus.Active;
}
}
Bulk Status Check
public async Task<Dictionary<string, CustomHostnameStatus>> GetStatusesAsync(string zoneId)
{
var statuses = new Dictionary<string, CustomHostnameStatus>();
await foreach (var hostname in cf.Zones.CustomHostnames.ListAllAsync(zoneId))
{
statuses[hostname.Hostname] = hostname.Status;
}
return statuses;
}
Required Permissions
| Permission | Scope | Level |
|---|---|---|
| SSL and Certificates | Zone | Read (for listing/get) |
| SSL and Certificates | Zone | Write (for create/update/delete) |