← Back to posts
AI & Automation

Using Claude to Automate Cisco Meraki via the Dashboard API

Meraki Automation with Claude

The Cisco Meraki Dashboard API is one of the cleanest REST APIs in the networking industry — every organization, network, and device is accessible via authenticated HTTPS calls returning JSON. This makes it an ideal platform for AI-assisted automation.

Claude adds the reasoning layer that the API alone can’t provide: interpreting alert patterns, generating consistent configuration across hundreds of networks, diagnosing client connectivity issues, and building natural language interfaces for network management tasks that previously required expert knowledge of Meraki’s UI.


Setup

import anthropic
import requests
import json

client = anthropic.Anthropic()

MERAKI_API_KEY = "your-meraki-api-key"
BASE_URL = "https://api.meraki.com/api/v1"

def meraki_get(path: str, params: dict = None) -> dict | list:
    r = requests.get(
        f"{BASE_URL}{path}",
        headers={"X-Cisco-Meraki-API-Key": MERAKI_API_KEY},
        params=params or {},
        timeout=30
    )
    r.raise_for_status()
    return r.json()

def meraki_put(path: str, payload: dict) -> dict:
    r = requests.put(
        f"{BASE_URL}{path}",
        headers={
            "X-Cisco-Meraki-API-Key": MERAKI_API_KEY,
            "Content-Type": "application/json"
        },
        json=payload,
        timeout=30
    )
    r.raise_for_status()
    return r.json()

def meraki_post(path: str, payload: dict) -> dict:
    r = requests.post(
        f"{BASE_URL}{path}",
        headers={
            "X-Cisco-Meraki-API-Key": MERAKI_API_KEY,
            "Content-Type": "application/json"
        },
        json=payload,
        timeout=30
    )
    r.raise_for_status()
    return r.json()

def ask_claude(prompt: str, system: str = None) -> str:
    kwargs = {
        "model": "claude-opus-4-5",
        "max_tokens": 2048,
        "messages": [{"role": "user", "content": prompt}]
    }
    if system:
        kwargs["system"] = system
    return client.messages.create(**kwargs).content[0].text

Pattern 1: Natural Language Network Configuration

Describe a network policy in plain English, get a Meraki API payload back:

def generate_meraki_config(network_id: str, description: str) -> dict:
    # Pull current network state for context
    current_config = {
        "vlans": meraki_get(f"/networks/{network_id}/appliance/vlans"),
        "firewall_rules": meraki_get(f"/networks/{network_id}/appliance/firewall/l3FirewallRules"),
        "ssids": meraki_get(f"/networks/{network_id}/wireless/ssids"),
    }
    
    prompt = f"""
Generate Cisco Meraki Dashboard API payload(s) for this request:

Request: {description}

Current network state:
{json.dumps(current_config, indent=2)}

Return a JSON array of API calls, each with:
- method: "GET" | "PUT" | "POST"
- path: the API path (e.g., /networks/{{network_id}}/appliance/vlans)
- payload: the request body (null for GET)
- description: what this call does

Use the current state to avoid conflicts.
Output ONLY valid JSON array.
"""
    return json.loads(ask_claude(
        prompt,
        system="You are a Cisco Meraki expert. Generate only valid Meraki Dashboard API calls."
    ))

# Example: add a guest network with captive portal
api_calls = generate_meraki_config(
    "N_123456789",
    """
    Add a new Guest WiFi SSID called 'Guest-WiFi':
    - WPA2 with password 'Welcome2024!'  
    - Isolated from corporate traffic (client isolation enabled)
    - Bandwidth limit: 10 Mbps down, 5 Mbps up per client
    - Splash page: click-through with terms of service
    - VLAN 100 for guest traffic
    - Available on all APs
    - Enabled 8am-10pm daily using scheduled availability
    """
)

