Freshness and lifecycle
How fresh is a job, and how do I know when it opened, reposted or closed?
Every job carries its own timestamps and a lifecycle status. You do not have to trust a freshness claim. You can read it off each record.
The timestamps
Section titled “The timestamps”| Field | Set by | Meaning | Can be null |
|---|---|---|---|
posted_at |
The employer | Date the employer posted the job, if known. | Yes. Many postings do not state it. |
first_seen_at |
Any source | Earliest time any source saw the job. | No |
last_seen_at |
Any source | Latest time any source saw the job. | No |
last_verified_at |
Origin check | Latest time the job was confirmed live at its origin. | Yes. null = never verified at origin. |
sources[].first_seen_at |
One provider | When this provider first saw the job. | No |
sources[].last_seen_at |
One provider | When this provider last saw the job. | No |
An illustrative job from the spec:
{ "posted_at": "2026-10-08T00:00:00Z", "first_seen_at": "2026-10-08T06:40:00Z", "last_seen_at": "2026-10-11T06:10:00Z", "last_verified_at": "2026-10-11T06:10:00Z", "status": "open", "closed_reason": null, "repost_count": 0}The canonical first_seen_at is the earliest of the sources[].first_seen_at values. The canonical last_seen_at is the latest of the sources[].last_seen_at values. So adding sources can only make a job look earlier-found and more recently seen, never staler.
Which timestamp to use
Section titled “Which timestamp to use”| You want | Use |
|---|---|
| “New jobs since my last sync” | first_seen_at. It is always set, and a repost does not reset it. |
| “How old is the opening?” | posted_at when set, else first_seen_at. |
| “Is it still up?” | status, then last_verified_at for proof from the origin. |
| “How stale is this record?” | Now minus last_seen_at. |
| “Which provider found it first?” | The sources[] entry with the earliest first_seen_at. |
Lifecycle
Section titled “Lifecycle”A job is open or closed. When it closes, closed_reason says why.
status |
closed_reason |
Meaning |
|---|---|---|
open |
null |
Live. closed_reason is always null while open. |
closed |
filled |
The role was filled. |
closed |
expired |
The listing ran out. |
closed |
removed |
The listing was taken down. |
closed |
unknown |
It is gone, the reason is not known. |
A re-listed job is not a new job. It keeps its id and repost_count goes up. See Canonical jobs.
Searches return only open jobs by default. Send filters.include_closed: true to get closed ones too, for example when you backfill history or reconcile a CRM.
curl https://api.betterjobs.cc/v1/jobs/search \ -H "Authorization: Bearer $BETTERJOBS_API_KEY" \ -H "BetterJobs-Version: 2026-10-01" \ -H "Content-Type: application/json" \ -d '{ "filters": { "company_domain_or": ["acme-robotics.example"], "posted_within_days": 30, "include_closed": true }, "waterfall": { "max_credits": 100 }, "limit": 100 }'import osimport requests
resp = requests.post( "https://api.betterjobs.cc/v1/jobs/search", headers={ "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}", "BetterJobs-Version": "2026-10-01", }, json={ "filters": { "company_domain_or": ["acme-robotics.example"], "posted_within_days": 30, "include_closed": True, }, "waterfall": {"max_credits": 100}, "limit": 100, },)resp.raise_for_status()for job in resp.json()["data"]: print(job["id"], job["status"], job["closed_reason"], job["last_seen_at"])const resp = await fetch('https://api.betterjobs.cc/v1/jobs/search', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`, 'BetterJobs-Version': '2026-10-01', 'Content-Type': 'application/json', }, body: JSON.stringify({ filters: { company_domain_or: ['acme-robotics.example'], posted_within_days: 30, include_closed: true, }, waterfall: { max_credits: 100 }, limit: 100, }),});if (!resp.ok) throw new Error(`BetterJobs ${resp.status}`);const { data } = await resp.json();for (const job of data) console.log(job.id, job.status, job.closed_reason, job.last_seen_at);Lifecycle events
Section titled “Lifecycle events”Instead of polling, watch a company or a saved search. Each change arrives as a typed event:
| Event | What happened | What to do | data | Cost |
|---|---|---|---|---|
job.opened | A new canonical job appeared for a watched company or saved search. | Trigger outbound. This is the only event that should start a sequence. | job (full Job) | 1 credit |
job.reposted | The same job was re-listed. repost_count went up. | Do not re-trigger outbound. Update your copy of the job only. | job (id, title, company, status, repost_count) | Free |
job.closed | The job is no longer live. closed_reason says why: filled, expired, removed or unknown. | Stop sequences tied to this job. Mark it closed in your CRM. | job (id, title, company, status, closed_reason) | Free |
job.updated | A field on an open job changed, for example salary or location. | Upsert the job by id. No outbound action. | job (full Job) | Free |
Only job.opened should start outbound. See Events and Webhooks.
How fresh are the sources?
Section titled “How fresh are the sources?”BetterJobs is only as fresh as the sources that see a job first. Each partner provider publishes its own refresh claims. They are theirs, not measured by BetterJobs:
| Source | Refresh, as published | Per |
|---|---|---|
| Reqbeat | Refresh: Corpus refreshed every 3 hours | per Reqbeat docs |
| SignalsAPI | Recheck: Sources rechecked every 15 minutes | per SignalsAPI docs |
| TheirStack | Discovery: 73% of jobs discovered the same day, 91% by the end of the next day | per TheirStack docs |
| JobsPipe | Freshness: Under 6h on Builder, under 1h on Scale | per JobsPipe docs |
| Coresignal | Recheck: Active postings rechecked within 24h | per Coresignal docs |
| Techmap | Feeds: Daily country feeds via AWS Data Exchange | per Techmap (jobdatafeeds.com) |
The BetterJobs index refresh rate will be published at GA. The freshest_first strategy asks the fastest-refreshing sources first. See the waterfall.
Related
Section titled “Related”- Canonical jobs: reposts and stable ids
- Sync patterns: backfill, daily upsert, closing stale jobs
- Detect hiring changes
- Field dictionary