Skip to content
API v1 preview — endpoints and fields may change before general availability.

Migrate from JobsPipe

How do I translate my JobsPipe queries and fields to BetterJobs?

View .md

JobsPipe and BetterJobs look alike from the outside: both search jobs with POST /v1/jobs/search and bill per job. The differences are inside: BetterJobs fans the search out to JobsPipe and up to six other sources, merges the results into canonical jobs, and never bills the same job twice.

JobsPipe is one of the six providers behind BetterJobs, so its postings can still reach you through the waterfall.

JobsPipe

jobspipe

Normalized job postings from 30+ sources with 12 months of history.

Sources
30+ sources incl. Greenhouse, Lever, Ashby, Workday, Indeed, LinkedIn
History
12 months
Freshness
Under 6h on Builder, under 1h on Scale
Credits
1 credit per job; one credit buys a job for the rest of the calendar month

Growth includes 2 partner providers; Pro and above include all six. GET /v1/providers shows what your plan enables.

Facts per JobsPipe docs.

Topic JobsPipe (per JobsPipe docs) BetterJobs
Search endpoint POST /v1/jobs/search POST /v1/jobs/search on https://api.betterjobs.cc
Sources 30+ sources incl. Greenhouse, Lever, Ashby, Workday, Indeed, LinkedIn BetterJobs index plus up to six providers, JobsPipe included, merged
Job price 1 credit per job 1 credit per unique job
Paying again One credit buys a job for the rest of the calendar month A job you already paid for returns free; the ledger shows already_paid
Ghost postings ghost_score p_real: probability the job is real, from cross-source corroboration
Taxonomies ISCO-08, ISIC, ESCO job_family strings and a fixed seniority enum. No ISCO, ISIC or ESCO codes in v1 preview
History 12 months posted_within_days up to 365, include_closed: true for closed jobs
Freshness Under 6h on Builder, under 1h on Scale Published at GA. Each job carries first_seen_at, last_seen_at, last_verified_at

The path is the same. Change the base URL, the auth header and the body: BetterJobs puts every filter inside one filters object and routing next to it in waterfall.

Before: POST <JobsPipe base URL>/v1/jobs/search + your JobsPipe filters and key
After: POST https://api.betterjobs.cc/v1/jobs/search
Authorization: Bearer bj_live_...
BetterJobs-Version: 2026-10-01
{ "filters": { ... }, "waterfall": { ... }, "limit": 100 }
Terminal window
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": {
"title_or": ["Senior Data Engineer", "Staff Data Engineer"],
"country_code_or": ["NL", "DE"],
"seniority_or": ["senior", "lead"],
"remote": true,
"posted_within_days": 14
},
"waterfall": { "strategy": "cheapest_first", "max_credits": 100 },
"limit": 100
}'

Rewrite each JobsPipe filter you use as one of the BetterJobs filters on Filters. A filter name BetterJobs does not know returns 400 unknown_filter with the name in error.param, so a missed rename fails on the first call instead of returning a wider result.

Every BetterJobs search response states its cost in metadata.credits_charged, plus the X-Credits-Charged and X-Credits-Remaining headers.

Field names in JobsPipe responses and their BetterJobs equivalents. Rows show only fields with a confident one-to-one match.

— = no confident one-to-one equivalent in that provider's published field names.
BetterJobsJobsPipe
titlejob_title
seniorityseniority
salarysalary_usd
posted_atdate_posted
first_seen_atdiscovered_at
last_seen_atlast_seen_at
last_verified_atverified_at
closed_reasonclosed_reason
sources[].urlurl
sources[].first_seen_atdiscovered_at
sources[].last_seen_atlast_seen_at

Every BetterJobs field, with type and null meaning, is in the field dictionary.

  1. Charged once, not once a month. Per JobsPipe docs, one credit buys a job for the rest of the calendar month. BetterJobs charges 1 credit the first time a job reaches you; later reads of that job in searches or GET /v1/jobs/{id} are free.

  2. Duplicates across sources are free. If JobsPipe and another source report the same opening, you pay for one canonical job. metadata.duplicates_merged counts the folded records.

  3. Empty pages and estimates are free. dry_run: true returns expected_unique_jobs_range and credits_range without fetching.

  4. Proof of charge. GET /v1/billing/ledger?job_id=... lists the original charge and every free re-read.

JobsPipe is a partner provider: Growth includes two partner providers, Pro and above include all six. GET /v1/providers shows whether your plan enables jobspipe. On Scale you can bring your own provider keys. Plans are on Credits and billing.

  1. Base URL, key and version header. See Authentication.

  2. Wrap filters. Move them into filters and rename them to BetterJobs names.

  3. Rename response fields. Use the table above, or the adapter below at the edge of your code.

  4. Replace taxonomy codes. Map your ISCO-08 or ESCO lists to job_family_or and seniority_or values. See Taxonomies.

  5. Drop month-boundary logic. Code that avoided re-fetching across months to save credits is no longer needed.

  6. Read provenance. sources[] shows which sources saw each job; the JobsPipe record appears with provider: "jobspipe".

If downstream code reads JobsPipe names, adapt each job once:

Terminal window
curl -s 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": {"title_or": ["Senior Data Engineer"], "posted_within_days": 14}, "limit": 100}' \
| jq '[.data[] | {
job_title: .title,
date_posted: .posted_at,
discovered_at: .first_seen_at,
last_seen_at: .last_seen_at,
verified_at: .last_verified_at,
seniority: .seniority,
closed_reason: .closed_reason,
url: ([.sources[].url | select(. != null)] | first),
p_real: .p_real
}]'
  • Same path, different host. Change the base URL and the key together. A key BetterJobs does not recognize returns 401 unauthorized.
  • closed_reason values are a fixed enum. BetterJobs uses filled, expired, removed, unknown, or null while open. Map JobsPipe values you stored before.
  • Seniority values differ. Check your seniority filters against Taxonomies before copying them.
  • null is unknown. last_verified_at: null means never verified at origin, not “dead”.