# Execute the API calls
for call in api_calls:
    print(f"Executing: {call['description']}")
    path = call['path'].replace('{network_id}', 'N_123456789')
    if call['method'] == 'PUT':
        result = meraki_put(path, call['payload'])
    elif call['method'] == 'POST':
        result = meraki_post(path, call['payload'])
    print(f"  ✓ Done")

Pattern 2: Client Connectivity Troubleshooter

When a user says “I can’t connect to WiFi,” this tool diagnoses the issue automatically:

def troubleshoot_client(network_id: str, client_identifier: str) -> dict:
    """
    client_identifier: MAC address, IP address, or username
    """
    # Gather client data from Meraki
    clients = meraki_get(f"/networks/{network_id}/clients", params={
        "timespan": 86400,  # last 24 hours
        "perPage": 100,
    })
    
    # Find matching client
    client = next(
        (c for c in clients if 
         c.get("mac", "").lower() == client_identifier.lower() or
         c.get("ip", "") == client_identifier or
         client_identifier.lower() in c.get("description", "").lower()),
        None
    )
    
    client_data = {"search_term": client_identifier, "found": client is not None}
    
    if client:
        client_id = client["id"]
        client_data.update({
            "profile": client,
            "events": meraki_get(f"/networks/{network_id}/events", params={
                "clientMac": client.get("mac", ""),
                "timespan": 3600,
                "perPage": 50,
            }).get("events", [])[:20],
            "connectivity": client.get("status", "unknown"),
        })
    
    # Get network alerts
    alerts = meraki_get(f"/networks/{network_id}/health/alerts")
    
    prompt = f"""
Troubleshoot WiFi connectivity for client: {client_identifier}

Client data: {json.dumps(client_data, indent=2)}
Network alerts: {json.dumps(alerts[:10], indent=2)}

Return JSON with:
- client_found: true/false
- current_status: description of client's current state
- root_cause: specific reason for connectivity issue (or "No issue found")
- event_timeline: key events from the last hour in chronological order
- is_device_issue: true if problem is isolated to this device
- is_network_issue: true if problem affects multiple clients
- recommended_actions: ordered list of steps to resolve
- escalation_needed: true if issue requires on-site or TAC support

Output ONLY valid JSON.
"""
    return json.loads(ask_claude(prompt))

result = troubleshoot_client("N_123456789", "aa:bb:cc:dd:ee:ff")
print(f"Status: {result['current_status']}")
print(f"Root cause: {result['root_cause']}")
for action in result['recommended_actions']:
    print(f"  → {action}")

Pattern 3: Multi-Site Security Audit

For MSPs or enterprises managing many Meraki networks, this audits all of them at once:

def audit_organization_security(org_id: str) -> list[dict]:
    networks = meraki_get(f"/organizations/{org_id}/networks")
    results = []
    
    for network in networks:
        nid = network["id"]
        
        try:
            # Gather security-relevant config
            network_data = {
                "name": network["name"],
                "firewall_rules": meraki_get(f"/networks/{nid}/appliance/firewall/l3FirewallRules"),
                "content_filtering": meraki_get(f"/networks/{nid}/appliance/contentFiltering"),
                "intrusion_settings": meraki_get(f"/networks/{nid}/appliance/security/intrusion"),
                "malware_settings": meraki_get(f"/networks/{nid}/appliance/security/malware"),
                "ssids": meraki_get(f"/networks/{nid}/wireless/ssids"),
            }
        except Exception:
            continue  # Skip networks without MX/MR
        
        prompt = f"""
Audit this Meraki network's security configuration.

Network: {network['name']}
Config: {json.dumps(network_data, indent=2)}

Return JSON with:
- security_score: 0-100
- critical_issues: list of immediate security risks
- warnings: list of non-critical concerns  
- ssid_issues: list of SSIDs with weak security settings
- firewall_gaps: description of firewall rule weaknesses
- top_3_fixes: most impactful changes to improve security

Output ONLY valid JSON.
"""
        audit = json.loads(ask_claude(prompt))
        audit["network_name"] = network["name"]
        audit["network_id"] = nid
        results.append(audit)
    
    # Sort by score ascending (worst first)
    return sorted(results, key=lambda x: x["security_score"])

