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.
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.
/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
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 bodyconst delivery = req.headers.get("x-pambase-delivery") ?? ""; // stable id for dedupeconst ok = await verifyWebhookSignature({payload: raw, // RAW body, not parsed JSONsignature,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 createdbreak;}await markProcessed(delivery);return new Response("ok", { status: 200 }); // ack within ~5s}
Express is the same idea — keep the raw body around:
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:
| Event | Payload | Fires 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
| Property | Behavior |
|---|---|
| Delivery | At-least-once — the same event may arrive more than once. |
| Idempotency | X-PAMbase-Delivery is a stable id; dedupe on it. |
| Signature | X-PAMbase-Signature = HMAC-SHA256(secret, raw body), hex. Verify over the raw bytes. |
| Retries | Non-2xx or timeout → exponential backoff, up to 6 attempts, then marked failed. |
| Ack window | Return 2xx within ~5s. Do slow work after acking (queue it). |
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:
const { id, fireAt } = await pambase.schedule({fireAt: "2026-05-29T13:00:00Z", // ISO string or Datekind: "weekly_digest",payload: { edition: "2026-W22" },});await pambase.listSchedules(); // pending schedules for this connectionawait 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.