Docs/Webhooks

Webhooks

PermitNetworks can send real-time HTTP notifications to your endpoints when important events occur. Webhooks are the recommended way to react to decisions, budget changes, and security events.

Configuring Webhooks

Register webhook endpoints in the Dashboard → Webhooks tab. Each endpoint has:

  • A URL (must be publicly reachable, HTTPS required in production)
  • An event filter (subscribe to specific event types or all events)
  • A signing secret (auto-generated — used for HMAC-SHA256 verification)

Webhooks can also be managed via POST /v1/webhooks in the API (requires webhooks:manage scope).

Event Types

decision.allow
decisions

Fired when an authorization request results in an allow decision.

decision.deny
decisions

Fired when an authorization request is denied by a policy.

decision.review
decisions

Fired when a decision has the review effect — allowed but flagged for human review.

budget.threshold
budget

Fired when an agent's budget consumption crosses the configured threshold (e.g. 80%).

budget.exceeded
budget

Fired when an agent's budget limit is reached. All subsequent decisions for this agent will be denied until the period resets.

agent.registered
agents

Fired the first time a new agent_id is seen in an authorization request.

agent.revoked
agents

Fired when an agent is manually revoked via the dashboard or API.

policy.updated
policies

Fired when a policy is created, updated, or disabled.

security.alert
security

Fired when unusual patterns are detected — e.g. rapid deny accumulation, invalid permit tokens, or anomalous request volume.

Payload Structure

Every webhook POST has the same envelope structure. The data field contains the event-specific payload.

JSON
{
  "id": "evt_01hwxyz9876543210fedcba",
  "type": "decision.deny",
  "created_at": "2025-04-21T10:30:00.000Z",
  "account_id": "acc_your_account",
  "data": {
    "decision": {
      "id": "dec_01hwxyz...",
      "effect": "deny",
      "reason": "rate_limit.exceeded",
      "agent_id": "billing-bot",
      "action": "payment.create",
      "resource": "account:acct_9kx2m",
      "evaluated_at": "2025-04-21T10:30:00.000Z"
    }
  }
}

// budget.threshold event data:
{
  "data": {
    "agent_id": "billing-bot",
    "policy_id": "pol_billing_payments",
    "budget_unit": "USD",
    "budget_limit": 50000,
    "budget_consumed": 40312.50,
    "threshold_pct": 0.8,
    "period": "monthly",
    "period_resets_at": "2025-05-01T00:00:00Z"
  }
}

Verifying Signatures

Each webhook request includes a X-PermitNetworks-Signature header containing an HMAC-SHA256 of the raw request body, signed with your endpoint's secret. Always verify this before processing the event.

TypeScript (Next.js Route Handler)
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-permitnetworks-signature");
  const secret = process.env.WEBHOOK_SECRET!;

  // Verify HMAC-SHA256
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  if (signature !== `sha256=${expected}`) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(rawBody);

  switch (event.type) {
    case "decision.deny":
      console.log("Denied:", event.data.decision.agent_id, event.data.decision.reason);
      break;
    case "budget.exceeded":
      console.log("Budget exceeded for agent:", event.data.agent_id);
      await notifyOncall(event.data);
      break;
    case "security.alert":
      await escalate(event.data);
      break;
  }

  return NextResponse.json({ received: true });
}
Express.js
import express from "express";
import crypto from "crypto";

const app = express();

app.post("/webhooks/permitnetworks",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-permitnetworks-signature"] as string;
    const secret = process.env.WEBHOOK_SECRET!;

    const expected = "sha256=" + crypto
      .createHmac("sha256", secret)
      .update(req.body)
      .digest("hex");

    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    )) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(req.body.toString());
    console.log("Received:", event.type);
    res.json({ received: true });
  }
);

Use crypto.timingSafeEqual() for the comparison to prevent timing attacks. Never use === to compare HMAC values.

Retry Policy

PermitNetworks considers a delivery successful if your endpoint returns a 2xx status within 10 seconds. On failure, we retry with exponential backoff:

AttemptDelayTotal elapsed
1st retry30 seconds30s
2nd retry5 minutes~6m
3rd retry30 minutes~36m
4th retry2 hours~2.6h
5th retry8 hours~10.6h
Final retry14 hours~24h — then abandoned

Failed deliveries and retry history are visible in the Dashboard → Webhooks → Deliveries log. You can manually replay any failed event from the dashboard.

Best Practices

  • Respond quickly (< 2 seconds) — move slow processing to a background job or queue.
  • Make handlers idempotent — retries can deliver the same event multiple times. Use event.id to deduplicate.
  • Always verify the signature before processing any event payload.
  • Subscribe only to the events you need — don't subscribe to everything and filter client-side.
  • Log the raw payload before processing so you can replay events if your handler throws.