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

Canonical jobs

What is a canonical job, and how are duplicates from different providers merged?

View .md

A canonical job is one real opening at one company. BetterJobs builds it from every provider record that describes that opening. You get one row per opening, not one row per provider.

A posting is one listing somewhere: an ATS page, a job board ad, an aggregator copy. One opening often has many postings. The employer lists it on its careers page. Boards copy it. Two months later it is re-listed with a fresh date.

A job is the opening behind those postings. BetterJobs returns jobs. Each job lists the postings it was built from in sources[]:

{
"id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB",
"title": "Head of Revenue Operations",
"company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" },
"repost_count": 0,
"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", "fields": ["salary", "seniority"] },
{ "provider": "techmap", "provider_job_id": "tm_3f9a2c71", "fields": ["job_family"] }
]
}

Illustrative record, trimmed. The full version is in Search jobs.

Three providers saw this opening. You pay for one job.

Records are compared on the signals that identify an opening:

  • Company. Resolved to one canonical company (company.id, company.domain).
  • Title. Normalized, so “Head of RevOps” and “Head of Revenue Operations” can match.
  • Location. City, region and country.
  • Posting URLs. The origin URL and apply_url, when sources report them.
  • Provider ids. A provider’s own provider_job_id keeps its record attached to the same job across requests.

When records match, they merge. Each canonical field takes its value from the source best placed to supply it. sources[].fields tells you which source supplied which field.

When an employer re-lists the same opening, it stays the same canonical job. repost_count goes up. id does not change.

  • A watch delivers job.reposted, not job.opened. Do not re-trigger outbound on it. See Events.
  • job.reposted is free. Only job.opened costs a credit.
  • A high repost_count is a signal worth reading: the role may be hard to fill, or the listing may be evergreen.

id (job_...) is stable across sources and requests. The same opening returns the same id from tomorrow’s search, from a different strategy, and from GET /v1/jobs/{id}.

Use it as your primary key:

  • Upsert on id in your database or CRM. See Sync patterns.
  • A job you already paid for is free on every later read. metadata.jobs_already_paid counts them. See Credits and billing.
  • company.id (cmp_...) works the same way for companies.

Do not key on sources[].provider_job_id or a posting URL. Those belong to one posting, and a job can gain or lose postings over time.

Nothing. metadata.duplicates_merged counts provider records folded into canonical jobs. They are free.