API reference
A scoped, authenticated REST surface for piping Permafrost data into your SIEM, IaC pipeline, or custom dashboard. Read-only at v1; write endpoints arrive when there's signal they're wanted. This page renders from the same OpenAPI 3.1 contract the API serves, so the docs and the wire format never drift.
Base URL and contract
The API is served at https://app.permafrostepm.com. Every route lives under the versioned /api/v1 prefix; a breaking change ships as a new version rather than mutating v1 in place.
The full machine-readable contract — every route, its required scope, parameters, the response envelope, and the rate-limit headers — is published as OpenAPI 3.1 at /api/v1/openapi.json. Generate a typed client from it, or import it into your API tooling of choice.
Authentication
Every request carries an Authorization: Bearer header with a key issued from Settings → API keys in your dashboard. Only customer admins can issue keys. Keys begin with the pfrost_ prefix and carry 160 bits of entropy.
The full key is displayed only at creation. Permafrost stores the SHA-256 hash, never the plaintext; if you lose the key, revoke it and issue a new one. Keys can be given an expiry at issuance and revoked at any time from the same screen — a revoked or expired key resolves to 401 immediately.
curl https://app.permafrostepm.com/api/v1/identities \
-H "Authorization: Bearer pfrost_a1b2c3d4e5f6..."A missing or invalid key returns 401. A valid key without the required scope returns 403 with the scope it was missing.
Scopes
Keys carry one or more capability scopes, bound at issuance time and immutable thereafter. Issue a separate key per integration so a compromised key can be revoked without breaking unrelated workflows, and so a noisy SIEM ingest cannot starve an IaC pipeline of rate-limit headroom.
read:identities— list and inspect users, groups, service principals, managed identities, and agent identities. Also gatesGET /api/v1/tenants.read:findings— list and inspect analyzer findings.read:roles— list right-sized role recommendations.manage:webhooks— list, create, and delete outbound webhook subscriptions. Required for any/api/v1/webhooksroute.provision:scim— reserved for SCIM provisioning. No route consumes it yet; it is published in the scope vocabulary so keys can be pre-issued.
Response envelope
List endpoints return a uniform shape. data carries the page of rows; meta carries pagination metadata and, when a tenant filter is active, the resolved Microsoft tenant id so you can confirm the scope was honored.
{
"data": [ { "id": "...", "displayName": "...", ... } ],
"meta": {
"total": 1820,
"limit": 100,
"offset": 0,
"tenant": "00000000-0000-0000-0000-000000000000"
}
}Detail endpoints (/identities/{id}, /findings/{id}) omit meta and return the object directly under data. Errors return a flat { error, reason? } body with the matching HTTP status.
Pagination and filters
List endpoints accept ?limit= (default 100, max 500 — larger values clamp rather than error) and ?offset= for zero-based paging. meta.total reports the full match count before pagination, so you can page deterministically.
All list endpoints accept ?tenant=<microsoftTenantId> to scope the result to a single connected tenant; omit it to return data from every tenant connected to this customer. Findings additionally accept ?severity=, ?status=, and ?findingType= (comma-separated where it makes sense, e.g. ?severity=critical,high).
Endpoints
Every route, with the scope it requires and a copy-paste example. This list is generated from the published OpenAPI contract.
/api/v1/identitiesread:identitiesList identities
curl https://app.permafrostepm.com/api/v1/identities \
-H "Authorization: Bearer pfrost_..."/api/v1/identities/{id}read:identitiesGet identity detail
curl https://app.permafrostepm.com/api/v1/identities/{id} \
-H "Authorization: Bearer pfrost_..."/api/v1/findingsread:findingsList findings
curl https://app.permafrostepm.com/api/v1/findings \
-H "Authorization: Bearer pfrost_..."/api/v1/findings/exportread:findingsExport findings (NDJSON/CEF)
curl https://app.permafrostepm.com/api/v1/findings/export \
-H "Authorization: Bearer pfrost_..."/api/v1/findings/{id}read:findingsGet finding detail
curl https://app.permafrostepm.com/api/v1/findings/{id} \
-H "Authorization: Bearer pfrost_..."/api/v1/rolesread:rolesList recommended roles
curl https://app.permafrostepm.com/api/v1/roles \
-H "Authorization: Bearer pfrost_..."/api/v1/uprread:uprGet the UPR rollup
curl https://app.permafrostepm.com/api/v1/upr \
-H "Authorization: Bearer pfrost_..."/api/v1/upr/principalsread:uprList per-principal UPR
curl https://app.permafrostepm.com/api/v1/upr/principals \
-H "Authorization: Bearer pfrost_..."/api/v1/upr/trendread:uprGet the UPR trend series
curl https://app.permafrostepm.com/api/v1/upr/trend \
-H "Authorization: Bearer pfrost_..."/api/v1/tenantsread:identitiesList connected tenants
curl https://app.permafrostepm.com/api/v1/tenants \
-H "Authorization: Bearer pfrost_..."/api/v1/webhooksmanage:webhooksList webhook subscriptions
curl https://app.permafrostepm.com/api/v1/webhooks \
-H "Authorization: Bearer pfrost_..."/api/v1/webhooksmanage:webhooksCreate a webhook subscription
curl -X POST https://app.permafrostepm.com/api/v1/webhooks \
-H "Authorization: Bearer pfrost_..." \
-H "Content-Type: application/json" \
-d '{
"name": "SOC pager",
"webhookUrl": "https://hooks.example.com/permafrost",
"webhookType": "generic",
"eventTypes": ["finding_critical", "finding_high"]
}'/api/v1/webhooks/{id}manage:webhooksGet a webhook subscription
curl https://app.permafrostepm.com/api/v1/webhooks/{id} \
-H "Authorization: Bearer pfrost_..."/api/v1/webhooks/{id}manage:webhooksDelete a webhook subscription
curl -X DELETE https://app.permafrostepm.com/api/v1/webhooks/{id} \
-H "Authorization: Bearer pfrost_..."Webhooks
Webhook subscriptions fan finding events out to Slack, Microsoft Teams, or any JSON receiver. Permafrost POSTs the payload after each analyzer pass for every severity the subscription opts into. Event types: finding_critical, finding_high, finding_medium, finding_low.
The create response contains a signingSecret field. This is the only time Permafrost will return it; persist it in your secret store immediately. Subsequent GET calls omit the field. If you lose the secret, delete the subscription and create a new one.
Every delivery carries two headers in addition to Content-Type: application/json:
X-Permafrost-Event— the event type string (e.g.finding_critical), useful for routing without parsing the body.X-Permafrost-Signature-256— HMAC-SHA256 of the exact request body bytes, hex-encoded, prefixed withsha256=. The format matches the common GitHub webhook signature scheme, so existing receiver libraries work without modification.
Verify the signature in your receiver before trusting the payload. A constant-time compare prevents timing oracles:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyPermafrostSignature(
rawBody: string,
signatureHeader: string | null,
secret: string,
): boolean {
if (!signatureHeader) return false;
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}Compute the HMAC over the raw request body bytes — not over a re-serialized JSON object. Frameworks that re-encode the body (object spread, JSON.parse + JSON.stringify) produce a different byte sequence and the comparison will fail.
For a managed push into Microsoft Sentinel — findings and Mode C remediation events streamed into a Log Analytics custom table within one sync cycle, no receiver to host — use the native Sentinel connector instead of a raw webhook. See SIEM integration.
Example
Pull all critical and high findings that are still open, across every tenant under this customer:
curl "https://app.permafrostepm.com/api/v1/findings?severity=critical,high&status=open&limit=200" \
-H "Authorization: Bearer pfrost_..."Rate limits
Each API key may make 1,000 requests per 15-minute window. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (epoch seconds until the window resets) headers. Over-quota requests return 429 Too Many Requests with a Retry-After header indicating seconds until the window resets.
Limits are enforced per key across all compute instances, so a burst spread across connections still respects the headline number. Issue separate keys for separate integrations to keep their windows independent.
Tenant isolation
Every row returned is scoped to the customer that issued the key, and spans all of this customer's connected tenants unless you narrow it with ?tenant=. A key issued by one customer can never read another customer's data — the isolation check happens at the database query layer, not as a post-filter.
A request that names a tenant id belonging to another customer returns 404 rather than 403, so the API never confirms the existence of a foreign row. Every finding, identity, and role row carries its azureTenantId so you can attribute it to the right connected tenant on your side.
Next stop
OpenAPI 3.1 spec
The machine-readable contract this page renders from. Generate a typed client or import it into your API tooling.
Next stop
SIEM integration
Stream findings and Mode C remediation events into Microsoft Sentinel, or export NDJSON / CEF for any CIM-compatible SIEM.
Next stop
Security posture
How Permafrost handles credentials, tokens, and tenant data end-to-end.
