Test your knowledge on this topic in the ENAUTO Exam Trainer — 186 questions across 5 interactive modes.
Cisco Meraki – Deep Dive for ENAUTO v2.0
Exam Relevance
Topic 3.0 Controller-Based Network Automation (30%) — Meraki is the cloud-managed controller.
Unlike Catalyst Center, Meraki operations are synchronous and the API lives entirely in the cloud.
Meraki enforces 5 API calls per second per organization. Exceeding this returns 429 Too Many Requests with a Retry-After header. The SDK handles retries automatically; with requests you must implement backoff yourself.
2 – Authentication with Python requests
Exam Topic: 3.2, 3.6
Meraki uses the simplest auth model among Cisco controllers — a single API key in every request.
How It Works
Generate an API key once from Meraki Dashboard → My Profile → API access.
Include it as the X-Cisco-Meraki-API-Key header in every request.
No token exchange. No session cookies. No expiry (unless manually regenerated).
Full Working Example
import requestsimport os# ── Best practice: store API key as environment variable ──────# export MERAKI_DASHBOARD_API_KEY="your_key_here"API_KEY = os.environ.get("MERAKI_DASHBOARD_API_KEY")BASE_URL = "https://api.meraki.com/api/v1"HEADERS = { "X-Cisco-Meraki-API-Key": API_KEY, "Content-Type": "application/json", "Accept": "application/json"}# ── Test: Get your organizations ──────────────────────────────response = requests.get(f"{BASE_URL}/organizations", headers=HEADERS)response.raise_for_status()for org in response.json(): print(f"Org: {org['name']} (ID: {org['id']})")
Exam Trap: Header Name
The header is X-Cisco-Meraki-API-Key — note the dashes and capitalization.
Common wrong answers on the exam:
Authorization: Bearer <key> — wrong scheme
X-Meraki-API-Key — missing “Cisco”
X-Auth-Token — that’s Catalyst Center
What Happens on Failure?
Status Code
Meaning
Likely Cause
401 Unauthorized
Invalid API key
Wrong or revoked key
403 Forbidden
Key valid, no org access
Key lacks permissions for this org
404 Not Found
Resource doesn’t exist
Wrong org/network/device ID
429 Too Many Requests
Rate limited
Exceeded 5 calls/sec
Rate Limit Handling with requests
import timedef meraki_get(url, headers, max_retries=3): """GET with automatic 429 retry backoff.""" for attempt in range(max_retries): response = requests.get(url, headers=headers) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 1)) print(f"Rate limited. Retrying in {retry_after}s...") time.sleep(retry_after) continue response.raise_for_status() return response.json() raise Exception("Max retries exceeded due to rate limiting")
3 – Authentication with Meraki SDK
Exam Topic: 3.2
Installation
pip install meraki
Full Working Example
import merakiimport osAPI_KEY = os.environ.get("MERAKI_DASHBOARD_API_KEY")# The SDK handles:# - API key header injection# - Rate limit retries (automatic backoff)# - Pagination (returns all results)# - Logging (optional)dashboard = meraki.DashboardAPI( api_key=API_KEY, suppress_logging=True, # Quiet mode maximum_retries=3, # Auto-retry on 429 wait_on_rate_limit=True # Pause instead of failing on 429)# ── Test authentication ───────────────────────────────────────orgs = dashboard.organizations.getOrganizations()for org in orgs: print(f"Org: {org['name']} (ID: {org['id']})")
SDK vs requests — When to Use What
Aspect
requests
meraki SDK
Rate limiting
Manual (handle 429)
Automatic backoff
Pagination
Manual (follow Link headers)
Returns all pages
Method naming
HTTP verbs + URLs
dashboard.category.methodName()
API key handling
Manual header in every call
Set once in constructor
Exam relevance
Must understand raw calls
Must know SDK constructor
4 – The Meraki Object Hierarchy
Understanding this hierarchy is critical — every API call requires IDs from the level above.
Meraki networks have 15 SSID slots (0–14). You don’t “create” an SSID — you configure an existing slot. A slot with enabled: false and no name is effectively unused.
# requests — send only the fields you want to changerequests.put( f"{BASE_URL}/networks/{NETWORK_ID}/wireless/ssids/{SSID_NUMBER}", headers=HEADERS, json={"enabled": False})# SDKdashboard.wireless.updateNetworkWirelessSsid( networkId=NETWORK_ID, number=SSID_NUMBER, enabled=False)
PUT, Not PATCH
Meraki SSID updates use PUT, but they behave like a partial update — you only need to send the fields you want to change. This is unusual and a potential exam trick.
7 – Day-0 Provisioning – Device Claiming
Exam Topic: 3.1 – Construct a controller-based network automation solution for Day-0 provisioning
Meraki Day-0: Claim and Go
Meraki’s Day-0 is simpler than Catalyst Center’s PnP because devices auto-connect to the cloud. The provisioning steps are:
Claim the device into a network using its serial number
Device powers on → connects to Meraki cloud → downloads config
Optionally set device name, address, tags
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Python API │────►│ Meraki Cloud │────►│ New Device │
│ Claim serial│ │ Assign to │ │ Powers on │
│ to network │ │ network │ │ Gets config │
└──────────────┘ └──────────────┘ └──────────────┘
Step 1 Step 2 Step 3
(your code) (automatic) (automatic)
Claim a Device into a Network (requests)
NETWORK_ID = "L_123456789012345678"# ── Claim one or more devices by serial number ───────────────claim_payload = { "serials": ["Q2HP-AJ22-UG72", "Q2HP-BK33-VH83"]}response = requests.post( f"{BASE_URL}/networks/{NETWORK_ID}/devices/claim", headers=HEADERS, json=claim_payload)response.raise_for_status()print("Devices claimed successfully")
# ── Update device name and location after claiming ────────────device_payload = { "name": "Floor-2-AP-01", "address": "123 Main Street, Building A", "tags": [" floor2 ", " ap "], "notes": "Provisioned via automation"}# requestsrequests.put( f"{BASE_URL}/devices/Q2HP-AJ22-UG72", headers=HEADERS, json=device_payload)# SDKdashboard.devices.updateDevice( serial="Q2HP-AJ22-UG72", name="Floor-2-AP-01", address="123 Main Street, Building A", tags=[" floor2 ", " ap "], notes="Provisioned via automation")
Bulk Provisioning Pattern
import csvdef bulk_provision_from_csv(dashboard, network_id, csv_path): """Claim and configure devices from a CSV inventory file.""" with open(csv_path, "r") as f: reader = csv.DictReader(f) devices = list(reader) # Step 1: Claim all serials at once (more efficient) serials = [d["serial"] for d in devices] dashboard.networks.claimNetworkDevices( networkId=network_id, serials=serials ) print(f"Claimed {len(serials)} devices") # Step 2: Configure each device individually for device in devices: dashboard.devices.updateDevice( serial=device["serial"], name=device["name"], address=device.get("address", ""), tags=device.get("tags", "").split(",") ) print(f" Configured: {device['name']} ({device['serial']})")
Example CSV (inventory.csv):
serial,name,address,tagsQ2HP-AJ22-UG72,Floor-2-AP-01,123 Main St, floor2 apQ2HP-BK33-VH83,Floor-3-AP-01,123 Main St, floor3 ap
Meraki Day-0 vs Catalyst Center Day-0
Aspect
Meraki
Catalyst Center PnP
Connection
Device → Cloud (automatic)
Device → DHCP → Controller
Claim method
Serial-based claiming
PnP device + claim + project
Config push
Network-level settings apply
Template-based per device
Complexity
Low — claim and go
Higher — DHCP, templates, projects
Provisioning time
Minutes (cloud)
Minutes to hours (on-prem)
8 – Configuration Templates and Action Batches
Exam Topics: 3.3, 3.2
Meraki’s approach to templates differs from Catalyst Center’s Jinja2 templates, but both serve the same purpose: consistent configuration at scale.
Meraki Configuration Templates
Configuration templates in Meraki are network blueprints that you create in the org and then bind to networks. Bound networks inherit the template settings.
ORG_ID = "549236"# ── List existing config templates ────────────────────────────templates = requests.get( f"{BASE_URL}/organizations/{ORG_ID}/configTemplates", headers=HEADERS).json()for t in templates: print(f"Template: {t['name']} (ID: {t['id']})") print(f" Product types: {t['productTypes']}")# SDKtemplates = dashboard.organizations.getOrganizationConfigTemplates( organizationId=ORG_ID)
ORG_ID = "549236"# ── Get status of all devices in the org ──────────────────────statuses = requests.get( f"{BASE_URL}/organizations/{ORG_ID}/devices/statuses", headers=HEADERS).json()# Count by statusfrom collections import Counterstatus_counts = Counter(d["status"] for d in statuses)print(f"Online: {status_counts.get('online', 0)}")print(f"Offline: {status_counts.get('offline', 0)}")print(f"Alerting: {status_counts.get('alerting', 0)}")# SDKstatuses = dashboard.organizations.getOrganizationDevicesStatuses( organizationId=ORG_ID)
Network Client Health
NETWORK_ID = "L_123456789012345678"# ── Get clients on the network ────────────────────────────────clients = requests.get( f"{BASE_URL}/networks/{NETWORK_ID}/clients", headers=HEADERS, params={"timespan": 86400} # Last 24 hours (in seconds)).json()print(f"Total clients in last 24h: {len(clients)}")for client in clients[:5]: print(f" {client.get('description', 'unknown'):<20} " f"{client['mac']:<18} " f"VLAN: {client.get('vlan', 'N/A')}")# SDKclients = dashboard.networks.getNetworkClients( networkId=NETWORK_ID, timespan=86400)
Device Uplink and Connection Health
# ── Get uplink status for all MX appliances ──────────────────uplinks = requests.get( f"{BASE_URL}/organizations/{ORG_ID}/appliance/uplink/statuses", headers=HEADERS).json()for device in uplinks: print(f"\nDevice: {device['serial']}") for uplink in device.get("uplinks", []): print(f" {uplink['interface']}: {uplink['status']} " f"(IP: {uplink.get('publicIp', 'N/A')})")# SDKuplinks = dashboard.appliance.getOrganizationApplianceUplinkStatuses( organizationId=ORG_ID)
Connection Stats per AP
# ── Wireless connection stats (useful for health dashboards) ─stats = requests.get( f"{BASE_URL}/networks/{NETWORK_ID}/wireless/connectionStats", headers=HEADERS, params={"timespan": 3600} # Last hour).json()print(f"Success: {stats.get('success', 0)}")print(f"Auth failures: {stats.get('authFailure', 0)}")print(f"DHCP failures: {stats.get('dhcpFailure', 0)}")print(f"DNS failures: {stats.get('dnsFailure', 0)}")
10 – Webhook-Based Monitoring
Exam Topic: 4.6 – Implement webhook-based monitoring using controllers
Instead of polling, Meraki can push alerts to your server.
Always validate the sharedSecret in your webhook receiver to ensure the request actually came from Meraki. This is a security best practice the exam may test under topic 3.5.
import merakiimport osAPI_KEY = os.environ.get("MERAKI_DASHBOARD_API_KEY")dashboard = meraki.DashboardAPI(api_key=API_KEY, suppress_logging=True)# ── 1. Discover the hierarchy ────────────────────────────────orgs = dashboard.organizations.getOrganizations()org_id = orgs[0]["id"]print(f"Org: {orgs[0]['name']}")networks = dashboard.organizations.getOrganizationNetworks( organizationId=org_id)# Find a wireless networkwireless_net = next( (n for n in networks if "wireless" in n["productTypes"]), None)if not wireless_net: print("No wireless network found!") exit()net_id = wireless_net["id"]print(f"Network: {wireless_net['name']}")# ── 2. Get current SSID config ───────────────────────────────ssids = dashboard.wireless.getNetworkWirelessSsids(networkId=net_id)print(f"\nCurrent SSIDs:")for ssid in ssids: status = "ENABLED" if ssid["enabled"] else "disabled" print(f" [{ssid['number']}] {ssid['name']:<25} {status}")# ── 3. Configure SSID slot 3 ────────────────────────────────dashboard.wireless.updateNetworkWirelessSsid( networkId=net_id, number=3, name="Automated-Guest", enabled=True, authMode="open", splashPage="Click-through splash page")print(f"\nConfigured SSID 3 as 'Automated-Guest'")# ── 4. Verify ────────────────────────────────────────────────updated = dashboard.wireless.getNetworkWirelessSsid( networkId=net_id, number=3)print(f"Verified: {updated['name']} (enabled={updated['enabled']})")
Gotchas for the Exam
Top Meraki Exam Pitfalls
1. SSIDs are slots (0–14), not created dynamically
You PUT to configure a slot. There’s no POST to create an SSID.
2. SSID config uses PUT, not POSTPUT /networks/{id}/wireless/ssids/{number} — the exam loves testing HTTP methods.
3. Rate limit = 5 calls/sec per organization
Not per user, not per network — per organization. Multiple scripts hitting the same org stack up.
4. Pagination uses Link headers, not offset/limit
Unlike Catalyst Center, Meraki pagination follows RFC 5988. The SDK handles this automatically.
5. Device identifier = serial number, not UUID
Catalyst Center uses UUIDs. Meraki uses serial numbers like Q2HP-AJ22-UG72.
6. API base URL is always api.meraki.com
Never changes. No on-prem option. If a question mentions “your Meraki server”, it’s a trap.
7. All Meraki write operations are synchronous
Unlike Catalyst Center, there’s no task polling. When the response comes back, the change is applied (or failed).
12 – Exam-Style Scenarios
Scenario 1: Rate Limiting (Topic 3.6)
Your script loops through 500 networks to check SSID settings. After ~50 iterations, you get 429 Too Many Requests. What should you do?
for network in all_networks: ssids = requests.get( f"{BASE_URL}/networks/{network['id']}/wireless/ssids", headers=HEADERS ).json()
Answer
Meraki enforces 5 requests/second per org. Solutions:
Add time.sleep(0.2) between calls to stay under the limit
Check the Retry-After header on 429 responses and wait
Switch to the SDK with wait_on_rate_limit=True (recommended)
Use action batches for bulk write operations
Scenario 2: Wrong HTTP Method for SSID (Topic 3.6)
You try to create a new SSID and get 405 Method Not Allowed. What's wrong?