Managing DNS Records with the Cloudflare API

Fred· AI Engineer & Developer Educator13 min read

Updating DNS records through Cloudflare's web interface gets old fast. If you're managing multiple domains, running deployments, or just tired of clicking through menus, the Cloudflare API will save you hours of work. This guide shows you exactly how to automate your DNS management with working code examples.

What You Need

  • A Cloudflare account with at least one domain
  • Basic understanding of DNS (A records, CNAMEs, etc.)
  • curl, Node.js, Python, or PHP installed

Getting Your API Credentials

You need an API token to interact with Cloudflare. Here's how to create one:

  1. Log into your Cloudflare dashboard
  2. Go to My ProfileAPI Tokens
  3. Click Create Token
  4. Use the "Edit zone DNS" template
  5. Set permissions to Zone → DNS → Edit
  6. Select the zones this token can access
  7. Create the token and save it immediately (you won't see it again)

Important Note: Store your API token securely. Never commit it to version control. Use environment variables instead.

API Fundamentals

All Cloudflare API requests go to: https://api.cloudflare.com/client/v4/

Every request needs these headers:

  • Authorization: Bearer YOUR_API_TOKEN
  • Content-Type: application/json

Step 1: Find Your Zone ID

Every domain in Cloudflare has a Zone ID, which us usually present in the address bar when navigating through your project on Cloudfalre. You'll need this for all DNS operations.

Run this once and save the ID:

curl

curl -X GET "https://api.cloudflare.com/client/v4/zones" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json"

JavaScript

const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
const BASE_URL = 'https://api.cloudflare.com/client/v4';

async function listZones() {
  const response = await fetch(`${BASE_URL}/zones`, {
    headers: {
      'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
      'Content-Type': 'application/json'
    }
  });

  const data = await response.json();
  
  if (data.success) {
    data.result.forEach(zone => {
      console.log(`Domain: ${zone.name}, Zone ID: ${zone.id}`);
    });
  }
}

listZones();

Python

import requests
import os

CLOUDFLARE_API_TOKEN = os.environ.get('CLOUDFLARE_API_TOKEN')
BASE_URL = 'https://api.cloudflare.com/client/v4'

def list_zones():
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    response = requests.get(f'{BASE_URL}/zones', headers=headers)
    data = response.json()
    
    if data['success']:
        for zone in data['result']:
            print(f"Domain: {zone['name']}, Zone ID: {zone['id']}")

list_zones()

PHP

<?php
$token = $_ENV['CLOUDFLARE_API_TOKEN'];
$baseUrl = 'https://api.cloudflare.com/client/v4';

$ch = curl_init("$baseUrl/zones");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $token",
    "Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
$data = json_decode($response, true);

if ($data['success']) {
    foreach ($data['result'] as $zone) {
        echo "Domain: {$zone['name']}, Zone ID: {$zone['id']}\n";
    }
}

curl_close($ch);

Tip: Save your Zone ID in an environment variable. You'll use it for every DNS operation.

Step 2: List All DNS Records

This will display all DNS records for your domain. You'll need record IDs for updates and deletions:

curl

ZONE_ID="your_zone_id_here"

curl -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json"

# Filter by type (optional)
curl -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?type=A" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json"

JavaScript

async function listDNSRecords(zoneId, type = null) {
  let url = `${BASE_URL}/zones/${zoneId}/dns_records`;
  if (type) url += `?type=${type}`;
  
  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
      'Content-Type': 'application/json'
    }
  });

  const data = await response.json();
  
  if (data.success) {
    data.result.forEach(record => {
      console.log(`${record.type} | ${record.name} | ${record.content} | ID: ${record.id}`);
    });
    return data.result;
  }
}

// List all records
listDNSRecords('your_zone_id');

// List only A records
listDNSRecords('your_zone_id', 'A');

Python

