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

Migrate from TheirStack

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

View .md

TheirStack and BetterJobs share a filter style: field names with suffixes such as _or and _not. Most queries translate line by line. The bigger changes are billing (re-reads are free) and the response shape (one canonical job with sources[]).

TheirStack is also one of the six providers behind BetterJobs. You can keep it in the mix and add the others with the same request.

TheirStack

theirstack

Global job postings, technographics inferred from job text, buying intent and firmographics.

Coverage
Global job postings; no person data
Discovery
73% of jobs discovered the same day, 91% by the end of the next day
Credits
1 API credit per job, 3 per company
Re-fetch
Re-fetching the same job is billed again (filter discovered_at_gte)

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

Facts per TheirStack docs.

Topic TheirStack (per TheirStack docs) BetterJobs
Sources TheirStack’s own job corpus BetterJobs index plus up to six providers, TheirStack included, merged
Filter grammar _or, _not, _gte / _lte, _max_age_days _or, _not, _gte on salary_min_gte, plus posted_within_days. See Filters
Job price 1 API credit per job 1 credit per unique job
Fetching the same job again Billed again; filter on discovered_at_gte to avoid it Free. Jobs you already paid for return at no charge
Company data 3 credits per company; technographics and firmographics GET /v1/companies/{domain}: 1 credit, hiring profile only (is_hiring, hiring_pulse)
Webhooks job.new, job.closed, company.new job.opened, job.reposted, job.closed, job.updated, company.hiring_started, company.hiring_stopped. See Events
Unknown filter names — 400 unknown_filter, never ignored

Your TheirStack job search body (illustrative; built from the field names and suffix grammar in TheirStack docs, so check exact names against your code):

{
"job_title_or": ["Head of RevOps", "Head of Revenue Operations"],
"job_title_not": ["Intern"],
"company_domain_or": ["acme-robotics.example", "northwind.example"],
"remote": true,
"discovered_at_gte": "2026-10-04T00:00:00Z",
"limit": 25
}

The same search on BetterJobs:

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": ["Head of RevOps", "Head of Revenue Operations"],
"title_not": ["Intern"],
"company_domain_or": ["acme-robotics.example", "northwind.example"],
"remote": true,
"posted_within_days": 7
},
"waterfall": { "strategy": "cheapest_first", "max_credits": 25 },
"limit": 25
}'
TheirStack pattern BetterJobs filter Note
job_title + _or title_or Sending job_title_or returns 400 unknown_filter with a “Did you mean” hint.
job_title + _not title_not
company_domain + _or company_domain_or
seniority + _or seniority_or BetterJobs values: see Taxonomies.
remote remote null or omitted = any. false = on-site or hybrid only.
min_annual_salary_usd + _gte salary_min_gte Compared in the job’s own currency, yearly. Not converted to USD.
discovered_at_gte, ..._max_age_days posted_within_days Relative days, counted from first_seen_at. No absolute date filter in v1 preview.
_lte on any field none Filter client-side for now.
filters on technographics or firmographics none Not in BetterJobs.

Everything goes inside a filters object. Routing, budget and paging sit next to it: waterfall, limit, cursor, dry_run.

Field names in TheirStack 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.
BetterJobsTheirStack
titlejob_title
company.domaincompany_domain
location.remoteremote
seniorityseniority
salarysalary_string
salary.minmin_annual_salary_usd
posted_atdate_posted
first_seen_atdiscovered_at
sources[].urlurl
sources[].first_seen_atdiscovered_at

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

TheirStack returns one record per job it found. BetterJobs returns one canonical job per real opening, merged from every source that saw it:

{
"id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB",
"title": "Head of Revenue Operations",
"company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" },
"posted_at": "2026-10-08T00:00:00Z",
"first_seen_at": "2026-10-08T06:40:00Z",
"salary": { "min": 110000, "max": 135000, "currency": "EUR", "period": "year", "origin": "declared" },
"sources": [
{ "provider": "betterjobs", "provider_job_id": "bj_idx_5521907", "fields": ["title", "description", "apply_url", "location", "employment_type", "posted_at"] },
{ "provider": "theirstack", "provider_job_id": "ts_88213377", "url": "https://jobs.acme-robotics.example/revops-lead", "fields": ["salary", "seniority"] }
]
}

Illustrative, trimmed. When TheirStack saw the job, its record is listed in sources[] with provider: "theirstack" and the fields it contributed.

  1. Re-reads are free. Per TheirStack docs, fetching the same job again is billed again, which is why you filter on discovered_at_gte. On BetterJobs, a job you already paid for comes back free and is counted in metadata.jobs_already_paid. Overlapping windows cost nothing.

  2. Duplicates across providers are free. If TheirStack and another source report the same opening, you pay 1 credit for the canonical job. metadata.duplicates_merged counts the folded records.

  3. Company lookups are a different product. TheirStack charges 3 credits per company (per its docs) for company data. BetterJobs charges 1 credit per hiring profile: open jobs, is_hiring, hiring_pulse, top job families. No firmographics.

  4. Proof of charge. GET /v1/billing/ledger?job_id=... shows when each job was charged and every free re-read.

TheirStack is a partner provider: Growth includes two partner providers, Pro and above include all six. To keep TheirStack in your results, check GET /v1/providers for enabled_on_your_plan. On Scale you can bring your own provider keys. Plans are on Credits and billing.

  1. Base URL, auth and version header. https://api.betterjobs.cc/v1, Authorization: Bearer bj_live_..., BetterJobs-Version: 2026-10-01. See Authentication.

  2. Wrap filters. Move filters into filters and rename them with the table above.

  3. Drop the re-fetch guard. Remove discovered_at_gte bookkeeping that only existed to avoid paying twice. Use posted_within_days with a day of overlap. See Sync patterns.

  4. Key on the canonical id. Store job_... ids. Keep the TheirStack id from sources[].provider_job_id only if you need to join old rows.

  5. Rename webhook handlers. job.new becomes job.opened, job.closed stays job.closed. Add a no-op for job.reposted: it must not start outreach. company.new has no equivalent. See Detect hiring changes.

  6. Handle partial results. A provider timeout returns 200 with metadata.status: partial. Log metadata.providers.failed; do not treat it as an error.

If downstream code expects TheirStack field names, adapt each job at the edge:

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": ["Head of RevOps"], "posted_within_days": 7}, "limit": 25}' \
| jq '[.data[] | {
job_title: .title,
company_domain: .company.domain,
date_posted: .posted_at,
discovered_at: .first_seen_at,
remote: .location.remote,
seniority: .seniority,
url: ([.sources[].url | select(. != null)] | first)
}]'
  • posted_within_days uses first sighting. It filters on first_seen_at, the closest match to TheirStack’s discovered_at, not date_posted.
  • null is unknown. location.remote: null means no source said. It is not false.
  • Typos fail loudly. Unknown filter names return 400 unknown_filter instead of a wider, more expensive result.
  • Enum values differ. Check seniority_or values against Taxonomies before you copy them over.