Webhook Receiver
What a custom HTTPS receiver needs to accept, verify and parse deliveries from a webhook destination with the Generic (signed JSON) format. Slack and Teams bodies are platform message formats, not a contract.
Sections
Contract rules
- The body contract is
schemaVersion1. Later versions may add fields, but existing fields won't be renamed or removed without a newschemaVersion. Ignore fields you don't recognize. - Header names always start with
X-StratoLens-, even when the product is white-labeled. - Verify
X-StratoLens-Signatureon every request and reject any that fail. The endpoint URL alone doesn't prove a request came from StratoLens.
Request
| Property | Value |
|---|---|
| Method | POST to the destination URL |
| Content-Type | application/json |
| When | After a scan, once per notification rule that triggered and lists this destination; and on tests |
| Success | Any 2xx status. The response body is ignored. |
| Timeout | 10 seconds per attempt |
| Retries | Up to 3 attempts on network error, timeout or 5xx, waiting 1 s then 4 s. 4xx and redirects are not retried; redirects are not followed. |
| Body size | At most 256 KB |
Acknowledge fast
Respond with 2xx as soon as you have queued the work. Slow receivers hit the 10-second timeout and get retried.
Rejections count toward auto-disable
Ten failed deliveries in a row auto-disable the destination, including deliveries your receiver rejects with 4xx.
Headers
| Header | Value | Notes |
|---|---|---|
X-StratoLens-Event | notification_alert or test | test for destination tests and rule test sends. |
X-StratoLens-Delivery | UUID | One per delivery, the same across its retries. Use it to drop duplicates. |
X-StratoLens-Timestamp | Unix time in seconds | Set fresh on every attempt. |
X-StratoLens-Signature | sha256=<hex> | HMAC-SHA256 of {timestamp}.{raw body}, keyed with the destination's signing secret. Recomputed on every attempt. |
| Custom header | As configured | Only when the destination has a custom request header. |
Verifying the Signature
- Read the raw request body as bytes, before any JSON parsing.
- Read
X-StratoLens-Timestampand build the string{timestamp}.{raw body}: the timestamp, a dot, then the body. - Compute HMAC-SHA256 over that string. Use the signing secret exactly as shown, a 64-character hex string, as the UTF-8 key. Don't hex-decode it.
- Hex-encode the result in lowercase, prefix it with
sha256=, and compare it withX-StratoLens-Signatureusing a constant-time comparison. - Optionally, reject requests whose timestamp is too far from your clock, to limit replays. StratoLens doesn't set a tolerance; choose your own, allowing for the retry delays.
The secret is shown once when you create the destination and once each time you click Rotate secret. StratoLens keeps only an encrypted copy and can't show it again. Rotation takes effect immediately: the next delivery is signed with the new secret, so update the receiver right after rotating.
Custom Request Header
Optionally, one extra header is sent with every delivery, for receivers behind API Management, an API gateway or a Function key, such as Ocp-Apim-Subscription-Key or Authorization. The header gets the request past the gateway; the signature proves it came from StratoLens.
Notes
- Name: a valid HTTP header name (letters, digits and
!#$%&'*+.^_`|~-), at most 256 characters. - Reserved names, rejected ignoring case:
Content-Type,Content-Length,Host,Connection,Transfer-Encoding, and anything starting withX-StratoLens-. - Value: at most 4,096 characters, with no control characters and no characters outside Latin-1, such as emoji.
- The value is write-only: stored encrypted and never displayed or logged. Use Replace to change it or Remove to drop it.
Body
Top level
| Field | Type | Meaning |
|---|---|---|
schemaVersion | number | Always 1 for this contract. |
event | string | notification_alert or test (same as the header). |
scanId | string | ID of the scan that triggered the alert. Rule test sends use test-email; destination tests use test-sample. |
scanTimestamp | string | ISO 8601 time of the scan (or of the test). |
entry.id | string | Notification rule ID. Destination tests use test-sample-entry. |
entry.name | string | Notification rule name. |
sections | array | One per triggered alert type, in the rule's order. |
Each section
| Field | Type | Meaning |
|---|---|---|
alertTypeId | string | Stable alert type ID (table below). Key on this, not displayName. |
displayName | string | Alert type name as shown in StratoLens. |
summaryLine | string | Human-readable one-line summary. Wording may change; don't parse it. |
matchCount | number | Total items that matched, after filters. |
deepLinkUrl | string, optional | Link into StratoLens. For change-tracking types it opens the Changes view with the scan pair preselected; for others, the alert type's page. Absent when StratoLens has no public base URL configured. |
items | array of objects | Up to 10 rows, in the same order as the email table. Each row is a flat object of string values; keys per alert type below. Always empty when the destination's Detail level is Summary. |
itemsTruncated | boolean | true when matchCount is larger than the number of rows in items (always true on Summary destinations when anything matched). |
Alert Type IDs and Row Keys
| alertTypeId | Display name | Row keys |
|---|---|---|
cost_anomalies | Cost Anomalies | resource, type, date, costImpact (e.g. "USD 123.45 increase") |
orphaned_resources | Orphaned Resources | resource, type, subscription, monthlyCost |
sizing | Sizing | resource (ends with " (updated)" for changed recommendations), type, current, recommended, monthlySavings |
access_optimizations | Access Optimizations | principal, role, scope, lastActivity ("Never" when never used) |
subscription_quotas | Subscription Quotas | subscription (ends with " (updated)" for changed entries), location, quotaFamily, usage (e.g. "48 / 50 vCPUs"), utilization (e.g. "96.0%") |
commitment_coverage | Commitment Coverage | name, type (Expiring, Underutilized or Purchase), subscription, impact |
security_recommendations | Security Recommendations | resource, type, severity, finding |
service_retirements | Service Retirements | resource, retirement, deadline ("Undated" when unknown), impact |
policy_exemption_expiry | Policy Exemption Expiration | exemption, scope, category, expiresOn (YYYY-MM-DD), daysLeft (e.g. "12 days"), assignment |
resource_changes | Resource Changes | name, resourceGroup, subscription, type, change |
defender_recommendation_changes | Defender Recommendation Changes | name, resourceGroup, subscription, type, change |
defender_plan_changes | Defender Plan Changes | plan, subscription, tier, change |
defender_vulnerability_changes | Defender Vulnerability Changes | cve, subscriptions (e.g. "3 subscriptions"), severity (e.g. "High · CVSS 8.1"), change |
security_alerts | Defender Alerts | name, alertName, resourceGroup, subscription, change |
advisor_changes | Advisor Recommendation Changes | name, resourceGroup, subscription, type, change |
policy_assignment_changes | Policy Assignment Changes | name, resourceGroup, subscription, type, change |
policy_compliance_changes | Policy Compliance Changes | name, resourceGroup, subscription, type, change |
activity_log_events | Activity Log Events | time (ISO 8601), operation, resource, caller, status |
Notes
changeisAdded,ModifiedorDeleted. Change-tracking rows list added items first, then modified, then deleted.- Values are display strings (formatted currency, percentages, names). Missing values are
N/Aor—. activity_log_eventssections always link to the Activity Explorer without a time window, unlike the email.
Example
A delivery to a destination with the Full detail level. Values in angle brackets are placeholders.
{
"schemaVersion": 1,
"event": "notification_alert",
"scanId": "<scan id>",
"scanTimestamp": "2026-10-25T06:12:41.000Z",
"entry": { "id": "<rule id>", "name": "Platform team alerts" },
"sections": [
{
"alertTypeId": "orphaned_resources",
"displayName": "Orphaned Resources",
"summaryLine": "<summary text>",
"matchCount": 1,
"deepLinkUrl": "https://<your StratoLens host>/app/recommendations/orphaned-resources?metricsTimeframeDays=7",
"items": [
{ "resource": "disk-old-01", "type": "Disks", "subscription": "Production", "monthlyCost": "USD 12.00" }
],
"itemsTruncated": false
}
]
}Troubleshooting
Signatures never match
Answer
Compute the HMAC over the raw body bytes exactly as received, not re-serialized JSON. Include the timestamp and the dot, and use the secret string as the key without hex-decoding it.
Signatures stopped matching
Answer
Someone rotated the secret. Copy the new secret into the receiver, or rotate again and update the receiver immediately.
I receive duplicate deliveries
Answer
A retry after a timeout or 5xx. Drop repeats with the same X-StratoLens-Delivery.
Items are always empty
Answer
The destination's Detail level is Summary. Switch it to Full.
Saving the custom header fails
Answer
If the error says the name "is reserved and cannot be overridden", pick a name that isn't in the reserved list.