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. 18 apps included.

Your agent signs itself up for Ambiguous. Paste one prompt. It asks three questions, then hands you the workspace to claim.