PodArmor docs
Integrations

Webhooks

HMAC-signed event payloads, delivery semantics, and verification.

PodArmor's webhooks are tenant-scoped: each tenant configures a destination URL + signing secret per event type. We POST a JSON payload, sign it with HMAC-SHA256, and retry on transient failure with exponential backoff.

Event types

EventWhen it firesUse case
image.scannedA new scan run completed for an image-version.Watch for CVE-count regressions; trigger CI re-pulls.
image.rebuiltA new immutable -r{epoch} tag was published.Auto-promote the new tag through your staging→prod pipeline.
cve.watchedA specific CVE id (configured per tenant) was matched in a scan.Get paged when a CVE you're tracking lands.
version_deprecatedAn admin marked a version as deprecated.Block deploys of the deprecated version in CI.

Payload shape

{
  "event": "image.scanned",
  "tenantId": "abc-123",
  "imageId": "image-uuid",
  "versionId": "version-uuid",
  "data": {
    "imageName": "java17-deploy",
    "version": "17.0.19-r0",
    "outstandingCount": 0,
    "residualCount": 19,
    "criticalCount": 0,
    "highCount": 0,
    "scannedAt": "2026-05-12T14:00:00Z"
  },
  "deliveryId": "delivery-uuid",
  "deliveredAt": "2026-05-12T14:00:01Z",
  "attemptNumber": 1
}

HMAC verification

Every POST includes an X-PodArmor-Signature header in the format sha256=<hex>. The signature is computed as:

HMAC-SHA256(secret, raw_body) → hex

Verify in your receiver before processing:

import { createHmac, timingSafeEqual } from 'crypto';

function verify(req, secret) {
  const header = req.headers['x-podarmor-signature'];
  if (!header || !header.startsWith('sha256=')) return false;
  const expected = createHmac('sha256', secret)
    .update(req.rawBody) // raw bytes, NOT JSON.stringify(req.body)
    .digest('hex');
  const got = header.slice(7);
  if (expected.length !== got.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}

Use raw bytes, not JSON.stringify(req.body) — Express's body-parser re-emits JSON with a different key order on some versions, and the signature won't match. Most frameworks expose a raw-body buffer (Express: app.use(bodyParser.raw({ type: 'application/json' })) and read req.body as a Buffer; tRPC / Hono / Fastify all expose similar).

Delivery semantics

  • At-least-once. A successful 2xx response from your receiver means we stop retrying. Anything else (4xx, 5xx, connection error, timeout) triggers a retry.
  • Retry policy. 10 attempts max, exponential backoff (60s → 60s × 2^N, capped at 1h). After 10 failures we mark the delivery failed and surface it in the portal's Webhooks page so an admin can see.
  • Order. We do NOT guarantee delivery order. Two events fired close together can arrive out of order; your receiver should be idempotent based on deliveryId.
  • Timeout. Each attempt times out after 60s.

Replay protection

Each deliveryId is unique per delivery. To protect against replays, deduplicate on deliveryId in your receiver and reject any payload with a deliveredAt older than ~10 minutes (clock-skew tolerant).

The HMAC signature alone is replay-vulnerable if an attacker captures one payload and replays it — the timestamp + deliveryId check closes that gap.

On this page