Guides

Webhook ingestion

Forward a webhook from any upstream into MemHQ as episodes.

Webhook ingestion

For sources that don't have a managed connector, the pattern is: stand up a thin handler that receives the upstream webhook and translates it into one /v1/memhq/add call per event.

Example: Slack message webhook

// app/api/slack/route.ts (Next.js)
import { NextResponse } from "next/server";
import { MemoryClient } from "@memhq/sdk";

const memhq = new MemoryClient();

export async function POST(req: Request) {
  const body = await req.json();

  // Slack URL verification — respond once on first setup.
  if (body.type === "url_verification") {
    return NextResponse.json({ challenge: body.challenge });
  }

  const event = body.event;
  if (event?.type !== "message" || event.subtype) {
    return NextResponse.json({ ok: true });
  }

  // Map Slack user → MemHQ user. In practice you'd look up the linked
  // identity in your own DB; for the example we use the Slack user ID
  // directly.
  await memhq.add({
    userId: `slack:${event.user}`,
    groupId: `slack:channel:${event.channel}`,
    messages: [
      {
        role: "user",
        content: event.text,
      },
    ],
    metadata: {
      source: "slack",
      ts: event.ts,
      channel: event.channel,
    },
  });

  return NextResponse.json({ ok: true });
}

Slack delivers the same event multiple times under retries. The reconciler's reinforce path absorbs the duplicates, but you can also short-circuit by hashing event.ts and skipping replays.

Bulk import from a file

For one-time backfill from an export (a JSON dump, a CSV of past transcripts), batch the add calls with bounded concurrency:

import { MemoryClient } from "@memhq/sdk";
import pLimit from "p-limit";

const memhq = new MemoryClient();
const limit = pLimit(8);

const transcripts: Transcript[] = JSON.parse(await Bun.file("export.json").text());

await Promise.all(
  transcripts.map((t) =>
    limit(() =>
      memhq.add({
        userId: t.userId,
        messages: t.messages,
        metadata: { source: "backfill", original_id: t.id },
      }),
    ),
  ),
);

The ingestion worker processes the queue at its own pace; the API accepts 4-figure-per-second peak bursts without backpressure.

Coming soon — full guide

Worked examples for Linear, GitHub, and Notion webhooks are in progress.