Skip to content
Integrations

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 gates GET /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/webhooks route.
  • 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.

GET/api/v1/identitiesread:identities

List identities

curl https://app.permafrostepm.com/api/v1/identities \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/identities/{id}read:identities

Get identity detail

curl https://app.permafrostepm.com/api/v1/identities/{id} \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/findingsread:findings

List findings

curl https://app.permafrostepm.com/api/v1/findings \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/findings/exportread:findings

Export findings (NDJSON/CEF)

curl https://app.permafrostepm.com/api/v1/findings/export \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/findings/{id}read:findings

Get finding detail

curl https://app.permafrostepm.com/api/v1/findings/{id} \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/rolesread:roles

List recommended roles

curl https://app.permafrostepm.com/api/v1/roles \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/uprread:upr

Get the UPR rollup

curl https://app.permafrostepm.com/api/v1/upr \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/upr/principalsread:upr

List per-principal UPR

curl https://app.permafrostepm.com/api/v1/upr/principals \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/upr/trendread:upr

Get the UPR trend series

curl https://app.permafrostepm.com/api/v1/upr/trend \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/tenantsread:identities

List connected tenants

curl https://app.permafrostepm.com/api/v1/tenants \
  -H "Authorization: Bearer pfrost_..."
GET/api/v1/webhooksmanage:webhooks

List webhook subscriptions

curl https://app.permafrostepm.com/api/v1/webhooks \
  -H "Authorization: Bearer pfrost_..."
POST/api/v1/webhooksmanage:webhooks

Create 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"]
  }'
GET/api/v1/webhooks/{id}manage:webhooks

Get a webhook subscription

curl https://app.permafrostepm.com/api/v1/webhooks/{id} \
  -H "Authorization: Bearer pfrost_..."
DELETE/api/v1/webhooks/{id}manage:webhooks

Delete 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 with sha256=. 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.