API calls for EAP success not returning all entires

mak2018
Getting noticed

API calls for EAP success not returning all entires

We have implemented some automation to log usernames on some of our SSIDs but finding that it doesn't always find and list all the users who have authenticated. 

 

When looking at Meraki's event logs there seems to be different types of EAP success entries and I am wondering if that has something to do with it? We want all Successful authentication (EAP success) which maps to 8021x_eap_success but it seems that isn't the case all the time. 

 

In an attempt to figure out why its not working I got a simple API script, input a 24 hour date range and the username (mapped to identity) and it will go search all of our networks for that identity in the logs.  But it fails to find a specific usernames on specific dates when they have matching event log entries. 

 

In the below screenshot, it will pull the identity from the second entry but not the first one even though they are the same user.  Meaning if the only log entry is the top one on any given day the API does not log/find it.  If the second log exists it will.  

 mak2018_1-1761838394630.png

 

Anyone have an idea why this isn't working?

 

Snippet of the code in question:

# Step 3: Fetch events with pagination
for network in networks:
    network_id = network['id']
    network_name = network['name']
    print(f"\nSearching {network_name}...")

    events_url = f'{BASE_URL}/networks/{network_id}/events'
    params = {
        'productType': 'wireless',
        'includedEventTypes[]': '8021x_eap_success',
        'occurredAfter': start_time.isoformat().replace('+00:00', 'Z'),
        'occurredBefore': end_time.isoformat().replace('+00:00', 'Z'),
        'perPage': 1000
    }

    starting_after = None
    match_count = 0

    while True:
        if starting_after:
            params['startingAfter'] = starting_after

        response = requests.get(events_url, headers=headers, params=params)
        response.raise_for_status()
        data = response.json()
        events = data.get('events', [])

        for event in events:
            occurred_at_str = event.get('occurredAt', '')
            try:
                occurred_at = datetime.fromisoformat(occurred_at_str.replace('Z', '+00:00'))
            except ValueError:
                continue

            if not (start_time <= occurred_at < end_time):
                continue

            client_mac = event.get('clientMac', '')
            identity_raw = event.get('eventData', {}).get('identity', 'N/A')
            normalized_identity = identity_raw.split('@')[0].lower()

            if normalized_identity != normalized_target_identity:
                continue

            matching_events.append([network_name, occurred_at.isoformat(), client_mac, identity_raw])
            match_count += 1

        starting_after = data.get('nextPageToken')
        if not starting_after:
            break

    print(f"  Matches found in {network_name}: {match_count}")

 

11 Replies 11
alemabrahao
Kind of a big deal
Kind of a big deal

Add a debug print for the full event object when identity is missing or doesn't match. You're normalizing the identity by stripping the domain and lowercasing it, but if the event doesn't include a domain or uses a different format (e.g., DOMAIN\username), this could cause mismatches.

 

I am not a Cisco Meraki employee. My suggestions are based on documentation of Meraki best practices and day-to-day experience.

Please, if this post was useful, leave your kudos and mark it as solved.
mak2018
Getting noticed

I don't think that is the issue.  But something appears to be different from the GUI to what the API can return.

 

In an effort to slim this down I added logic to search for a specific username, on a specific network as well as debug to show a match which works, but no match no data.  And that is the problem I cannot get it to match and pull every single EAP success for a 24 hour period for some users and no idea why.  

 

 

Matched Event:
{'occurredAt': '2025-10-28T23:47:45.480374Z', 'networkId': 'L_609111849601866880', 'type': '8021x_eap_success', 'description': 'Successful authentication (EAP success)', 'clientId': 'k1e8554', 'clientDescription': 'devicename', 'clientMac': '56:0a:ab:1c:e1:2f', 'category': '8021x', 'deviceSerial': 'xxxxxxx', 'deviceName': 'ap1', 'ssidNumber': 0, 'ssidName': 'wireless', 'eventData': {'radio': '1', 'vap': '0', 'client_mac': '56:0A:AB:1C:E1:2F', 'identity': 'user1'}}

 

If I have 2 log entries for 2 different users 1 after the other the script finds the first one user1 but not the second user2 when its clear as day there are entries for both.

 

