Last active
February 1, 2026 17:18
-
-
Save alexios-angel/fd236e27014e311e934b58c43e7cbda2 to your computer and use it in GitHub Desktop.
Get domain/zone information - Cloudflare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Cloudflare Domain Information Exporter | |
| Exports domain information from Cloudflare to a CSV file including: | |
| - Domain Name | |
| - Status (Active/Inactive) | |
| - Registrar | |
| - Expires | |
| - Set to Auto Renew? | |
| - Number of Unique Visitors (last 30 days) | |
| - Zone ID | |
| - Plan (Free/Business/Enterprise) | |
| Usage: | |
| python cloudflare_domain_export.py --api-token YOUR_API_TOKEN --account-id YOUR_ACCOUNT_ID [-o output.csv] | |
| Or using environment variables: | |
| export CLOUDFLARE_API_TOKEN=YOUR_API_TOKEN | |
| export CLOUDFLARE_ACCOUNT_ID=YOUR_ACCOUNT_ID | |
| python cloudflare_domain_export.py [-o output.csv] | |
| Required API Token Permissions: | |
| - Zone:Read (for all zones) | |
| - Analytics:Read (for unique visitor stats via GraphQL) | |
| - Registrar:Read (for domain expiry/auto-renew info - if domains registered with CF) | |
| --------------------------------------------------------------------------------------------- | |
| Copyright 2026 Alexios Angel | |
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software | |
| and associated documentation files (the “Software”), to deal in the Software without | |
| restriction, including without limitation the rights to use, copy, modify, merge, publish, | |
| distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom | |
| the Software is furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all copies or | |
| substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING | |
| BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |
| DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| """ | |
| import argparse | |
| import csv | |
| import os | |
| import sys | |
| from datetime import datetime, timedelta, timezone | |
| try: | |
| import cloudflare | |
| from cloudflare import Cloudflare | |
| except ImportError: | |
| print("Error: cloudflare package not installed.") | |
| print("Install it with: pip install cloudflare") | |
| sys.exit(1) | |
| try: | |
| import requests | |
| except ImportError: | |
| print("Error: requests package not installed.") | |
| print("Install it with: pip install requests") | |
| sys.exit(1) | |
| try: | |
| from whois import whois as whois_lookup | |
| except ImportError: | |
| print("Error: python-whois package not installed.") | |
| print("Install it with: pip install python-whois") | |
| sys.exit(1) | |
| def get_all_zones(client: Cloudflare) -> list: | |
| """ | |
| Fetch all zones (domains) from Cloudflare account using auto-pagination. | |
| The official SDK provides auto-paginating iterators. | |
| """ | |
| zones = [] | |
| # The SDK auto-paginates when iterating | |
| for zone in client.zones.list(): | |
| zones.append(zone) | |
| return zones | |
| def get_unique_visitors_graphql(api_token: str, zone_id: str) -> str: | |
| """ | |
| Fetch unique visitor count for a zone using GraphQL API (last 30 days). | |
| The old zone analytics API is deprecated; GraphQL is the current method. | |
| """ | |
| try: | |
| end_date = datetime.now(timezone.utc).date() | |
| start_date = end_date - timedelta(days=30) | |
| headers = { | |
| 'Authorization': f'Bearer {api_token}', | |
| 'Content-Type': 'application/json' | |
| } | |
| # GraphQL query for unique visitors using httpRequests1dGroups | |
| query = """ | |
| query GetUniqueVisitors($zoneTag: String!, $date_geq: String!, $date_lt: String!) { | |
| viewer { | |
| zones(filter: { zoneTag: $zoneTag }) { | |
| httpRequests1dGroups( | |
| limit: 10000, | |
| filter: { date_geq: $date_geq, date_lt: $date_lt } | |
| ) { | |
| uniq { | |
| uniques | |
| } | |
| } | |
| } | |
| } | |
| } | |
| """ | |
| variables = { | |
| 'zoneTag': zone_id, | |
| 'date_geq': start_date.isoformat(), | |
| 'date_lt': end_date.isoformat() | |
| } | |
| response = requests.post( | |
| 'https://api.cloudflare.com/client/v4/graphql', | |
| headers=headers, | |
| json={'query': query, 'variables': variables}, | |
| timeout=30 | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if 'data' in data and data['data']: | |
| zones_data = data['data'].get('viewer', {}).get('zones', []) | |
| if zones_data: | |
| groups = zones_data[0].get('httpRequests1dGroups', []) | |
| # Sum up unique visitors across all days | |
| total_uniques = sum( | |
| group.get('uniq', {}).get('uniques', 0) | |
| for group in groups | |
| ) | |
| return str(total_uniques) | |
| return 'N/A' | |
| except Exception as e: | |
| # Analytics might not be available for all plans or zones | |
| return 'N/A' | |
| def get_registrar_domains(client: Cloudflare, account_id: str) -> dict: | |
| """ | |
| Fetch all domains registered with Cloudflare Registrar for the account. | |
| Returns a dict mapping domain name to registration info. | |
| """ | |
| domains_info = {} | |
| try: | |
| # Use auto-pagination for registrar domains | |
| for domain in client.registrar.domains.list(account_id=account_id): | |
| domain_name = getattr(domain, 'name', None) | |
| if not domain_name: | |
| # Some responses use 'id' as the domain identifier | |
| domain_name = getattr(domain, 'id', None) | |
| if domain_name: | |
| domains_info[domain_name] = { | |
| 'expires_at': getattr(domain, 'expires_at', None), | |
| 'auto_renew': getattr(domain, 'auto_renew', None), | |
| 'locked': getattr(domain, 'locked', None), | |
| } | |
| except cloudflare.NotFoundError: | |
| # Account may not have registrar access | |
| pass | |
| except cloudflare.PermissionDeniedError: | |
| # Token may not have registrar permissions | |
| print("Warning: No permission to access Registrar API. Expiry/auto-renew data will be N/A.") | |
| except Exception as e: | |
| print(f"Warning: Could not fetch registrar domains: {e}") | |
| return domains_info | |
| def get_whois_info(domain_name: str) -> dict: | |
| """ | |
| Perform WHOIS lookup to get registrar and expiry info for non-Cloudflare domains. | |
| Returns dict with 'registrar' and 'expires' keys. | |
| """ | |
| try: | |
| w = whois_lookup(domain_name) | |
| registrar = None | |
| expires = None | |
| # whois returns a dict-like object | |
| # Get registrar | |
| if 'registrar' in w and w['registrar']: | |
| registrar = w['registrar'] | |
| # Get expiration date | |
| if 'expiration_date' in w and w['expiration_date']: | |
| exp_date = w['expiration_date'] | |
| # Sometimes returns a list of dates | |
| if isinstance(exp_date, list): | |
| exp_date = exp_date[0] | |
| if hasattr(exp_date, 'strftime'): | |
| expires = exp_date.strftime('%Y-%m-%d') | |
| else: | |
| expires = str(exp_date)[:10] | |
| return { | |
| 'registrar': registrar, | |
| 'expires': expires | |
| } | |
| except Exception as e: | |
| # WHOIS lookup can fail for various reasons | |
| return { | |
| 'registrar': None, | |
| 'expires': None | |
| } | |
| def format_plan_name(plan) -> str: | |
| """Extract and format plan name.""" | |
| if plan is None: | |
| return 'Unknown' | |
| if hasattr(plan, 'name'): | |
| return plan.name | |
| elif isinstance(plan, dict): | |
| return plan.get('name', 'Unknown') | |
| return 'Unknown' | |
| def format_status(status: str) -> str: | |
| """Format zone status to Active/Inactive.""" | |
| if status and status.lower() == 'active': | |
| return 'Active' | |
| return 'Inactive' | |
| def export_domains_to_csv(api_token: str, account_id: str, output_file: str): | |
| """Main function to export domain information to CSV.""" | |
| # Initialize Cloudflare client with API token | |
| client = Cloudflare(api_token=api_token) | |
| print(f"Using Account ID: {account_id}") | |
| print("Fetching zones from Cloudflare...") | |
| zones = get_all_zones(client) | |
| print(f"Found {len(zones)} zones") | |
| # Fetch registrar domain info (for expiry and auto-renew) | |
| print("Fetching registrar domain information...") | |
| registrar_domains = get_registrar_domains(client, account_id) | |
| print(f"Found {len(registrar_domains)} domains in Cloudflare Registrar") | |
| # Prepare CSV data | |
| csv_data = [] | |
| for i, zone in enumerate(zones, 1): | |
| domain_name = zone.name | |
| print(f"Processing [{i}/{len(zones)}]: {domain_name}") | |
| # Basic zone info | |
| zone_id = zone.id | |
| status = format_status(zone.status) | |
| plan = format_plan_name(zone.plan) | |
| original_registrar = getattr(zone, 'original_registrar', None) | |
| # Get domain registration info from pre-fetched registrar data | |
| reg_info = registrar_domains.get(domain_name, {}) | |
| is_cloudflare_registrar = bool(reg_info) | |
| # Initialize values | |
| registrar = None | |
| expires_str = 'N/A' | |
| auto_renew_str = 'N/A' | |
| if is_cloudflare_registrar: | |
| # Domain is registered with Cloudflare | |
| registrar = 'Cloudflare' | |
| expires = reg_info.get('expires_at') | |
| auto_renew = reg_info.get('auto_renew') | |
| # Format expiry date | |
| if expires: | |
| if isinstance(expires, str): | |
| expires_str = expires[:10] | |
| elif hasattr(expires, 'strftime'): | |
| expires_str = expires.strftime('%Y-%m-%d') | |
| else: | |
| expires_str = str(expires)[:10] | |
| # Format auto-renew | |
| if auto_renew is not None: | |
| auto_renew_str = 'Yes' if auto_renew else 'No' | |
| else: | |
| # Domain is not registered with Cloudflare | |
| # First try the original_registrar from zone info | |
| if original_registrar: | |
| registrar = original_registrar | |
| # If registrar is still unknown, do WHOIS lookup | |
| if not registrar or registrar.lower() == 'unknown': | |
| print(f" → WHOIS lookup for {domain_name}...") | |
| whois_info = get_whois_info(domain_name) | |
| if whois_info.get('registrar'): | |
| registrar = whois_info['registrar'] | |
| if whois_info.get('expires'): | |
| expires_str = whois_info['expires'] | |
| if not registrar: | |
| registrar = 'Unknown' | |
| # Get analytics (unique visitors) via GraphQL | |
| unique_visitors = get_unique_visitors_graphql(api_token, zone_id) | |
| csv_data.append({ | |
| 'Domain Name': domain_name, | |
| 'Status': status, | |
| 'Registrar': registrar, | |
| 'Expires': expires_str, | |
| 'Set to Auto Renew?': auto_renew_str, | |
| 'Number of Unique Visitors': unique_visitors, | |
| 'Zone ID': zone_id, | |
| 'Plan': plan | |
| }) | |
| # Write to CSV | |
| fieldnames = [ | |
| 'Domain Name', | |
| 'Status', | |
| 'Registrar', | |
| 'Expires', | |
| 'Set to Auto Renew?', | |
| 'Number of Unique Visitors', | |
| 'Zone ID', | |
| 'Plan' | |
| ] | |
| with open(output_file, 'w', newline='', encoding='utf-8') as csvfile: | |
| writer = csv.DictWriter(csvfile, fieldnames=fieldnames) | |
| writer.writeheader() | |
| writer.writerows(csv_data) | |
| print(f"\nExport complete! Data saved to: {output_file}") | |
| print(f"Total domains exported: {len(csv_data)}") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Export Cloudflare domain information to CSV', | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python cloudflare_domain_export.py --api-token YOUR_API_TOKEN --account-id YOUR_ACCOUNT_ID | |
| python cloudflare_domain_export.py -t YOUR_API_TOKEN -a YOUR_ACCOUNT_ID -o my_domains.csv | |
| Using environment variables: | |
| export CLOUDFLARE_API_TOKEN=YOUR_API_TOKEN | |
| export CLOUDFLARE_ACCOUNT_ID=YOUR_ACCOUNT_ID | |
| python cloudflare_domain_export.py | |
| Creating an API Token: | |
| 1. Go to https://dash.cloudflare.com/profile/api-tokens | |
| 2. Click "Create Token" | |
| 3. Use "Custom token" and add these permissions: | |
| - Zone > Zone > Read | |
| - Zone > Analytics > Read | |
| - Account > Registrar: Domains > Read (for expiry/auto-renew info) | |
| 4. Set Zone Resources to "Include > All zones" | |
| Finding your Account ID: | |
| 1. Go to https://dash.cloudflare.com | |
| 2. Select any domain in your account | |
| 3. Find "Account ID" in the right sidebar under "API" | |
| Notes: | |
| - Uses API Token authentication (recommended over Global API Key) | |
| - Domain registration info (expiry, auto-renew) is only available for | |
| domains registered through Cloudflare Registrar | |
| - For non-Cloudflare domains, WHOIS lookup is performed to get registrar info | |
| - Unique visitors are fetched via GraphQL API (last 30 days) | |
| - The script handles pagination automatically for accounts with many domains | |
| """ | |
| ) | |
| parser.add_argument( | |
| '--api-token', '-t', | |
| help='Cloudflare API Token (or set CLOUDFLARE_API_TOKEN env var)', | |
| default=os.environ.get('CLOUDFLARE_API_TOKEN') | |
| ) | |
| parser.add_argument( | |
| '--account-id', '-a', | |
| help='Cloudflare Account ID (or set CLOUDFLARE_ACCOUNT_ID env var)', | |
| default=os.environ.get('CLOUDFLARE_ACCOUNT_ID') | |
| ) | |
| parser.add_argument( | |
| '-o', '--output', | |
| help='Output CSV file path (default: cloudflare_domains.csv)', | |
| default='cloudflare_domains.csv' | |
| ) | |
| args = parser.parse_args() | |
| # Validate API token | |
| if not args.api_token: | |
| print("Error: API token is required.") | |
| print("Provide it via --api-token argument or CLOUDFLARE_API_TOKEN environment variable.") | |
| print("\nTo create an API token:") | |
| print(" 1. Go to https://dash.cloudflare.com/profile/api-tokens") | |
| print(" 2. Click 'Create Token'") | |
| print(" 3. Add permissions: Zone:Read, Analytics:Read, Registrar:Read") | |
| sys.exit(1) | |
| # Validate Account ID | |
| if not args.account_id: | |
| print("Error: Account ID is required.") | |
| print("Provide it via --account-id argument or CLOUDFLARE_ACCOUNT_ID environment variable.") | |
| print("\nTo find your Account ID:") | |
| print(" 1. Go to https://dash.cloudflare.com") | |
| print(" 2. Select any domain") | |
| print(" 3. Find 'Account ID' in the right sidebar under 'API'") | |
| sys.exit(1) | |
| try: | |
| export_domains_to_csv(args.api_token, args.account_id, args.output) | |
| except cloudflare.APIConnectionError as e: | |
| print(f"Error connecting to Cloudflare API: {e}") | |
| sys.exit(1) | |
| except cloudflare.AuthenticationError as e: | |
| print(f"Authentication failed. Check your API token: {e}") | |
| sys.exit(1) | |
| except cloudflare.PermissionDeniedError as e: | |
| print(f"Permission denied. Ensure your token has required permissions: {e}") | |
| sys.exit(1) | |
| except cloudflare.APIStatusError as e: | |
| print(f"Cloudflare API error: {e}") | |
| sys.exit(1) | |
| except Exception as e: | |
| print(f"Unexpected error: {e}") | |
| raise | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment