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.allowFired when an authorization request results in an allow decision.
decision.denyFired when an authorization request is denied by a policy.
decision.reviewFired when a decision has the review effect — allowed but flagged for human review.
budget.thresholdFired when an agent's budget consumption crosses the configured threshold (e.g. 80%).
budget.exceededFired when an agent's budget limit is reached. All subsequent decisions for this agent will be denied until the period resets.
agent.registeredFired the first time a new agent_id is seen in an authorization request.
agent.revokedFired when an agent is manually revoked via the dashboard or API.
policy.updatedFired when a policy is created, updated, or disabled.
security.alertFired 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.
{
"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.
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 });
}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:
| Attempt | Delay | Total elapsed |
|---|---|---|
| 1st retry | 30 seconds | 30s |
| 2nd retry | 5 minutes | ~6m |
| 3rd retry | 30 minutes | ~36m |
| 4th retry | 2 hours | ~2.6h |
| 5th retry | 8 hours | ~10.6h |
| Final retry | 14 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.