mak2018_0-1761843101377.png

 

:~/meraki-automation$ python3 meraki-eap-v1.py
Enter the date to run (YYYY-MM-DD): 2025-10-28
Enter the identity to search for (e.g., user or user@domain.com): user1
Enter the network name to search in (or part of it): hq

Searching HQ...
  Matches found in HQ: 1

Saved 1 matching events to auth-user1-2025-10-28.csv

:~/meraki-automation$ python3 meraki-eap-v1.py
Enter the date to run (YYYY-MM-DD): 2025-10-28
Enter the identity to search for (e.g., user or user@domain.com): user2
Enter the network name to search in (or part of it): hq

Searching HQ...
  Matches found in HQ: 0

Saved 0 matching events to auth-user2-2025-10-28.csv

 

 

import requests
import csv
from datetime import datetime, timedelta, timezone
import re

API_KEY = 'MY-API-KEY'
BASE_URL = 'https://api.meraki.com/api/v1'

headers = {
    'X-Cisco-Meraki-API-Key': API_KEY,
    'Content-Type': 'application/json'
}

def main():
    # Prompt for date, identity, and network name filter
    date_input = input("Enter the date to run (YYYY-MM-DD): ")
    identity_input = input("Enter the identity to search for (e.g., user or user@domain.com): ")
    network_filter = input("Enter the network name to search in (or part of it): ").lower()

    try:
        target_date = datetime.strptime(date_input, "%Y-%m-%d").date()
    except ValueError:
        print("Invalid date format. Please use YYYY-MM-DD.")
        return

    normalized_target_identity = identity_input.split('@')[0].lower()
    start_time = datetime.combine(target_date, datetime.min.time()).replace(tzinfo=timezone.utc)
    end_time = start_time + timedelta(days=1)

    orgs = requests.get(f'{BASE_URL}/organizations', headers=headers).json()
    org_id = orgs[0]['id']
    networks = requests.get(f'{BASE_URL}/organizations/{org_id}/networks', headers=headers).json()

    # Filter networks based on user input
    filtered_networks = [net for net in networks if network_filter in net['name'].lower()]
    if not filtered_networks:
        print(f"No networks found matching '{network_filter}'.")
        return

    matching_events = []

    for network in filtered_networks:
        network_id = network['id']
        network_name = network['name']
        print(f"\nSearching {network_name}...")

        url = f'{BASE_URL}/networks/{network_id}/events'
        params = {
            'productType': 'wireless',
            'includedEventTypes[]': '8021x_eap_success',
            'occurredAfter': start_time.isoformat().replace('+00:00', 'Z'),
            'occurredBefore': end_time.isoformat().replace('+00:00', 'Z'),
            'perPage': 1000
        }

        match_count = 0
        while url:
            response = requests.get(url, headers=headers, params=params)
            response.raise_for_status()
            data = response.json()
            events = data.get('events', [])

            for event in events:
                occurred_at_str = event.get('occurredAt', '')
                try:
                    occurred_at = datetime.fromisoformat(occurred_at_str.replace('Z', '+00:00'))
                except ValueError:
                    continue
                if not (start_time <= occurred_at < end_time):
                    continue

                client_mac = event.get('clientMac', '')
                identity_raw = event.get('eventData', {}).get('identity', 'N/A')
                normalized_identity = identity_raw.split('@')[0].lower()

                if normalized_identity != normalized_target_identity:
                    continue

                matching_events.append([network_name, occurred_at.isoformat(), client_mac, identity_raw])
                match_count += 1

            # Check for rel="next" in Link header
            link_header = response.headers.get('Link', '')
            match = re.search(r'<([^>]+)>;\s*rel="next"', link_header)
            url = match.group(1) if match else None
            params = None  # Clear params for next URL

        print(f"  Matches found in {network_name}: {match_count}")

    filename = f"Meraki-auth-{normalized_target_identity}-{target_date.isoformat()}.csv"
    with open(filename, 'w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerow(['Network Name', 'Occurred At', 'Client MAC', 'Identity'])
        writer.writerows(matching_events)

    print(f"\nSaved {len(matching_events)} matching events to {filename}")

if __name__ == '__main__':
    main()

 

alemabrahao
Kind of a big deal
Kind of a big deal

Try printing the repr(identity_raw) to see the exact string, including hidden characters. To isolate the issue from your script, try querying the API directly using curl or Postman for the same network and date range. This can help confirm whether the API is returning the event for user at all.

I am not a Cisco Meraki employee. My suggestions are based on documentation of Meraki best practices and day-to-day experience.

Please, if this post was useful, leave your kudos and mark it as solved.
mak2018
Getting noticed

I would if I could get the syntax right...I can't even get the CURL command to run successfully (in windows) without specifying a date range or specific event type.  Runs fine in Linux though. 

 

Can you help me with the right syntax for date/time?  

 

curl -L -H 'X-Cisco-Meraki-API-Key: <meAPIKEY>' -X GET -H 'Content-Type: application/json' 'https://api.meraki.com/api/v1/networks/L_609111849601866880/events?productType=wireless'

curl: (6) Could not resolve host: application
curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535

 

alemabrahao
Kind of a big deal
Kind of a big deal

Try this.

 

https://api.meraki.com/api/v1/networks/L_609111849601866880/events?productType=wireless&occurredAfte...

I am not a Cisco Meraki employee. My suggestions are based on documentation of Meraki best practices and day-to-day experience.

Please, if this post was useful, leave your kudos and mark it as solved.
mak2018
Getting noticed

Yeah that worked, but returned only 6 or 7 random results and only from 10/30, nothing between 27th and the 28th so not sure its interpreting the date range correctly?  None of the events returned were for 8021x_eap_success either.

 

What would be the correct syntax for exact date and time and only 8021x_eap_success?  IE 10/28/25 16:08:13 PDT. Starting to wonder if I need to search for event type description?  Meaning Successful authentication (EAP success) vs the event type?

 

I really just want to see what querying for this exact log entry will return:

 

mak2018_0-1761850437959.png

 

 

alemabrahao
Kind of a big deal
Kind of a big deal

I'm not sure, but you could try this syntax.

 

https://api.meraki.com/api/v1/networks/L_609111849601866880/events?productType=wireless&includedEven...

 

I haven't had time to test it.

I am not a Cisco Meraki employee. My suggestions are based on documentation of Meraki best practices and day-to-day experience.

Please, if this post was useful, leave your kudos and mark it as solved.
mak2018
Getting noticed

It works and returns just those event types, but again only 5 or 6 of them and all from today.  I feel like something is missing or broken with their API that is simply not allowing this to work right.

mak2018
Getting noticed

So this is the correct syntax for a specific data/time range.  You have to use startingAfter and startingBefore vs occurred.  But even then it doesn't return that specific event log and Meraki  support has gone back to their engineering team to figure out why.  

 

 

https://api.meraki.com/api/v1/networks/L_609111849601866880/events?productType=wireless&includedEventTypes[]=8021x_eap_success&startingAfter=2025-10-28T23:08:00Z&startingBefore=2025-10-28T23:13:00Z

 

 

PhilipDAth
Kind of a big deal
Kind of a big deal

>We have implemented some automation to log usernames on some of our SSIDs but finding that it doesn't always find and list all the users who have authenticated. 

 

I don't think this approach will work reliably.  The reason for this is that the RADIUS supplicant (Meraki) can only see a username in the outer header, and a client doesn't even need to fill this in.  For example, if you are using PEAP, the actual authentication is done in an encrypted tunnel, and Meraki will never see the username—unless the client voluntarily adds it to the outer, unencrypted header.

 

You should do this at the RADIUS server level.

mak2018
Getting noticed

It works for a majority of the time.  Meaning if a user logs into one of our SSIDs via RADIUS/EAP successfully it generates an event log in Meraki.  We have other sources of truth that we use to correlate that data and it works, but less than ideal.   I just want to make sure that if that specific event log is generated than our Meraki API calls will return it correctly and that simply doesn't always happen and that is why I am here. 

 

Does windows NPS support API calls? 

Get notified when there are additional replies to this discussion.