integrations

Express

A complete TypeScript receiver using Express. Handles signature verification, filters by job type, logs each posting, and returns a fast 200.

Dependencies

npm install express body-parser
npm install -D @types/express tsx typescript

server.ts

// server.ts
import express, { Request, Response } from "express";
import bodyParser from "body-parser";
import { createHmac, timingSafeEqual } from "crypto";

const app = express();
app.use(bodyParser.json());

const WEBHOOK_SECRET = "replace-with-your-sign-key";

// Recursively sort object keys — replicates Python's sort_keys=True
function sortObjectKeys(obj: unknown): unknown {
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
  if (obj !== null && typeof obj === "object") {
    return Object.fromEntries(
      Object.entries(obj as Record<string, unknown>)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([k, v]) => [k, sortObjectKeys(v)]),
    );
  }
  return obj;
}

function verifySignature(
  jobs: Record<string, unknown>[],
  secret: string,
  received: string,
): boolean {
  const sorted = [...jobs]
    .sort((a, b) => (String(a.url) < String(b.url) ? -1 : 1))
    .map(sortObjectKeys);
  const canonical = JSON.stringify(sorted);
  const expected = createHmac("sha256", secret)
    .update(canonical, "utf8")
    .digest("hex");
  try {
    return timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(received, "hex"),
    );
  } catch {
    return false;
  }
}

app.post("/webhook", (req: Request, res: Response) => {
  const receivedSig = req.headers["webhook-signature"] as string | undefined;
  if (!receivedSig) {
    return res.status(401).json({ error: "Missing signature" });
  }

  const jobs: Record<string, unknown>[] = req.body?.data ?? [];

  if (!verifySignature(jobs, WEBHOOK_SECRET, receivedSig)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  for (const job of jobs) {
    // Skip test events in production
    if (job.is_test) {
      console.log("Skipping test event");
      continue;
    }

    // Filter by job type
    if (job.is_intern) {
      console.log(`Internship: ${job.title} at ${job.company_name}`);
    } else if (job.is_fte) {
      console.log(`FTE: ${job.title} at ${job.company_name}`);
    }
  }

  return res.json({ ok: true, received: jobs.length });
});

app.listen(3000, () => console.log("Listening on :3000"));

Run it

npx tsx server.ts

Local testing with ngrok

Freshbatch needs a public URL to POST to. During local development, use ngrok to tunnel your local server:

# In a separate terminal
ngrok http 3000

# Copy the https:// URL ngrok gives you, e.g.:
# https://abc123.ngrok-free.app/webhook
# Paste that into Dashboard Settings as your webhook URL

For signature verification details, see Signature Verification — TypeScript