org_audits = audit_organization_security("123456")
print(f"Audited {len(org_audits)} networks")
print(f"\nLowest scoring networks:")
for network in org_audits[:5]:
    print(f"  {network['network_name']}: {network['security_score']}/100")
    for issue in network['critical_issues']:
        print(f"    ⚠️  {issue}")

Pattern 4: Bulk Configuration Deployment

Deploy consistent configuration across all networks in an organization:

def deploy_policy_everywhere(org_id: str, policy_description: str, dry_run: bool = True) -> list[dict]:
    networks = meraki_get(f"/organizations/{org_id}/networks")
    deployment_results = []
    
    for network in networks:
        nid = network["id"]
        
        # Generate network-specific payload
        prompt = f"""
Generate Meraki API calls to implement this policy for network '{network['name']}' (ID: {nid}):

Policy: {policy_description}

Return a JSON array of API calls with method, path, and payload.
Make the path specific to network ID {nid}.
Output ONLY valid JSON array.
"""
        try:
            api_calls = json.loads(ask_claude(prompt))
        except Exception as e:
            deployment_results.append({"network": network["name"], "status": "error", "error": str(e)})
            continue
        
        if dry_run:
            deployment_results.append({
                "network": network["name"],
                "status": "dry_run",
                "planned_calls": len(api_calls),
                "calls": api_calls
            })
        else:
            # Execute calls
            for call in api_calls:
                try:
                    if call["method"] == "PUT":
                        meraki_put(call["path"], call["payload"])
                    elif call["method"] == "POST":
                        meraki_post(call["path"], call["payload"])
                except Exception as e:
                    deployment_results.append({"network": network["name"], "status": "failed", "error": str(e)})
                    break
            else:
                deployment_results.append({"network": network["name"], "status": "success"})
    
    return deployment_results

# Dry run first
results = deploy_policy_everywhere(
    "123456",
    "Block all social media (Facebook, Instagram, TikTok, Twitter/X) during business hours 8am-5pm Mon-Fri",
    dry_run=True
)
print(f"Would deploy to {len(results)} networks")

Pattern 5: Natural Language NOC Dashboard

Give your NOC a conversational interface to Meraki:

class MerakiAssistant:
    def __init__(self, org_id: str):
        self.org_id = org_id
        self.history = []
        self.system = f"""You are a Meraki network management assistant for organization {org_id}.
You have access to the Meraki Dashboard API. When asked about network state,
tell the engineer exactly what API call to make and how to interpret the results.
When asked to make changes, generate the exact API payload and confirm before executing.
Be conversational but precise. Always mention the API path used."""

    def ask(self, question: str) -> str:
        self.history.append({"role": "user", "content": question})
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            system=self.system,
            messages=self.history
        ).content[0].text
        self.history.append({"role": "assistant", "content": response})
        return response

# Natural language operations
assistant = MerakiAssistant("123456")

print(assistant.ask("How many clients are currently connected across all our networks?"))
print(assistant.ask("Which network has the most clients right now?"))
print(assistant.ask("Show me any networks that had connectivity issues in the last 24 hours"))
print(assistant.ask("Generate a config to add a new VLAN 200 called 'IoT-Devices' to all MX networks"))

Rate Limiting and Best Practices

The Meraki Dashboard API has rate limits (typically 10 requests/second per org). For bulk operations:

import time

def rate_limited_get(path: str, delay: float = 0.15) -> dict | list:
    """Add small delay between requests to stay under rate limit"""
    time.sleep(delay)
    return meraki_get(path)

Always use dry_run=True the first time you run any bulk operation. The Meraki API makes changes immediately — there’s no staging or commit step like IOS-XR.

// Found this useful? Share it or start a conversation.