Get told when a job closes
Follow the jobs you hold with a monitor and receive a signed job.closed webhook when one closes, instead of re-checking them.
You list jobs on your board, or have candidates in process for them, and need to know when a posting closes. Instead of re-fetching every job every day, put their ids in a monitor: JobsPipe sends a signed job.closed webhook to your endpoint when one closes.
Monitors are on paid plans and cost no credits, including every event they send.
Monitors send job.closed only. To be told about new postings, save a search as a Signal with a webhook destination.
Create a webhook endpoint
Add an https URL in the dashboard under Delivery methods → Webhooks, or with your API key:
curl https://api.jobspipe.dev/api/webhooks \
-H "Authorization: Bearer jp_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/jobspipe", "enabledEvents": ["job.closed"] }'The response shows the signingSecret (whsec_...) once; store it. GET /api/webhooks lists your endpoints with their ids, which the monitor takes as webhook_id.
Create the monitor
Pass up to 1,000 job ids per request, exactly as search returned them.
curl https://api.jobspipe.dev/v1/monitors \
-H "Authorization: Bearer jp_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"type": "job",
"ids": ["4464825573", "8739185002", "no-such-job"],
"webhook_id": "0f8b6c1e-3a52-4d7e-9c41-2b8f7a6d5e30",
"expires_in_days": 90
}'import requests
HEADERS = {"Authorization": "Bearer jp_live_your_key_here"}
monitor = requests.post(
"https://api.jobspipe.dev/v1/monitors",
headers=HEADERS,
json={
"type": "job",
"ids": ["4464825573", "8739185002", "no-such-job"],
"webhook_id": "0f8b6c1e-3a52-4d7e-9c41-2b8f7a6d5e30",
"expires_in_days": 90,
},
).json()
for job in monitor["already_closed"]:
take_down(job["id"])const headers = {
Authorization: "Bearer jp_live_your_key_here",
"Content-Type": "application/json",
};
const monitor = await fetch("https://api.jobspipe.dev/v1/monitors", {
method: "POST",
headers,
body: JSON.stringify({
type: "job",
ids: ["4464825573", "8739185002", "no-such-job"],
webhook_id: "0f8b6c1e-3a52-4d7e-9c41-2b8f7a6d5e30",
expires_in_days: 90,
}),
}).then((r) => r.json());
for (const job of monitor.already_closed) takeDown(job.id);{
"id": "7f3c1a52-8a3e-4c1e-9f0a-2d6b4e1c9a77",
"type": "job",
"events": ["job.closed"],
"status": "active",
"expires_at": "2026-12-26T10:00:00.000Z",
"target_count": 1,
"open_target_count": 1,
"already_closed": [{ "id": "8739185002", "closed_at": "2026-09-20T00:00:00Z", "closed_reason": "gone" }],
"not_found": ["no-such-job"]
}Only open jobs are added. Already-closed jobs and unknown ids are reported back, for free, so you can act on them straight away.
Keep it in sync
Add jobs as you list them and remove the ones you take down yourself (up to 1,000 ids per request). List the records to see which are open and which are closed.
MON=https://api.jobspipe.dev/v1/monitors/7f3c1a52-8a3e-4c1e-9f0a-2d6b4e1c9a77
H="Authorization: Bearer jp_live_your_key_here"
curl "$MON/ids" -H "$H" -H "Content-Type: application/json" -d '{ "ids": ["5512093381", "5512093390"] }'
curl -X DELETE "$MON/ids" -H "$H" -H "Content-Type: application/json" -d '{ "ids": ["4464825573"] }'
curl "$MON/ids?limit=100" -H "$H"Adding answers added, target_count, already_closed and not_found; removing answers removed. The list returns data and a next_cursor to pass back as cursor.
Receive and verify deliveries
When a monitored job closes, your endpoint receives:
{
"event": "job.closed",
"id": "4464825573",
"timestamp": 1789374765,
"data": {
"job_id": "4464825573",
"monitor_ids": ["7f3c1a52-8a3e-4c1e-9f0a-2d6b4e1c9a77"],
"closed_reason": "gone",
"closed_at": "2026-09-14T08:12:45.123456+00:00",
"title": "Registered Nurse",
"company": "Northwind Health"
}
}X-JobsPipe-Signature is the hex HMAC-SHA256 of `${timestamp}.${rawBody}` with your signing secret, where timestamp is the X-JobsPipe-Timestamp header. Check it against the raw body and reject timestamps older than five minutes.
import hashlib, hmac, json, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b"whsec_your_signing_secret"
@app.post("/hooks/jobspipe")
def jobspipe_hook():
raw = request.get_data()
ts = request.headers.get("X-JobsPipe-Timestamp", "0")
sig = request.headers.get("X-JobsPipe-Signature", "")
expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(expected, sig):
abort(401)
event = json.loads(raw)
if event["event"] == "job.closed":
take_down(event["id"])
return "", 204import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const SECRET = "whsec_your_signing_secret";
app.post("/hooks/jobspipe", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const ts = req.get("X-JobsPipe-Timestamp") ?? "0";
const sig = req.get("X-JobsPipe-Signature") ?? "";
const expected = createHmac("sha256", SECRET).update(`${ts}.${raw}`).digest("hex");
const valid =
Math.abs(Date.now() / 1000 - Number(ts)) <= 300 &&
expected.length === sig.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(raw);
if (event.event === "job.closed") takeDown(event.id);
res.sendStatus(204);
});Answer 2xx within 10 seconds. Failed deliveries are retried up to 6 times over about 8.5 hours with the same id, so use it to drop duplicates. See delivery and retries.
Renew, review and delete
A monitor runs 1 to 365 days (default 90) and then stops sending. Renewing sets a new expiry from now and also works on an expired monitor.
H="Authorization: Bearer jp_live_your_key_here"
curl https://api.jobspipe.dev/v1/monitors/7f3c1a52-8a3e-4c1e-9f0a-2d6b4e1c9a77/renew \
-H "$H" -H "Content-Type: application/json" -d '{ "expires_in_days": 90 }'
curl "https://api.jobspipe.dev/v1/monitors?limit=100" -H "$H"
curl https://api.jobspipe.dev/v1/monitors/7f3c1a52-8a3e-4c1e-9f0a-2d6b4e1c9a77 -H "$H"
curl -X DELETE https://api.jobspipe.dev/v1/monitors/7f3c1a52-8a3e-4c1e-9f0a-2d6b4e1c9a77 -H "$H"A daily job that lists your monitors and renews any whose expires_at is near keeps them running. Delete a monitor once its open_target_count reaches 0.
Builder and Growth plans can monitor 100,000 records in total, Scale and Business 1,000,000. Only verified closes are sent: gone (the posting page is gone) and closed (the source marks it closed).