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