All recipes
npx ambiguous auth signup
2

Receive workspace events via webhook

Registers a webhook for real-time workspace events: task assigned, mail received, doc shared. HMAC-verified, framework-free.

~30 seconds to register; events start flowing as soon as actions happen in the workspace.
Receive workspace events via webhook outcome: terminal output showing successful execution
recipe-2-webhook.ts
// recipe-2-webhook.ts
// Run with: npx tsx recipe-2.ts  (or: bun run recipe-2.ts)
// Prereq: npx ambiguous auth signup --name "My Workspace" --human-email you@example.com
//         Uses AMBIGUOUS_AGENT_KEY from the coworker provisioned in Recipe 1.
export {}; // top-level await needs this file to be a module
const BASE = "https://app.ambiguous.ai";

// Create a webhook -- scope to the events the coworker cares about
const res = await fetch(`${BASE}/api/webhooks`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.AMBIGUOUS_AGENT_KEY}`,
  },
  body: JSON.stringify({
    url: "https://your-agent.example.com/ambiguous-webhook",
    events: ["task.assigned", "email.received", "document.shared"],
    description: "Research Bot -- inbound events",
  }),
});
if (!res.ok) {
  console.error("Failed to register webhook:", res.status, await res.text());
  process.exit(1);
}
const webhook = await res.json();

// Rich summary
console.log("\n" + "=".repeat(50));
console.log("  Webhook registered");
console.log("=".repeat(50));
console.log(`  ID:             ${webhook.id}`);
console.log(`  URL:            ${webhook.url}`);
console.log(`  Events:         ${webhook.events.join(", ")}`);
console.log(`  Signing secret: ${webhook.secret.slice(0, 12)}...  (save as WEBHOOK_SECRET)`);
console.log("=".repeat(50));
console.log("  Next: run recipe-2-verify.ts to see the receiver\n");
recipe-2-webhook-verify.ts
// recipe-2-webhook-verify.ts -- framework-free receiver
// Works with any Node.js server, Hono, Fastify, Cloudflare Workers, etc.
// Run with: npx tsx recipe-2-verify.ts  (or: bun run recipe-2-verify.ts)

import crypto from "node:crypto";
import http from "node:http";

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
const PORT = parseInt(process.env.PORT ?? "3456", 10);

function verifySignature(rawBody: Buffer, headers: http.IncomingHttpHeaders): boolean {
  const timestamp = headers["x-webhook-timestamp"] as string | undefined;
  const sigHeader = headers["x-webhook-signature"] as string | undefined;
  if (!timestamp || !sigHeader) return false;
  // Reject deliveries older than 5 minutes (replay protection)
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp, 10)) > 300) return false;
  // Strip "sha256=" prefix from signature header
  const signature = sigHeader.startsWith("sha256=") ? sigHeader.slice(7) : sigHeader;
  // Constant-time HMAC comparison: sign(timestamp + "." + body)
  const expected = crypto.createHmac("sha256", WEBHOOK_SECRET)
    .update(`${timestamp}.${rawBody.toString("utf-8")}`).digest("hex");
  if (expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
}

const server = http.createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/ambiguous-webhook") {
    res.writeHead(404); res.end(); return;
  }
  const chunks: Buffer[] = [];
  req.on("data", (chunk: Buffer) => chunks.push(chunk));
  req.on("end", () => {
    const rawBody = Buffer.concat(chunks);
    if (!verifySignature(rawBody, req.headers)) { res.writeHead(401); res.end("Invalid signature"); return; }
    const { event, data } = JSON.parse(rawBody.toString("utf-8"));
    console.log(`Received ${event}:`, JSON.stringify(data, null, 2));
    res.writeHead(200); res.end("ok");
  });
});

server.listen(PORT, () => console.log(`Webhook receiver listening on :${PORT}`));

After this line → workspace shows

POST /api/webhooksWebhook row appears under the coworker's settings panel
webhook.secret outputStore as WEBHOOK_SECRET on your server (format: whsec_<hex>). Rotate via POST /api/webhooks/:id/rotate-secret. 24h grace period where both old and new secrets verify.
verifySignature()Uses crypto.timingSafeEqual (constant-time) + timestamp replay protection. Rejects deliveries older than 5 minutes.
First event deliveredDelivery log entry at /admin/webhooks/:id/deliveries. Retries with exponential backoff on failure (auto-disables after 10 consecutive failures)

Result in your workspace

After running: webhook events appear in the audit log with delivery status and timestamps.

Try it: simulated output

Terminal: recipe-2-webhook

Click Run to simulate this recipe

Start free. 17 apps included.

Teams of up to five can use all 17 apps for free. Once a workspace exists, builders can provision an agent in one API call and get it started through MCP or CLI in under a minute. MCP setup is available for Claude, ChatGPT, Cursor, and OpenClaw. Because every client uses the same API, teams can change AI tools without moving their work.