Join us at FabCon Atlanta from March 16 - 20, 2026, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.
Register now!To celebrate FabCon Vienna, we are offering 50% off select exams. Ends October 3rd. Request your discount now.
I'm trying to pull discovery data (e.g. which users accessed which apps, like ChatGPT) using the API. I generate a valid access token using client credentials (Azure AD App Registration), and I tried both:
So my first question is:
Which endpoint and method actually returns usable Defender for Cloud Apps data?
Once I get access working, I’d like to:
My second set of questions:
Any working examples, architectures, or documentation would be really appreciated. Thanks so much!
Best,
Gülce
Solved! Go to Solution.
Hi @gulce,
Thank you for reaching out to Microsoft Fabric Community.
Thank you @johnbasha33 for the prompt response.
The API is not returning all the apps or users you see in the defender because,
Please follow below steps:
This will return all records from all reports, matching what you see in the portal.
Thanks and regards,
Anjan Kumar Chippa
Hi @gulce
Python Example: Defender for Cloud Apps (MCAS) API
import requests
import json
import csv
# === Azure AD App Registration ===
tenant_id = "<your-tenant-id>"
client_id = "<your-client-id>"
client_secret = "<your-client-secret>"
mcas_domain = "<yourtenant>" # without .onmicrosoft.com or any suffix
# === Token Endpoint for OAuth2 ===
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/token"
# === Get OAuth Token for MCAS API ===
token_data = {
'grant_type': 'client_credentials',
'client_id': client_id,
'client_secret': client_secret,
'resource': f'https://{mcas_domain}.portal.cloudappsecurity.com'
}
response = requests.post(token_url, data=token_data)
access_token = response.json().get('access_token')
if not access_token:
print("Failed to get token:", response.text)
exit()
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
# === Discovery API Endpoint ===
api_url = f"https://{mcas_domain}.portal.cloudappsecurity.com/api/discovery_users/"
all_data = []
while api_url:
print(f"Fetching page: {api_url}")
resp = requests.get(api_url, headers=headers)
if resp.status_code != 200:
print("Failed to fetch data:", resp.text)
break
data = resp.json()
all_data.extend(data.get('data', []))
api_url = data.get('nextLink') # MCAS pagination
# === Save to CSV ===
csv_file = 'discovery_users.csv'
if all_data:
keys = all_data[0].keys()
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
dict_writer = csv.DictWriter(f, fieldnames=keys)
dict_writer.writeheader()
dict_writer.writerows(all_data)
print(f"Saved {len(all_data)} records to {csv_file}")
else:
print("No data received.")
Directory.Read.All
(under Microsoft Graph)
Grant MCAS API access permissions in the Defender for Cloud Apps portal
Admin consent is required
Did I answer your question? Mark my post as a solution! Appreciate your Kudos !!
Thank you for your reply.
I’m able to pull data via the API, but I noticed that not all records are returned.
For example, in the Generative AI category, I see 127 records on the portal, but the API only returns 8–9 records.
Also, not all users are returned either.
Do I need to implement pagination?
If so, how exactly should it be done?
Here are the URLs I used:
https://{mcas_domain}.portal.cloudappsecurity.com/api/v1/discovery/discovered_apps/
https://{mcas_domain}.portal.cloudappsecurity.com/api/v1/discovery/users/
And here is the script I used:
import requests
import pandas as pd
# 🔐 Token and domain info
access_token = "....."
mcas_domain = "<......>"
headers = {
'Authorization': f'Token {access_token}',
'Content-Type': 'application/json'
}
# 🔄 API URL: discovered_apps
api_url = f"https://{mcas_domain}.portal.cloudappsecurity.com/api/v1/discovery/discovered_apps/"
all_data = []
while api_url:
resp = requests.get(api_url, headers=headers)
print("Status code:", resp.status_code)
if resp.status_code != 200:
print("Error:", resp.text)
break
data = resp.json()
print("Number of records on page:", len(data.get("data", [])))
all_data.extend(data.get("data", []))
api_url = data.get("nextLink")
df = pd.json_normalize(all_data)
# 💾 Save to CSV
df.to_csv("discovered_apps.csv", index=False)
print("✅ discovered_apps.csv file created.")
Hi @gulce,
Thank you for reaching out to Microsoft Fabric Community.
Thank you @johnbasha33 for the prompt response.
The API is not returning all the apps or users you see in the defender because,
Please follow below steps:
This will return all records from all reports, matching what you see in the portal.
Thanks and regards,
Anjan Kumar Chippa
hi @v-achippa
Thanks. I currently call /api/v1/discovery/discovered_apps to collect the appIds, then use those appIds together with a single streamId to call /api/v1/discovery/app_users for each appId. However, I only ever get a maximum of ~101 rows.
Based on your guidance, here’s the plan I will follow:
I’d like to confirm two details:
Thanks again for the guidance
Hi @gulce,
Yes, you should pass the nextQueryFilters object back in the request body under filters exactly as JSON, do not wrap it in the quotes.
And the 100/101 rows per page is a fixed limit and it cannot be increased, so pagination is required to get all results.
Thanks and regards,
Anjan Kumar Chippa
Hi @v-achippa
Testing Cloud Discovery – app_users API with Postman
Endpoint
POST https://<TENANT>.portal.cloudappsecurity.com/api/v1/discovery/app_users/
Headers
Authorization: Token <ACCESS_TOKEN> Content-Type: application/json
Request body I used
Works either flat or wrapped in filters. The wrapped form is the “official” one.
{"filters": {"streamId": "......................","appId": 11394,"timeframe": 90},"sortField": "trafficTotalBytes","sortDirection": "desc" }
{"streamId": "......................","appId": 11394,"timeframe": 90,"sortField": "trafficTotalBytes","sortDirection": "desc" }
Response (summary)
{"total": 101,"hasNext": false,"data": [ ...users... ] }
Have you personally managed to fetch page 2+ from POST /api/v1/discovery/app_users? When the response returns hasNext: true with a nextQueryFilters object, do you paste that object verbatim inside filters for the next request (i.e., { "filters": <nextQueryFilters> }), rather than sending a top-level nextQueryFilters (which throws “Invalid filter params”)? If yes, could you share a minimal working body (redact IDs) and whether nextQueryFilters contained skip/page or a different token? Also, is the page size fixed (~101) or can it be increased?
Hi @gulce,
Yes when hasNext: true is returned, you need to take the nextQueryFilters object from the response and paste it in the verbatim inside filters for the next request, like for example
{
"filters": { ...nextQueryFilters... },
"streamId": "<streamId>",
"appId": <appId>,
"limit": 100
}
Do not send the nextQueryFilters as a top level field because that will fail.
And the page size is fixed at 100/101, it cannot be increased. The only way to get all results is to keep looping with hasNext or nextQueryFilters until there are no more pages.
Thanks and regards,
Anjan Kumar Chippa
Hi @gulce,
As we haven’t heard back from you, we wanted to kindly follow up to check if the solution I have provided for the issue worked? or let us know if you need any further assistance.
Thanks and regards,
Anjan Kumar Chippa
Hi @gulce,
We wanted to kindly follow up to check if the solution I have provided for the issue worked? or let us know if you need any further assistance.
Thanks and regards,
Anjan Kumar Chippa
Hi @gulce,
As we haven’t heard back from you, we wanted to kindly follow up to check if the solution I have provided for the issue worked? or let us know if you need any further assistance.
Thanks and regards,
Anjan Kumar Chippa