Fiber AI
Search & discovery

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

  1. Filter postings by company, title, status, posted date, applicants, function, industry, pay, experience, modality, employment type, seniority, and country/region.
  2. Count first with the job-posting count: one credit, totalJobsFound, no posting payload.
  3. Page through matches with job-posting search. You pay per posting returned.
  4. Read structured fields on each hit: description, salary range, location, modality, applicant range, and the posting URL.

Operations

JobOperationHTTPWhen to use
CountjobPostingSearchCountPOST /v1/job-search/countIterate filters; "how many?" before pulling rows
SearchjobPostingSearchPOST /v1/job-searchFetch 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

CountSearch
Credits1 per request (flat)1 per posting returned (default)
PayloadtotalJobsFounddata[] plus nextCursor
PaginationNonecursor in, nextCursor out
Empty matchesStill 1 credit0 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).

FilterShapeWhat it does
companiesDiscriminated unionJobs at specific companies. One identifier type per request — do not mix slugs with domains in the same object
titlestring[]Partial match on job titles (e.g. ["Software Engineer", "Manager"])
isActive"true" | "false" | "no_preference"Active only, closed only, or both
postedAtDate selectionWhen the role was posted (absolute range or relative window)
numApplicants{ lowerBound?, upperBound? }Applicant-count range
jobFunctionsenum[]Function categories (Engineering, Sales, Operations, …)
industriesenum[]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
employmentTypeenum[]Full-time, Part-time, Contract, Temporary, Internship, Volunteer, Other
seniorityLevelenum[]Entry level, Associate, Mid-Senior level, Director, Executive, Internship
countryOrRegionCodestring[]ISO 3166-1 alpha-3 countries (USA, IND, GBR) and/or region codes (X-APAC, X-LATAM, X-ANGLOSPHERE, X-EMEA, …)
jobLocationTypesame as modalityDeprecated. 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:

identifiervalue 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 title arrays plus a large pageSize is how spend spikes.
  • Prefer jobModality over jobLocationType.
  • 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.
  • chargeInfo is authoritative — published rates can differ per org.

Use cases

  • GTM / AI SDR: companies actively hiring for a role your product supports (the /search catalog 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 searchParams on 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

OperationDefault credits
jobPostingSearch1 per posting returned
jobPostingSearchCount1 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

On this page