Skip to content

Webhooks

Webhooks let PLUR push events at your systems instead of you polling. Useful for piping engram activity into Slack, audit warehouses, Datadog dashboards, or any other internal system.

In the admin dashboard at /admin/webhooks (org-wide) or /me/webhooks (your own events):

  1. Click New webhook.
  2. Enter the target URL.
  3. Pick events to subscribe to (see Events below).
  4. PLUR generates a webhook secret — copy it; it’s used for HMAC verification.
  5. Optional: test-fire to verify your endpoint accepts the request.
EventFires when
engram.createdAn engram is saved.
engram.updatedAn engram’s fields are updated (status, activation, feedback).
engram.retiredAn engram moves to retired status.
engram.pinnedAn admin pins / unpins.
pack.installedA Knowledge Pack is installed.
pack.uninstalledA Pack is removed.
pack.exportedA Pack is exported.
session.startedA new session is created.
session.endedA session is closed (carries summary + new engram count).
user.createdNew user provisioned (SCIM or manual).
user.disabledUser is disabled.
audit.entryAny audit row is written. High-volume — subscribe carefully.

You can subscribe to all of them or just the ones you care about.

Every webhook POST has this envelope:

{
"event": "engram.created",
"id": "wh-evt-2026-0525-001",
"ts": "2026-05-25T14:32:18.123Z",
"org": "acme",
"actor": { "type": "user", "id": "u_alice" },
"data": { /* event-specific payload */ }
}

Headers:

Content-Type: application/json
X-PLUR-Event: engram.created
X-PLUR-Delivery: wh-dlv-2026-0525-001
X-PLUR-Signature: sha256=<hex hmac>

The signature is HMAC-SHA256 over the raw request body with the webhook’s secret as the key. Verify before processing:

import crypto from 'node:crypto';
function verify(rawBody: string, signature: string, secret: string): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}

In Python:

import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)

Use constant-time comparison — naive == is timing-attack-prone.

Delivery is at-least-once. On non-2xx response or timeout:

  • Retry with exponential backoff: 30 s, 1 min, 5 min, 15 min, 1 h.
  • Max 5 attempts.
  • If all attempts fail, webhook is marked failing.
  • After 24 hours of continuous failures, the webhook is auto-disabled and the admin gets an email.

Idempotency: every event carries an id field. Your handler should be idempotent on that ID — PLUR may deliver the same event twice during retries.

Every delivery carries a fresh X-PLUR-Delivery ID and ts. Reject deliveries with timestamps older than 5 minutes (against your server’s clock). The signature alone doesn’t prevent replay; the timestamp does.

When auto-disabled, the webhook stays registered but is paused. Re-enable from /admin/webhooks or /me/webhooks after fixing your endpoint. Test-fire before re-enabling — you don’t want the next real event to be the canary.

Default rate: ~50 events/sec/webhook. Higher-volume orgs can raise this; talk to your PLUR contact. The dispatcher (src/webhooks/dispatcher.ts) holds a small in-process queue — if your endpoint is slow, deliveries back up, retries kick in, and eventually the webhook auto-disables. Make your endpoint fast: 200ms target, 1s ceiling.

  • For triggering an LLM call — webhooks are not real-time enough; round-trip is ~50ms but retries can push effective latency to seconds. Use the MCP/REST API.
  • For a Slack notification on every recall — too noisy. Subscribe to engram.created / pack.installed instead.
  • For audit-grade compliance pipelines — use the audit log export instead, or subscribe to audit.entry and periodically reconcile with a CSV export to catch missed deliveries.