Job Search
Filter 13M+ open roles by title, pay, and location, then count matches before you pull postings.
We built job search so you can treat open roles as data: titles, compensation, seniority, location, and whether the posting is still active. Use it to pull the postings themselves, or as a hiring-intent signal: companies staffing up for roles your product supports.
We cover 13M+ postings, about 1M new per week.
Search and count share the same searchParams object; count is how you
size a query before you pay for rows.
The payload looks like a listing you'd browse on a major job board: title,
company, location, pay when listed, and whether the req is still open.
job_url on a result is typically a LinkedIn job link. You don't need to
sign in anywhere.
Filter by employer the way you'd scan a careers page: Microsoft, Amazon, Google, Salesforce, Stripe — via domain, LinkedIn company URL, slug, or org ID.
This is job postings, not people changing jobs. Role-change alerts live
on Tracker (person_changed_company
and related career rules). To find
candidates from a job description, paste it into
natural-language search. US
hourly and trade listings are a separate API:
Blue-collar job search.
What it does
- Filter postings by company, title, status, posted date, applicants, function, industry, pay, experience, modality, employment type, seniority, and country/region.
- Count first with the job-posting count: one credit,
totalJobsFound, no posting payload. - Page through matches with job-posting search. You pay per posting returned.
- Read structured fields on each hit: description, salary range, location, modality, applicant range, and the posting URL.
Operations
| Job | Operation | HTTP | When to use |
|---|---|---|---|
| Count | jobPostingSearchCount | POST /v1/job-search/count | Iterate filters; "how many?" before pulling rows |
| Search | jobPostingSearch | POST /v1/job-search | Fetch posting records (pageSize 1–1000, default 25) |
Both routes allow 120 requests / minute. Count has no pageSize or
cursor.
Call functions from @fiberai/sdk. Request and response Zod schemas live on
@fiberai/sdk/zod — JobPostingSearchData ↔ zJobPostingSearchData, same
pattern for count. Parse untrusted input with those schemas; the function
import stays small if you never load /zod.
import {
jobPostingSearch,
jobPostingSearchCount,
} from "@fiberai/sdk";
import type {
JobPostingSearchCountData,
JobPostingSearchData,
} from "@fiberai/sdk";
import {
zJobPostingSearchCountData,
zJobPostingSearchData,
} from "@fiberai/sdk/zod";
const searchParams: JobPostingSearchCountData["body"]["searchParams"] = {
title: ["Account Executive"],
isActive: "true",
jobModality: ["Remote"],
countryOrRegionCode: ["USA"],
postedAt: {
strategy: "relative",
window: { method: "within", period: "day", upperBound: 30 },
},
};
const countRequest: JobPostingSearchCountData =
zJobPostingSearchCountData.parse({
body: {
apiKey: process.env.FIBER_API_KEY!,
searchParams,
},
});
const count: Awaited<ReturnType<typeof jobPostingSearchCount>> =
await jobPostingSearchCount(countRequest);
console.log("Matches:", count.data?.output.totalJobsFound);
const searchRequest: JobPostingSearchData = zJobPostingSearchData.parse({
body: {
apiKey: process.env.FIBER_API_KEY!,
pageSize: 25,
searchParams,
},
});
const page: Awaited<ReturnType<typeof jobPostingSearch>> =
await jobPostingSearch(searchRequest);
console.log("First title:", page.data?.output.data[0]?.title);
console.log("Next page:", page.data?.output.nextCursor);
console.log("Cost:", page.data?.chargeInfo);Search vs count
| Count | Search | |
|---|---|---|
| Credits | 1 per request (flat) | 1 per posting returned (default) |
| Payload | totalJobsFound | data[] plus nextCursor |
| Pagination | None | cursor in, nextCursor out |
| Empty matches | Still 1 credit | 0 credits for an empty page (unfilled pageSize is refunded) |
Job count costs 1 credit, like the company and people counts — every count is the cheap way to tighten filters before you pull descriptions.
Filters (searchParams)
Search and count take the same object. Every field is optional; {} is a
valid unfiltered query. Unknown fields are rejected (strict schema).
| Filter | Shape | What it does |
|---|---|---|
companies | Discriminated union | Jobs at specific companies. One identifier type per request — do not mix slugs with domains in the same object |
title | string[] | Partial match on job titles (e.g. ["Software Engineer", "Manager"]) |
isActive | "true" | "false" | "no_preference" | Active only, closed only, or both |
postedAt | Date selection | When the role was posted (absolute range or relative window) |
numApplicants | { lowerBound?, upperBound? } | Applicant-count range |
jobFunctions | enum[] | Function categories (Engineering, Sales, Operations, …) |
industries | enum[] | Standardized industries (Software, Healthcare, Finance, …) |
annualSalaryUsd | { lowerBound?, upperBound? } | Annual pay in USD |
yearsOfExperience | { lowerBound?, upperBound? } | Experience required |
jobModality | "On-site" | "Remote" | "Hybrid"[] | Match any listed modality |
employmentType | enum[] | Full-time, Part-time, Contract, Temporary, Internship, Volunteer, Other |
seniorityLevel | enum[] | Entry level, Associate, Mid-Senior level, Director, Executive, Internship |
countryOrRegionCode | string[] | ISO 3166-1 alpha-3 countries (USA, IND, GBR) and/or region codes (X-APAC, X-LATAM, X-ANGLOSPHERE, X-EMEA, …) |
jobLocationType | same as modality | Deprecated. Use jobModality |
Full enum members for functions, industries, and region codes are on the OpenAPI pages — send the canonical strings, not free-text synonyms.
Company identifiers
Pick one identifier; value is always an array:
identifier | value examples |
|---|---|
linkedinSlug | ["microsoft"] |
domain | ["microsoft.com"] |
linkedinUrl | ["https://www.linkedin.com/company/microsoft/"] |
linkedinOrgID | ["1035"] (numeric strings) |
{
"companies": {
"identifier": "domain",
"value": ["stripe.com"]
}
}Posted date (postedAt)
Discriminated on strategy:
Absolute — a fixed window, regardless of when you run the query:
{
"strategy": "absolute",
"range": { "lowerBound": "2026-01-01", "upperBound": "2026-03-31" }
}Relative — a sliding window from today. Prefer method: "within" with
period of day | week | month | quarter | year and at least one
of lowerBound / upperBound (counts of that period ago). method: "calendar"
with which: "current" | "previous" pins to this or last calendar period.
{
"strategy": "relative",
"window": { "method": "within", "period": "day", "upperBound": 14 }
}What a posting contains
Each item in data includes job_id, title, company_name,
company_logo_url, posted_at, job_url, description, status
(active | closed), modality, seniority_level, employment_type,
job_function, standard_industries, applicant and experience ranges,
compensation_range (currency + hr / m / yr / daily), and
annual_salary_usd. Location is standardized_location (city, region,
country, coordinates).
job_location_type on the response is deprecated; read modality.
Closed postings aren't deleted. When a role closes, its status flips to
closed and it stays searchable, so filter with isActive: "true" for open
roles only, or "no_preference" to see the history too.
Pagination
Pass nextCursor from a response back as cursor. Treat the token as opaque
— do not construct or edit it. null means no further page. A bad cursor
returns 400 (Invalid cursor value.) and refunds the hold.
The cursor marks a position in the result order; it doesn't remember your
filters. Send the same searchParams with every page, and if you change a
filter, start again without a cursor.
Using it effectively
- Count, then search. Broad
titlearrays plus a largepageSizeis how spend spikes. - Prefer
jobModalityoverjobLocationType. - Resolve companies with typeaheads before filling
companies, so slugs and domains are ones we can match. - Honor HTTP 429 (120/min). Failed searches refund the credit hold.
chargeInfois authoritative — published rates can differ per org.
Use cases
- GTM / AI SDR: companies actively hiring for a role your product supports
(the
/searchcatalog frames this as hiring-intent, 1 credit per posting). - Recruiting intel: compensation, seniority, and remote mix on live reqs.
- Competitive staffing: titles and volume at a named competitor (
companies). - Trend checks: repeat the same
searchParamson the job-posting count over time without downloading descriptions.
In the dashboard, the same idea is a company filter: companies whose postings match titles, seniority, pay, modality, and status — see Using the UI. That finds companies; these endpoints return postings.
Trade and blue-collar listings
Hourly and trade roles are a different product. See Blue-collar job search for US hourly and trade listings.
Credits
| Operation | Default credits |
|---|---|
jobPostingSearch | 1 per posting returned |
jobPostingSearchCount | 1 per request |
See Billing & credits.
Credit amounts are defaults — custom pricing is available for bulk use cases.
Related: Blue-collar job search · Agentic search · Typeaheads · Tracker · MCP · SDKs