def list_dns_records(zone_id, record_type=None):
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    url = f'{BASE_URL}/zones/{zone_id}/dns_records'
    if record_type:
        url += f'?type={record_type}'
    
    response = requests.get(url, headers=headers)
    data = response.json()
    
    if data['success']:
        for record in data['result']:
            print(f"{record['type']} | {record['name']} | {record['content']} | ID: {record['id']}")
        return data['result']

# List all records
list_dns_records('your_zone_id')

# List only A records  
list_dns_records('your_zone_id', 'A')

PHP

<?php
function listDNSRecords($zoneId, $type = null) {
    global $token, $baseUrl;
    
    $url = "$baseUrl/zones/$zoneId/dns_records";
    if ($type) $url .= "?type=$type";
    
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer $token",
        "Content-Type: application/json"
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $data = json_decode($response, true);
    
    if ($data['success']) {
        foreach ($data['result'] as $record) {
            echo "{$record['type']} | {$record['name']} | {$record['content']} | ID: {$record['id']}\n";
        }
        return $data['result'];
    }
    
    curl_close($ch);
}

// List all records
listDNSRecords('your_zone_id');

// List only A records
listDNSRecords('your_zone_id', 'A');

Step 3: Create DNS Records

Creating new DNS records is straightforward. Here's what each field means:

Field Description Example
type Record type A, AAAA, CNAME, TXT, MX
name Full domain or subdomain app.example.com or @ for root
content The value (IP, domain, text) 192.0.2.1
ttl Time to live (1 = automatic) 1 or 3600 (seconds)
proxied Use Cloudflare's proxy/CDN true or false

Create an "A" Record

An "A" record is the IP of the server you wish to point to. Add an "A" record with the following:

curl

ZONE_ID="your_zone_id_here"

curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "A",
    "name": "app.example.com",
    "content": "192.0.2.1",
    "ttl": 1,
    "proxied": true
  }'

JavaScript

async function createARecord(zoneId, name, ip, proxied = true) {
  const response = await fetch(`${BASE_URL}/zones/${zoneId}/dns_records`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      type: 'A',
      name: name,
      content: ip,
      ttl: 1,
      proxied: proxied
    })
  });

  const data = await response.json();
  
  if (data.success) {
    console.log('A record created:', data.result);
    return data.result;
  } else {
    console.error('Error:', data.errors);
  }
}

createARecord('your_zone_id', 'app.example.com', '192.0.2.1');

Python

def create_a_record(zone_id, name, ip, proxied=True):
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    record_data = {
        'type': 'A',
        'name': name,
        'content': ip,
        'ttl': 1,
        'proxied': proxied
    }
    
    response = requests.post(
        f'{BASE_URL}/zones/{zone_id}/dns_records',
        headers=headers,
        json=record_data
    )
    
    data = response.json()
    
    if data['success']:
        print('A record created:', data['result'])
        return data['result']
    else:
        print('Error:', data['errors'])

create_a_record('your_zone_id', 'app.example.com', '192.0.2.1')

PHP

<?php
function createARecord($zoneId, $name, $ip, $proxied = true) {
    global $token, $baseUrl;
    
    $data = [
        'type' => 'A',
        'name' => $name,
        'content' => $ip,
        'ttl' => 1,
        'proxied' => $proxied
    ];
    
    $ch = curl_init("$baseUrl/zones/$zoneId/dns_records");
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer $token",
        "Content-Type: application/json"
    ]);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $result = json_decode($response, true);
    
    if ($result['success']) {
        echo "A record created\n";
        return $result['result'];
    } else {
        echo "Error: " . json_encode($result['errors']) . "\n";
    }
    
    curl_close($ch);
}

createARecord('your_zone_id', 'app.example.com', '192.0.2.1');

Create Other Record Types

CNAMES are for the subdomains and redirecting "www" prefixes to the root domain.

CNAME Record (point www to root domain)

curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "CNAME",
    "name": "www.example.com",
    "content": "example.com",
    "ttl": 1,
    "proxied": true
  }'

