On this page
Key takeaways
- A job post is one record with a stable id, a company resolved to an id, and an is_active state. The id is your de-dupe key.
- The feed and the API return the same record. License the feed to hold every posting in your own index, or query the API to watch a list of accounts and pay per request.
- The signal is a loop: search on a schedule, keep the posts you have not seen, and group them by company into one alert.
- Deal with reposts and expiries before you alert anyone, and keep the source label on every post.
Most pages about job postings data argue over counts. This one skips that. The record is the easy part, and most providers return roughly the same fields. The hard part is the loop that turns a stream of posts into one line a user can act on: "Example Corp opened 4 roles this week." This guide shows the record, both ways to get it, the request body, the de-dupe rule, the repost problem, the source field and what each query costs.
The record: what one job post contains
Forager publishes the job post schema in two places. The data feed schema (version 2.0.0) is the full record delivered to your warehouse. The API search result is a trimmed version of the same record with the organization and location expanded into objects. The fields that matter for a signal are on both.
| Field (feed) | Type | What it is for |
|---|---|---|
forager_id | integer | The stable id of the post. Your de-dupe key. The API returns it as id. |
source_id | string, nullable | The post's id on the site it came from. |
title | string, nullable | The role. The API search filter on this field takes a boolean text query. |
description | string, nullable | Full posting text. Searchable the same way. |
url | string, nullable | Where the post lives. |
date_featured | date, nullable | The date attached to the listing. Drives the date_featured_start and date_featured_end filters. |
date_updated | date, nullable | When Forager last crawled the record. |
is_active | boolean | Whether the post is still live. Defaults to true in a search. |
is_remote | boolean | Remote flag. |
location | string, nullable | Location as written in the post. |
parsed_location | array of ParsedLocation (id, name) | The location resolved to Forager location ids. In the API it comes back as a location object with osm_locations. |
salary_details | string, nullable | Salary text where the post stated one. |
organization_forager_id | integer, nullable | The company id. In the API it is organization.id. |
organization_name, organization_domain, organization_linkedin_url | string, nullable | The company, resolved. The API nests these under organization. |
source (API) | enum | The job board the post was collected from; the API reference lists the values. |
Three things to notice. First, the company is resolved to an id, not left as a string, so "Example Corp", "Example Corp., Inc." and "example.com" are the same company in every query. Second, is_active is a state, not a filter you have to infer from a date. Third, the id is stable across runs, which is what makes the de-dupe step in the next section a one-line set lookup.
Feed or API: same record, two shapes
You get the same record either way. The choice is about who holds the index.
License the job postings feed when your product needs every posting in its own index: a job board, a labor-market model, a search feature over all open roles. The feed is an annual license, delivered as a Snowflake private listing or as compressed drops (NDJSON by default, with CSV, TSV and Parquet) to your S3 or GCS bucket, daily, weekly or monthly. Each row carries id, date_updated, schema_version and the record in serialized_data, so a schema bump never breaks a pipeline that pins a version. Delivery cadence is how often files arrive. Refresh cadence, how often the underlying posts are recrawled, is written into the agreement.
Query the job postings API when you watch a list of accounts and want to know what changed. POST /datastorage/job_search/ takes organization_ids, is_active, date_featured_start and page, and returns the posts that match. You pay per request, you hold no index, and the loop below runs on a schedule.
Both shapes use the same company ids, so a product can start on the API and move the heavy queries to the feed later without remapping companies.
per job search call, against 1 credit for a people search or an organization search.
Build the signal in four steps
The loop is: resolve companies to ids once, search since the last run, drop ids you have already seen, group by company. The full recipe with cURL, Python and JavaScript is in the docs at recipes/hiring-signals. The parts that matter are below.
1. Resolve each company to an id (free)
Organizations autocomplete takes a name or a domain and returns candidates whose text reads "name - domain", so you can confirm you have the right company before you store the id.
curl -G "https://api-v2.forager.ai/api/$FORAGER_ACCOUNT_ID/datastorage/autocomplete/organizations/" \
-H "X-API-KEY: $FORAGER_API_KEY" \
--data-urlencode "q=example.com"
Autocomplete is free and allows 10 requests per second per account. Do this once per company and cache the id. A watch list of 2,000 accounts is a few minutes of setup, not a recurring cost.
2. Search their active posts since the last run (2 credits)
POST /api/{account_id}/datastorage/job_search/
{
"organization_ids": [123, 456, 789],
"is_active": true,
"date_featured_start": "2026-10-28",
"page": 1
}
Set date_featured_start to the date of your previous run, so each run asks for less. title and description accept a boolean text query if you only care about a team, for example "data engineer" OR "analytics engineer". Add job_source to restrict to one board, and locations or locations_exclude (location ids from the locations autocomplete) to restrict by place.
The response shape, from the API reference, with fictional values:
{
"total_search_results": 4,
"search_results": [
{
"id": 48213,
"source": "indeed",
"source_id": "4071234567",
"date_featured": "2026-10-28",
"title": "Senior data engineer",
"url": "https://example.com/careers/48213",
"is_active": true,
"is_remote": true,
"location": { "id": 77, "name": "Austin, TX", "osm_locations": [] },
"organization": { "id": 123, "name": "Example Corp", "domain": "example.com", "linkedin_info": {} }
}
]
}
Page with page until you have collected total_search_results rows.
3. Drop the ids you have already seen
Keep a table of post ids per company with two dates: first_seen (your run date when the id first appeared) and last_seen (the most recent run that returned it). On each run:
new_ids = result_ids - seen_ids # alert on these
still_live = result_ids & seen_ids # update last_seen
gone = seen_ids_active - result_ids # candidate expiries, see the next section
A post that is still open shows up in every run until it closes. Without this step a daily job alerts on the same role every day. With it, a role is one event, on the day it first appears.
4. Group per company, one alert per run
Group new_ids by organization.id and emit one line per company: name, count of new roles, the titles, the earliest date_featured. A rep or a sourcer reads "Example Corp opened 4 roles this week, three of them data engineering" and acts. Forty separate alerts for forty posts is noise.
Handle the empty case as no signal, not an error: an empty search_results means nothing new matched. A 403 means the key is missing or wrong, so stop and fix credentials rather than retry. A 429 means back off.
Reposts and expiries
Two things distort any posting stream, and the loop above handles both if you keep your own state.
Reposts. The same role is often listed more than once across boards or re-listed after it ages out. Ficstar's September 2026 comparison of job posting data providers cites a 2021 ACM paper that quotes Textkernel: an average job ad is reposted two to five times, depending on the country (retrieved 2026-11-01). Inside Forager data, match on the post id, not on title plus company, and you will not alert twice on one id. A genuine re-listing on a second board arrives as a second id with a different source and source_id. If your product should treat those as one opening, add a second-level key of normalized title plus organization.id within a window, and label it as yours, since Forager does not do that merge for you.
Expiries. is_active tells you a post is still live, so a search with is_active: true only ever returns open roles. To see a close, keep last_seen yourself: a post you alerted on that stops appearing in is_active: true results has closed or expired. That gives you time-to-fill per role and a "hiring cooled off" signal, two things a feed of current posts cannot say on its own. Posting data is intent, not hires. Treat an open role as a company that is looking, not as a hire that happened.
Sources, labelled
Every API result carries a source field, and the search request accepts the same values in job_source. Two uses. First, a filter: if your users only trust roles the company itself posted to one network, restrict to it. Second, a label: show the source next to each post in your UI, so a user can tell a direct listing from an aggregator copy and can click through on url to check it. A source that is a documented enum, not free text, means your filter cannot silently break on a spelling change.
Cost and limits
| Call | Credits | Notes |
|---|---|---|
| Organizations autocomplete | Free | 10 requests per second per account. Resolve once, cache the id. |
| Job post event search | 2 per request | Per request, not per result. One page of results for one or many companies is one request. |
| Job post event search totals | Not yet documented | Same body, returns only total_search_results. |
Search is charged per request, so the way to keep a watch list cheap is to batch companies into one organization_ids array and narrow the window with date_featured_start. A daily run over a watch list that fits in one page costs 2 credits a day. The credit balance change log and consumption totals endpoints show you exactly what each run spent.
From signal to people
A company that just opened roles is a company to sell to or to source from, and the same ids carry over.
- Sales platforms: take
organization.idfrom the alert into a people search to find the hiring manager or the budget owner at that account. The docs recipe prospect-list walks through the role search and the contact lookups that follow. - Recruiting platforms: the titles in the alert are the roles you need candidates for. The docs recipe source-candidates covers searching people by role and reaching them on personal email or mobile.
Both paths start from the same company id you resolved in step 1, which is the reason to resolve ids once and keep them.
FAQ
What fields are in a job post record?
Title, description, URL, listing date (date_featured), active flag, remote flag, location as written plus a parsed location, salary text where stated, the source and source id, and the company resolved to a Forager id with name, domain and profile URL. The feed schema (version 2.0.0) lists every field and type.
How often are job postings refreshed?
Records carry date_updated, the date Forager last crawled them, so you can measure freshness on your own sample. For the feed, refresh terms are written into the license agreement. Delivery cadence (daily, weekly or monthly) is separate from refresh cadence.
How do I avoid alerting on a repost?
Key your seen-set on the post id. A post that is still open returns the same id every run and triggers nothing. If you also want to collapse the same role listed on two boards, add your own second key of normalized title plus company id within a date window, and label that merge as your own logic.
Should I use the API or the feed?
API when you watch a list of accounts and want to know what changed since the last run. Feed when your product needs every posting in its own index. The company ids are the same in both, so you can start on one and add the other.
What does a job search cost?
2 credits per request, however many companies are in organization_ids and however many posts come back on that page. Autocomplete is free. The totals endpoint's cost is not yet documented.
Build the signal on your own watch list
Get a free API key with $5 in credits, resolve your first ten companies with autocomplete and run one search. If your product needs the whole index, book a demo and ask about the job postings feed.
- 800M+ people and 65M+ companies
- Get $5 in free credits
- No credit card to sign up
#1 of 10 in Clay's 2026 test of mobile data providers Source: Clay