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.
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; if you lose the plaintext, revoke the key and issue a new one.
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.
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.
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.
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 Azure tenant id.
{
"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.
Pagination and filters
List endpoints accept ?limit= (default 100, max 500) and ?offset=. All endpoints accept ?tenant=<azureTenantId> to scope the result to a single connected tenant; omit it to return data from every connected tenant for this customer.
Endpoints
/api/v1/identitiesread:identitiesList identities discovered across this customer's connected tenants.
/api/v1/identities/{id}read:identitiesFull identity detail including role assignments.
/api/v1/findingsread:findingsList analyzer findings. Filters: severity, status, findingType.
/api/v1/findings/{id}read:findingsFull finding detail.
/api/v1/rolesread:rolesList right-sized role recommendations with their target identity.
/api/v1/tenantsread:identitiesList connected Azure tenants for this customer.
/api/v1/webhooksmanage:webhooksList active outbound webhook subscriptions for this customer.
/api/v1/webhooksmanage:webhooksCreate a webhook subscription. Returns the signing secret once.
/api/v1/webhooks/{id}manage:webhooksGet one webhook subscription. The signing secret is never returned.
/api/v1/webhooks/{id}manage:webhooksDeactivate a webhook subscription (soft delete).
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.
Create a 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"]
}'The 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=. Format matches GitHub's 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) will produce a different byte sequence and the comparison will fail.
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 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. Issue separate keys for separate integrations so a noisy SIEM ingest does not starve your IaC pipeline of headroom.
Tenant isolation
Every row returned is scoped to the customer that issued the key. A key issued by customer A can never read customer B's data — the isolation check happens at the database query layer, not as a post-filter. Requests that name a tenant id belonging to another customer return 404 rather than 403 so the API never confirms the existence of a foreign row.