curl --request GET \
--url https://api.petrasecurity.com/v1/posture-reports \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.petrasecurity.com/v1/posture-reports"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.petrasecurity.com/v1/posture-reports', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.petrasecurity.com/v1/posture-reports",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.petrasecurity.com/v1/posture-reports"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.petrasecurity.com/v1/posture-reports")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.petrasecurity.com/v1/posture-reports")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"totalReports": 1,
"reports": [
{
"id": "7c1a4a1e-3b2f-4a55-9d7e-1f0c2b3d4e5f",
"url": "https://app.petrasecurity.com/reporting/posture/editor?scanId=7c1a4a1e-3b2f-4a55-9d7e-1f0c2b3d4e5f",
"tenant": {
"petraTenantId": "ZZk90t",
"name": "Acme Corp.",
"microsoftTenantId": "0f3b9a4e-6c7d-4e8f-9a0b-1c2d3e4f5a6b"
},
"reportType": "FOLLOW_UP",
"baseline": {
"id": "2d9e8f7a-6b5c-4d3e-8f2a-1b0c9d8e7f6a",
"score": 58,
"completedAt": "2026-05-02T14:10:22.000Z"
},
"createdAt": "2026-08-01T10:00:00.000Z",
"completedAt": "2026-08-01T10:04:37.000Z",
"activityLookbackDays": 90,
"score": 81,
"grade": "B",
"previousScore": 58,
"previousGrade": "D",
"controls": {
"assessed": 27,
"secure": 17,
"fixed": 5,
"inProgress": 1,
"open": 4,
"drifted": 0,
"notAssessed": 2
},
"globalAdmins": {
"count": 3,
"previousCount": 6,
"bestPractice": "4 or fewer",
"strongMfaCount": 3,
"dataComplete": true,
"admins": [
{
"userPrincipalName": "admin@acmecorp.com",
"status": "secure",
"mfaEnforced": "Conditional Access",
"mfaStrength": "Authenticator app",
"mfaStrengthStrong": true,
"passwordAge": "41 days",
"passwordAgeWarning": false,
"failedSignIns": 0
}
]
},
"topRisks": [
{
"controlId": "blockLegacyAuth",
"name": "Block legacy sign-in protocols that bypass MFA",
"status": "open",
"severity": "critical"
}
],
"checks": [
{
"controlId": "blockLegacyAuth",
"category": "conditionalAccess",
"name": "Block legacy sign-in protocols that bypass MFA",
"status": "open",
"severity": "critical",
"currentState": "No Conditional Access policy blocks legacy authentication.",
"previousState": null,
"why": "Legacy protocols skip MFA entirely",
"suggestedFix": "Block legacy authentication tenant-wide",
"observedUsage": "90d: 0 legacy sign-ins. Fixing breaks nothing."
},
{
"controlId": "disableSmtpAuth",
"category": "exchange",
"name": "Disable SMTP AUTH tenant-wide",
"status": "fixed",
"severity": "high",
"currentState": "SMTP AUTH is disabled tenant-wide.",
"previousState": "Enabled",
"why": "SMTP AUTH bypasses modern authentication",
"suggestedFix": "Disabled",
"observedUsage": null
},
{
"controlId": "globalAdmins",
"category": "globalAdmins",
"name": "Limit the number of Global Administrators",
"status": "secure",
"severity": "high",
"currentState": "3 Global Administrators.",
"previousState": "6",
"why": "Each admin is a high value target",
"suggestedFix": "Limit to 4 or fewer Global Administrators",
"observedUsage": null
}
]
}
]
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}Get Posture Reports
Retrieve completed Microsoft 365 security posture reports for the tenants in your organization. By default this returns the most recent completed report for each tenant, newest first. Each report includes the score and grade, a rollup of control results, the Global Administrators review, the top risks, and every individual check with its status, severity, current state, and suggested fix. Results reflect any edits made to the report in the Petra dashboard, so they match the PDF your clients see.
curl --request GET \
--url https://api.petrasecurity.com/v1/posture-reports \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.petrasecurity.com/v1/posture-reports"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.petrasecurity.com/v1/posture-reports', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.petrasecurity.com/v1/posture-reports",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.petrasecurity.com/v1/posture-reports"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.petrasecurity.com/v1/posture-reports")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.petrasecurity.com/v1/posture-reports")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"totalReports": 1,
"reports": [
{
"id": "7c1a4a1e-3b2f-4a55-9d7e-1f0c2b3d4e5f",
"url": "https://app.petrasecurity.com/reporting/posture/editor?scanId=7c1a4a1e-3b2f-4a55-9d7e-1f0c2b3d4e5f",
"tenant": {
"petraTenantId": "ZZk90t",
"name": "Acme Corp.",
"microsoftTenantId": "0f3b9a4e-6c7d-4e8f-9a0b-1c2d3e4f5a6b"
},
"reportType": "FOLLOW_UP",
"baseline": {
"id": "2d9e8f7a-6b5c-4d3e-8f2a-1b0c9d8e7f6a",
"score": 58,
"completedAt": "2026-05-02T14:10:22.000Z"
},
"createdAt": "2026-08-01T10:00:00.000Z",
"completedAt": "2026-08-01T10:04:37.000Z",
"activityLookbackDays": 90,
"score": 81,
"grade": "B",
"previousScore": 58,
"previousGrade": "D",
"controls": {
"assessed": 27,
"secure": 17,
"fixed": 5,
"inProgress": 1,
"open": 4,
"drifted": 0,
"notAssessed": 2
},
"globalAdmins": {
"count": 3,
"previousCount": 6,
"bestPractice": "4 or fewer",
"strongMfaCount": 3,
"dataComplete": true,
"admins": [
{
"userPrincipalName": "admin@acmecorp.com",
"status": "secure",
"mfaEnforced": "Conditional Access",
"mfaStrength": "Authenticator app",
"mfaStrengthStrong": true,
"passwordAge": "41 days",
"passwordAgeWarning": false,
"failedSignIns": 0
}
]
},
"topRisks": [
{
"controlId": "blockLegacyAuth",
"name": "Block legacy sign-in protocols that bypass MFA",
"status": "open",
"severity": "critical"
}
],
"checks": [
{
"controlId": "blockLegacyAuth",
"category": "conditionalAccess",
"name": "Block legacy sign-in protocols that bypass MFA",
"status": "open",
"severity": "critical",
"currentState": "No Conditional Access policy blocks legacy authentication.",
"previousState": null,
"why": "Legacy protocols skip MFA entirely",
"suggestedFix": "Block legacy authentication tenant-wide",
"observedUsage": "90d: 0 legacy sign-ins. Fixing breaks nothing."
},
{
"controlId": "disableSmtpAuth",
"category": "exchange",
"name": "Disable SMTP AUTH tenant-wide",
"status": "fixed",
"severity": "high",
"currentState": "SMTP AUTH is disabled tenant-wide.",
"previousState": "Enabled",
"why": "SMTP AUTH bypasses modern authentication",
"suggestedFix": "Disabled",
"observedUsage": null
},
{
"controlId": "globalAdmins",
"category": "globalAdmins",
"name": "Limit the number of Global Administrators",
"status": "secure",
"severity": "high",
"currentState": "3 Global Administrators.",
"previousState": "6",
"why": "Each admin is a high value target",
"suggestedFix": "Limit to 4 or fewer Global Administrators",
"observedUsage": null
}
]
}
]
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"details": "The provided API key is invalid or has been revoked"
}
}latestOnly=false to see report history.
Each report includes every assessed control under checks. Values reflect any edits made to the report in the dashboard, so the API matches the PDF your clients see.
You can find a tenant’s Petra ID in the URL when viewing a tenant in the dashboard (e.g. app.petrasecurity.com/tenant/<tenantId>). You can also pass a Microsoft tenant ID and the endpoint will resolve it automatically.Authorizations
Bearer token authentication. Include your API key in the Authorization header as 'Bearer YOUR_API_KEY'
Query Parameters
Filter to a specific tenant. Accepts either a Petra tenant ID or a Microsoft tenant ID. You can find a tenant's Petra ID in the URL when viewing a tenant in the dashboard (e.g. app.petrasecurity.com/tenant/). Returns 404 if the tenant is not in your organization.
When true (the default), return only the most recent completed report per tenant. Set to false to return report history, newest first.
Only return reports completed on or after this date. Accepts ISO 8601 format, either date only (e.g. 2026-03-01) or full date-time (e.g. 2026-03-05T00:53:10.402Z).
Maximum number of reports to return. Defaults to 100, maximum 500.
1 <= x <= 500