TXT Record (for domain verification, SPF, etc.)

curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "TXT",
    "name": "_verification.example.com",
    "content": "verification-code-12345",
    "ttl": 1
  }'

MX Record (for email)

curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "MX",
    "name": "example.com",
    "content": "mail.example.com",
    "priority": 10,
    "ttl": 3600
  }'

Step 4: Update DNS Records

To update a record, you need its ID (from Step 2). Use PUT to replace the entire record or PATCH to update specific fields:

curl

# Full update (PUT)
RECORD_ID="your_record_id_here"

curl -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "A",
    "name": "app.example.com",
    "content": "203.0.113.50",
    "ttl": 1,
    "proxied": true
  }'

# Partial update (PATCH) - just change the IP
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "content": "203.0.113.100"
  }'

JavaScript

async function updateDNSRecord(zoneId, recordId, updates) {
  // Use PATCH for partial updates
  const response = await fetch(
    `${BASE_URL}/zones/${zoneId}/dns_records/${recordId}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(updates)
    }
  );

  const data = await response.json();
  
  if (data.success) {
    console.log('Record updated:', data.result);
    return data.result;
  } else {
    console.error('Error:', data.errors);
  }
}

// Just update the IP
updateDNSRecord('zone_id', 'record_id', { content: '203.0.113.100' });

Python

def update_dns_record(zone_id, record_id, updates):
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    # Use PATCH for partial updates
    response = requests.patch(
        f'{BASE_URL}/zones/{zone_id}/dns_records/{record_id}',
        headers=headers,
        json=updates
    )
    
    data = response.json()
    
    if data['success']:
        print('Record updated:', data['result'])
        return data['result']
    else:
        print('Error:', data['errors'])

# Just update the IP
update_dns_record('zone_id', 'record_id', {'content': '203.0.113.100'})

PHP

<?php
function updateDNSRecord($zoneId, $recordId, $updates) {
    global $token, $baseUrl;
    
    $ch = curl_init("$baseUrl/zones/$zoneId/dns_records/$recordId");
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer $token",
        "Content-Type: application/json"
    ]);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($updates));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $result = json_decode($response, true);
    
    if ($result['success']) {
        echo "Record updated\n";
        return $result['result'];
    } else {
        echo "Error: " . json_encode($result['errors']) . "\n";
    }
    
    curl_close($ch);
}

// Just update the IP
updateDNSRecord('zone_id', 'record_id', ['content' => '203.0.113.100']);

Step 5: Delete DNS Records

Deleting a record is simple. Just need the record ID:

curl

RECORD_ID="your_record_id_here"

curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json"

JavaScript

async function deleteDNSRecord(zoneId, recordId) {
  const response = await fetch(
    `${BASE_URL}/zones/${zoneId}/dns_records/${recordId}`,
    {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
        'Content-Type': 'application/json'
      }
    }
  );

  const data = await response.json();
  
  if (data.success) {
    console.log('Record deleted successfully');
    return true;
  } else {
    console.error('Error:', data.errors);
    return false;
  }
}

deleteDNSRecord('zone_id', 'record_id');

Python

def delete_dns_record(zone_id, record_id):
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    response = requests.delete(
        f'{BASE_URL}/zones/{zone_id}/dns_records/{record_id}',
        headers=headers
    )
    
    data = response.json()
    
    if data['success']:
        print('Record deleted successfully')
        return True
    else:
        print('Error:', data['errors'])
        return False

delete_dns_record('zone_id', 'record_id')

PHP

<?php
function deleteDNSRecord($zoneId, $recordId) {
    global $token, $baseUrl;
    
    $ch = curl_init("$baseUrl/zones/$zoneId/dns_records/$recordId");
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer $token",
        "Content-Type: application/json"
    ]);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $result = json_decode($response, true);
    
    if ($result['success']) {
        echo "Record deleted successfully\n";
        return true;
    } else {
        echo "Error: " . json_encode($result['errors']) . "\n";
        return false;
    }
    
    curl_close($ch);
}

deleteDNSRecord('zone_id', 'record_id');

Practical Example: Dynamic DNS Updater

Here's a complete script that updates your DNS when your IP changes. Perfect for home servers:

Python Version

#!/usr/bin/env python3
import requests
import os
import sys

CLOUDFLARE_API_TOKEN = os.environ.get('CLOUDFLARE_API_TOKEN')
ZONE_ID = os.environ.get('CLOUDFLARE_ZONE_ID')
RECORD_NAME = 'home.example.com'
BASE_URL = 'https://api.cloudflare.com/client/v4'

def get_public_ip():
    """Get current public IP address"""
    try:
        response = requests.get('https://api.ipify.org?format=json')
        return response.json()['ip']
    except:
        print("Failed to get public IP")
        sys.exit(1)

def get_dns_record(zone_id, record_name):
    """Find the DNS record by name"""
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    response = requests.get(
        f'{BASE_URL}/zones/{zone_id}/dns_records?name={record_name}&type=A',
        headers=headers
    )
    
    data = response.json()
    
    if data['success'] and len(data['result']) > 0:
        record = data['result'][0]
        return record['id'], record['content']
    return None, None

def update_dns_record(zone_id, record_id, record_name, new_ip):
    """Update DNS record with new IP"""
    headers = {
        'Authorization': f'Bearer {CLOUDFLARE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    response = requests.patch(
        f'{BASE_URL}/zones/{zone_id}/dns_records/{record_id}',
        headers=headers,
        json={'content': new_ip}
    )
    
    return response.json()

def main():
    # Get current public IP
    current_ip = get_public_ip()
    print(f'Current public IP: {current_ip}')
    
    # Get existing DNS record
    record_id, existing_ip = get_dns_record(ZONE_ID, RECORD_NAME)
    
    if not record_id:
        print(f'DNS record {RECORD_NAME} not found. Create it first.')
        sys.exit(1)
    
    print(f'DNS currently points to: {existing_ip}')
    
    # Update if IP changed
    if current_ip != existing_ip:
        print(f'IP changed! Updating DNS...')
        result = update_dns_record(ZONE_ID, record_id, RECORD_NAME, current_ip)
        
        if result['success']:
            print(f'✓ DNS updated to {current_ip}')
        else:
            print(f'✗ Update failed: {result["errors"]}')
            sys.exit(1)
    else:
        print('IP unchanged. Nothing to do.')

if __name__ == '__main__':
    main()

JavaScript Version

#!/usr/bin/env node

const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
const ZONE_ID = process.env.CLOUDFLARE_ZONE_ID;
const RECORD_NAME = 'home.example.com';
const BASE_URL = 'https://api.cloudflare.com/client/v4';

async function getPublicIP() {
    try {
        const response = await fetch('https://api.ipify.org?format=json');
        const data = await response.json();
        return data.ip;
    } catch (error) {
        console.error('Failed to get public IP');
        process.exit(1);
    }
}

async function getDNSRecord(zoneId, recordName) {
    const response = await fetch(
        `${BASE_URL}/zones/${zoneId}/dns_records?name=${recordName}&type=A`,
        {
            headers: {
                'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
                'Content-Type': 'application/json'
            }
        }
    );
    
    const data = await response.json();
    
    if (data.success && data.result.length > 0) {
        const record = data.result[0];
        return { id: record.id, content: record.content };
    }
    return { id: null, content: null };
}

async function updateDNSRecord(zoneId, recordId, newIP) {
    const response = await fetch(
        `${BASE_URL}/zones/${zoneId}/dns_records/${recordId}`,
        {
            method: 'PATCH',
            headers: {
                'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ content: newIP })
        }
    );
    
    return await response.json();
}

async function main() {
    // Get current public IP
    const currentIP = await getPublicIP();
    console.log(`Current public IP: ${currentIP}`);
    
    // Get existing DNS record
    const { id: recordId, content: existingIP } = await getDNSRecord(ZONE_ID, RECORD_NAME);
    
    if (!recordId) {
        console.error(`DNS record ${RECORD_NAME} not found. Create it first.`);
        process.exit(1);
    }
    
    console.log(`DNS currently points to: ${existingIP}`);
    
    // Update if IP changed
    if (currentIP !== existingIP) {
        console.log('IP changed! Updating DNS...');
        const result = await updateDNSRecord(ZONE_ID, recordId, currentIP);
        
        if (result.success) {
            console.log(`✓ DNS updated to ${currentIP}`);
        } else {
            console.error(`✗ Update failed:`, result.errors);
            process.exit(1);
        }
    } else {
        console.log('IP unchanged. Nothing to do.');
    }
}

main();

Setup tip: Add this script to cron (Linux/Mac) or Task Scheduler (Windows) to run every 5-10 minutes for automatic dynamic DNS.

Error Handling

Always check the success field in responses. Cloudflare provides detailed error messages:

JavaScript

async function safeAPICall(url, options) {
  try {
    const response = await fetch(url, options);
    const data = await response.json();
    
    if (!data.success) {
      console.error('API Error:', data.errors);
      data.errors.forEach(error => {
        console.error(`Code ${error.code}: ${error.message}`);
      });
      return null;
    }
    
    return data.result;
  } catch (error) {
    console.error('Request failed:', error.message);
    return null;
  }
}

Python

def safe_api_call(method, url, headers, json_data=None):
    try:
        if method == 'GET':
            response = requests.get(url, headers=headers)
        elif method == 'POST':
            response = requests.post(url, headers=headers, json=json_data)
        elif method == 'PATCH':
            response = requests.patch(url, headers=headers, json=json_data)
        elif method == 'DELETE':
            response = requests.delete(url, headers=headers)
        
        data = response.json()
        
        if not data['success']:
            print('API Error:', data['errors'])
            for error in data['errors']:
                print(f"Code {error['code']}: {error['message']}")
            return None
        
        return data['result']
    except Exception as e:
        print(f'Request failed: {e}')
        return None

Rate Limits

Cloudflare allows 1200 requests per 5 minutes per API token. If you hit the limit, you'll get a 429 status code. The response includes a Retry-After header telling you how long to wait.

For production scripts, implement retry logic with exponential backoff. Most operations won't hit these limits unless you're doing bulk updates.

Security Best Practices

  • Never hardcode tokens - Use environment variables
  • Use .env files locally - Add to .gitignore immediately
  • Create specific tokens - One per application, minimal permissions
  • Rotate tokens regularly - Set calendar reminders
  • Monitor token usage - Check Cloudflare's audit logs

Quick Reference

Operation Method Endpoint
List zones GET /zones
List DNS records GET /zones/{zone_id}/dns_records
Create DNS record POST /zones/{zone_id}/dns_records
Update DNS record PUT/PATCH /zones/{zone_id}/dns_records/{record_id}
Delete DNS record DELETE /zones/{zone_id}/dns_records/{record_id}

Common DNS Record Types

Type Purpose Example Content
A IPv4 address 192.0.2.1
AAAA IPv6 address 2001:db8::1
CNAME Alias to another domain example.com
MX Mail server (with priority) mail.example.com
TXT Text data (SPF, DKIM, verification) v=spf1 include:_spf.example.com ~all

Conclusion

The Cloudflare API makes DNS automation straightforward. Whether you're building deployment pipelines, managing multiple domains, or just tired of clicking through web interfaces, these API calls will save you hours.

Start simple - maybe just automate your most common DNS task. Once you see how much time it saves, you'll find yourself automating more and more of your DNS management.

Related Guides

Official Cloudflare Resources

Fred

Fred

AUTHOR

Full-stack developer with 10+ years building production applications. I've been deploying to Cloudflare's edge network since Workers launched in 2017.