PAMbaseDocs
How-to

Webhooks & scheduling.

Most of PAMbase is pull — your app asks for context or reads memory. Webhooks are the push half: PAMbase POSTs to a URL you register the moment something relevant changes on a connected user's memory. Scheduling lets you ask PAMbase to call you back at a future time. Together they make cross-app reactions possible — without any two apps ever integrating directly.

Prerequisites
You hold a connection token with read scopes for the data you want to be notified about (see Connect an app), and a public HTTPS endpoint to receive POSTs. A scope is one permission; you are only ever notified within the scopes the user granted you.

The example: a newsletter that reacts to taste

A user uses Margin (a reading companion) and also subscribes to a separate newsletter app. When the user's taste shifts, Margin remembers a preference memory. The newsletter app holds memory:read:preference and subscribes to preference memories. The instant Margin writes that memory, PAMbase pushes a memory.created webhook to the newsletter app, which regenerates the user's digest to match the new interest — and neither app ever knew the other existed.

1. Register a subscription

In the dev portal under your app's Webhooks tab, add a delivery URL and optional filters: the event types and/or scope namespaces you care about (blank = everything you're allowed to read). You receive a signing secret once, on creation — copy it immediately into your secret store. You can rotate it any time; rotation invalidates the old secret.

You only ever receive what you can read
Subscriptions are gated by the same permission model as /v1/context. The newsletter app is notified about preference memories only because it holds memory:read:preference. See Scope & consent below.

2. Verify the signature over the RAW body, then handle it

Verify before you parse
The signature is computed over the exact bytes PAMbase sent. If your framework parses JSON before you verify, re-serializing changes the bytes and verification fails. Capture the raw body, verify it first, and only parse afterward.
app/api/pambase/webhook/route.ts
import { verifyWebhookSignature } from "@pambase/sdk";
// Next.js App Router: req.text() gives the raw body before any JSON parsing.
export async function POST(req: Request) {
const raw = await req.text();
const signature = req.headers.get("x-pambase-signature") ?? ""; // HMAC-SHA256 hex of the raw body
const delivery = req.headers.get("x-pambase-delivery") ?? ""; // stable id for dedupe
const ok = await verifyWebhookSignature({
payload: raw, // RAW body, not parsed JSON
signature,
secret: process.env.PAMBASE_WEBHOOK_SECRET!,
});
if (!ok) return new Response("bad signature", { status: 401 });
// Dedupe — delivery is at-least-once. Skip if we've already handled this id.
if (await alreadyProcessed(delivery)) return new Response("ok", { status: 200 });
const event = JSON.parse(raw);
switch (event.type) {
case "memory.created":
// { memory: { type, scope, content, importance }, sourceApp, aiId, connectionId }
if (event.memory.scope === "preference") await regenerateDigest(event.aiId);
break;
case "signal.created":
// { signalType, payload, sourceApp, aiId, connectionId, occurredAt }
break;
case "schedule.fired":
// { kind, payload, firedAt, aiId, connectionId } ← from a schedule() you created
break;
}
await markProcessed(delivery);
return new Response("ok", { status: 200 }); // ack within ~5s
}

Express is the same idea — keep the raw body around:

express.ts
import express from "express";
import { verifyWebhookSignature } from "@pambase/sdk";
const app = express();
// Capture the raw bytes for this route (no JSON parsing yet).
app.post("/pambase/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const raw = req.body.toString("utf8");
const ok = await verifyWebhookSignature({
payload: raw,
signature: req.header("x-pambase-signature") ?? "",
secret: process.env.PAMBASE_WEBHOOK_SECRET!,
});
if (!ok) return res.status(401).end();
const event = JSON.parse(raw);
// ...handle by event.type, dedupe on X-PAMbase-Delivery...
res.status(200).end();
});

The outbound events

PAMbase sends three event types. Their payload shapes:

EventPayloadFires when
memory.created{ memory:{type,scope,content,importance}, sourceApp, aiId, connectionId }A new memory is written in a scope you can read.
signal.created{ signalType, payload, sourceApp, aiId, connectionId, occurredAt }Another app emits a signal you subscribe to; the payload flows through verbatim.
schedule.fired{ kind, payload, firedAt, aiId, connectionId }A schedule you created reaches its fireAt time.

Delivery guarantees

PropertyBehavior
DeliveryAt-least-once — the same event may arrive more than once.
IdempotencyX-PAMbase-Delivery is a stable id; dedupe on it.
SignatureX-PAMbase-Signature = HMAC-SHA256(secret, raw body), hex. Verify over the raw bytes.
RetriesNon-2xx or timeout → exponential backoff, up to 6 attempts, then marked failed.
Ack windowReturn 2xx within ~5s. Do slow work after acking (queue it).
Ack fast, work later
If handling takes longer than a few seconds, enqueue the (already-verified) payload and return 200 immediately. Slow handlers trigger retries and duplicate work — dedupe on the delivery id regardless. For the newsletter app, that means: verify, enqueue a “regenerate digest” job, ack, and rebuild the digest off the queue.

3. Schedule your own triggers

schedule() gives PAMbase a sense of time. When fireAt elapses you receive a schedule.fired webhook (shape above) carrying your kind and payload. For example, the newsletter app schedules a weekly send:

typescript
const { id, fireAt } = await pambase.schedule({
fireAt: "2026-05-29T13:00:00Z", // ISO string or Date
kind: "weekly_digest",
payload: { edition: "2026-W22" },
});
await pambase.listSchedules(); // pending schedules for this connection
await pambase.cancelSchedule(id); // cancel before it fires

Scope & consent (gotchas)

  • You are never notified about data your scopes can't read — fanout is gated like /v1/context.
  • An app is never notified of its own events (no self-loops) — Margin won't receive a webhook for the preference it just wrote.
  • If the user revokes the connection, deliveries stop immediately. The connections page badges apps that receive notifications.

What to expect next

  • Remember & recall — the write side that triggers these webhooks.
  • Catalog — webhook event payloads and standard event types.
  • Security — signature verification and secret rotation in depth.