On this page
WHMCS DNS Automation with Cloudflare API: A Step-by-Step Guide
If you run a hosting business — especially one offering domain registration, reseller hosting, or VPS — you’ve probably spent too many hours manually adding DNS zones or records for new domains.
I’ve been there. Manual DNS setup in WHMCS is tedious, error-prone, and scales poorly. But when I integrated Cloudflare’s API to automate DNS provisioning, the time saved was massive — and my clients noticed too.
In this guide, I’ll walk you through how to automate domain DNS provisioning in WHMCS using the Cloudflare API — from setup to real-world code examples. No fluff, no theory. Just what works.
Why Automate DNS in WHMCS?
Before diving in, let’s be honest: WHMCS doesn’t natively support deep Cloudflare integration out-of-the-box. You can manually link domains to Cloudflare, but that’s not scalable.
Here’s what automation buys you:
- Zero-touch DNS provisioning for new domain registrations or transfers
- Instant DNS record updates (e.g., after server migration or SSL deployment)
- Consistent DNS setup — no more “why did this break?” debugging
- Reduced ticket volume — clients manage DNS self-service via WHMCS portal
If you’re using Cloudflare (and most of us are), this integration pays off fast.
Prerequisites
Before coding, make sure you have:
- ✅ A WHMCS installation (v8.5+ recommended)
- ✅ A Cloudflare API Token with permissions:
Zone:DNS:ReadandZone:DNS:Edit(we’ll set this up next) - ✅ Basic PHP/WHMCS hook knowledge (we’ll keep it simple)
- ✅ One or more domains added in Cloudflare as full (not partial) zones
Step 1: Create a Cloudflare API Token
This is the most common stumbling block — and it’s easy to mess up.
- Log in to Cloudflare Dashboard
- Go to My Profile → API Tokens
- Click Create Token
- Use the Custom Token template
- Set permissions:
- Zone: Read
- Zone: DNS: Edit
- Under Zone Resources, select All zones in all accounts (or restrict to specific zones if you’re cautious)
- Copy the token — do not lose it. You’ll never see it again.
🔒 Pro Tip: Never store this token in plain text in your WHMCS database. We’ll use WHMCS’s secure configuration file.
Step 2: Store the Token Securely in WHMCS
Open configuration.php (in /whmcsroot/includes/) and add:
// Cloudflare API configuration
$cloudflare_api_token = 'YOUR_CLOUDFLARE_API_TOKEN_HERE';
$cloudflare_api_email = '[email protected]'; // Only needed if using legacy API key
🛑 Do not hardcode secrets directly in hooks or modules. Always use
configuration.php.
Step 3: Hook Into WHMCS Domain Registration
We’ll use a WHMCS hook to trigger DNS provisioning right after a domain is registered.
Create a new file: includes/hooks/cloudflare_dns_provision.php
<?php
use WHMCS\Database\Capsule;
add_hook('AfterDomainRegister', 1, function($vars) {
$domainId = $vars['domainid'];
$domainName = $vars['domain'];
// Fetch domain configuration
$domain = Capsule::table('tblhosting')
->where('domainid', $domainId)
->first();
if (!$domain || $domain->domain != $domainName) {
logActivity("WHMCS: Skipping DNS provisioning — domain mismatch");
return;
}
// Only proceed if domain is active and managed via Cloudflare
$service = Capsule::table('tblhosting')
->where('id', $domain->id)
->where('domainstatus', 'Active')
->first();
if (!$service) return;
// Get Cloudflare zone ID (we’ll fetch it via API)
$zoneId = getCloudflareZoneId($domainName);
if (!$zoneId) {
logActivity("WHMCS: Cloudflare zone not found for {$domainName}");
return;
}
// Create default DNS records
$records = [
['type' => 'A', 'name' => '@', 'content' => $domain->serverip ?: '192.0.2.1', 'ttl' => 1, 'priority' => null],
['type' => 'A', 'name' => 'www', 'content' => $domain->serverip ?: '192.0.2.1', 'ttl' => 1, 'priority' => null],
['type' => 'MX', 'name' => '@', 'content' => 'mail.example.com', 'ttl' => 1, 'priority' => 10],
['type' => 'TXT', 'name' => '@', 'content' => 'v=spf1 include:_spf.google.com ~all', 'ttl' => 1, 'priority' => null],
];
foreach ($records as $record) {
createCloudflareDnsRecord($zoneId, $record);
}
logActivity("WHMCS: DNS records provisioned for {$domainName} via Cloudflare");
});
⚠️ This is a simplified example. In production, you’d want better error handling, retries, and logging.
Step 4: Helper Functions — Zone Detection & Record Creation
Add these to the same hook file (or a helper file you include):
function getCloudflareZoneId($domainName) {
$apiUrl = 'https://api.cloudflare.com/client/v4/zones';
$params = [
'name' => $domainName,
'status' => 'active',
'page' => 1,
'per_page' => 1
];
$query = http_build_query($params);
$ch = curl_init("{$apiUrl}?{$query}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $GLOBALS['cloudflare_api_token'],
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
logActivity("WHMCS: Cloudflare API error (zone lookup): {$httpCode}");
return false;
}
$data = json_decode($response, true);
if (!empty($data['result'][0]['id'])) {
return $data['result'][0]['id'];
}
return false;
}
function createCloudflareDnsRecord($zoneId, $record) {
$apiUrl = "https://api.cloudflare.com/client/v4/zones/{$zoneId}/dns_records";
$payload = json_encode([
'type' => $record['type'],
'name' => $record['name'],
'content' => $record['content'],
'ttl' => $record['ttl'],
'priority' => $record['priority'] ?? null,
'proxied' => false // Set to true if you want Cloudflare proxying
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $GLOBALS['cloudflare_api_token'],
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
logActivity("WHMCS: Failed to create DNS record ({$record['type']} {$record['name']}): {$httpCode} - " . curl_error($ch));
return false;
}
return true;
}
How It Works:
- The hook fires on
AfterDomainRegister - It fetches the Cloudflare zone ID by querying
/zoneswith the domain name - Then it creates basic DNS records: A, AAAA (optional), MX, TXT
- You can customize records per client or product — more on that below
Step 5: Handle Domain Transfers & Re-registrations
Domains aren’t always new — sometimes they’re transferred in. Add this hook:
add_hook('AfterDomainTransfer', 1, function($vars) {
// Reuse getCloudflareZoneId() and createCloudflareDnsRecord() from above
$domainName = $vars['domainname'];
$zoneId = getCloudflareZoneId($domainName);
if (!$zoneId) {
logActivity("WHMCS: Zone not found for transferred domain {$domainName}");
return;
}
// Optional: delete existing records first if re-syncing
deleteExistingDnsRecords($zoneId);
// Then provision again
$records = [
['type' => 'A', 'name' => '@', 'content' => '192.0.2.1', 'ttl' => 1],
['type' => 'MX', 'name' => '@', 'content' => 'mail.example.com', 'ttl' => 1, 'priority' => 10],
];
foreach ($records as $record) {
createCloudflareDnsRecord($zoneId, $record);
}
logActivity("WHMCS: DNS records synced for transferred domain {$domainName}");
});
💡 For full control, you might want to add a “DNS Sync” button in the client area — but that’s a future upgrade.
Step 6: Optional — Client Portal Integration
To let users manage DNS themselves, add a simple tab in the client area:
- Create
modules/domains/dns.php - Use WHMCS’s
ClientArea::setTemplateFilename()to inject content - Fetch existing records with:
function getCloudflareDnsRecords($zoneId) {
$apiUrl = "https://api.cloudflare.com/client/v4/zones/{$zoneId}/dns_records";
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $GLOBALS['cloudflare_api_token'],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
return $data['result'] ?? [];
}
Then render a table with edit/delete buttons. (Full implementation is out of scope here, but it’s just CRUD over the API.)
Comparison: Manual vs. Automated DNS Setup
| Task | Manual Setup | Automated (Cloudflare API) |
|---|---|---|
| Time per domain | 5–10 minutes | <1 second |
| Error risk | High (typos, missing MX) | Very low (validated payload) |
| Scaling | Fails at 100+ domains | Scales to 10k+ |
| Client self-service | Not possible | Yes (with extra UI) |
| Audit trail | None | Full log via logActivity() |
| Maintenance | Ongoing admin effort | One-time setup + monitoring |
I’ve seen hosting providers go from 2 hours/day of DNS work to near-zero — just by automating this.
Real-World Gotchas (And How to Avoid Them)
Here’s what I learned the hard way:
❌ 1. Using the Wrong API Endpoint
Cloudflare has two APIs:
- Legacy API (key + email) → deprecated
- API Tokens (recommended) → token-only, scoped permissions
Always use API Tokens, not Global API Keys.
❌ 2. Not Handling Rate Limits
Cloudflare allows ~1,200 requests/5 minutes per token. If you provision 500 domains at once, you’ll hit the limit.
Fix: Add exponential backoff in createCloudflareDnsRecord():
$retry = 0;
$maxRetries = 3;
while ($retry < $maxRetries) {
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode == 429) {
$retry++;
sleep(2 ** $retry); // exponential backoff
continue;
}
break;
}
❌ 3. Hardcoding Default Records
One-size-fits-all DNS records rarely work. I recommend:
- Grouping by product (e.g., “WordPress Hosting” → preloads A, MX, TXT with
wp-confighints) - Letting clients override via custom fields in WHMCS
❌ 4. Ignoring TTL and Proxied Status
Cloudflare’s default ttl: 1 (auto) is great for speed, but some services (like Google Workspace) require higher TTLs.
Always double-check:
'ttl' => ($record['type'] == 'MX') ? 3600 : 1,
'proxied' => ($record['type'] == 'A' && $domain->uses_cloudflare_proxy) ? true : false,
Testing Your Integration
Before going live:
- Use WHMCS’s Test Domain Registration feature
- Check
WHMCS → Utilities → Logs → Activity LogforWHMCS:entries - Manually verify in Cloudflare dashboard that records appear
- Try a failed scenario: remove zone permission → see if logs catch it
If the hook fires but records don’t appear, enable CURLOPT_VERBOSE temporarily in your cURL calls.
FAQ: WHMCS + Cloudflare DNS Automation
Q: Can I use this with Cloudflare Partner accounts?
A: Yes — just create a separate API token for each partner account and store them dynamically (e.g., by tblclients.customfield).
Q: Does this work with domain-only orders (no hosting)?
A: Yes — but you’ll need to hook into AfterDomainRegister and AfterDomainTransfer, and fetch the domain name from $vars['domainname'].
Q: What if Cloudflare is down?
A: Add a fallback: if API fails, log it and skip DNS creation — don’t block domain registration.
Q: Can I sync DNS changes from Cloudflare back to WHMCS?
A: Not easily — WHMCS doesn’t track DNS state. You’d need a separate cron job to poll Cloudflare and sync.
Q: Is this secure?
A: Yes — if you follow the guide: store tokens in configuration.php, never expose them in hooks or templates, and use API tokens (not global keys).
Final Thoughts
Automating DNS with Cloudflare in WHMCS isn’t just about saving time — it’s about reducing human risk. One typo in an MX record can break email for a whole business. One missing A record can make a site unreachable.
When I first built this, I worried about complexity. But after a few iterations, it became one of the most reliable parts of my hosting stack.
Start small: just provision A and MX for new domains. Then expand.
If you’re building a scalable hosting business, this is one automation you can’t afford to skip.
— Mahbubur
Sysadmin and WHMCS integrator
mahbuburriad.com