Open menu

Job Changes API: Date-Range Queries for New Roles, Promotions, and Moves

Job Changes API: Date-Range Queries for New Roles, Promotions, and Moves

The Job Changes API returns every tracked person who changed roles inside a date range, typed and timestamped. New jobs, promotions, lateral title moves, first jobs: you pick the type and the window, it returns the records.

I learned why this matters the embarrassing way. In 2023 I emailed “our champion” Anna K. at a Munich fintech a full quarter after she had left the company. The reply came from a shared inbox, asking to be removed. Nobody on our side had noticed she was gone.

So let’s make sure that never happens to you. Here’s the endpoint, the shapes, and two recipes you can run this week.

📌 TL;DR: POST https://api.cufinder.io/v2/jca with start_date and end_date (YYYY-MM-DD), a type (company_change, promotion, lateral_title_change, or no_company_to_company), and an optional page. Auth is an x-api-key header. Cost is 1 credit per request. Each record carries the person's linkedin_url, a detected_at timestamp, and from/to blocks with company and title.

What is a job changes API?

A job changes API is an endpoint that reports people who moved roles: who they were, where they went, and when it was detected. It exists because people move constantly. The Bureau of Labor Statistics puts median employee tenure at 3.9 years. And contact records age even faster than tenure suggests: data degradation is well documented, and every unnoticed move is one more silently wrong row. Your CRM is quietly rotting either way. The only question is whether you find out before or after you hit send.

One distinction before we go further, because the names blur. Hiring-signals feeds watch JOB ADS: postings a company publishes about roles it wants to fill (we cover that motion in the hiring-signals workflow). A job changes API watches PEOPLE: actual humans whose roles changed. Ads signal intent to grow. Moves signal a person you know, or should know, in a new seat.

“Track when people change roles, new jobs, promotions, and title moves, across any date range.”

CUFinder Job Changes reference

And why do sellers care so much? Because a mover is the warmest cold contact that exists. Someone who bought from you once and just landed somewhere new is a pipeline event, not a database row. The strategy side of that argument lives in our buying signals guide; this page stays technical.

The manual ways first (honestly)

You do not need an API to track a dozen champions. Sales Navigator raises job-change alerts on saved leads, and the network feed surfaces “congratulate Anna on the new role” prompts. Even a monthly 30-minute pass through a spreadsheet of your top 25 relationships genuinely works. I ran exactly that ritual for two years.

But the manual methods share three limits. There’s no export: alerts arrive as notifications, not as data your CRM can act on. No structure either: you get “new role!” but not a typed from/to record. And there’s no history: you can’t ask “who moved last quarter?” after the fact. Past 50 tracked people, or past one seat, the ritual stops scaling. That’s the honest line where an endpoint earns its place.

The request: two dates and a type

The request body is four fields, three required. Dates use the ISO 8601 YYYY-MM-DD format, and authentication is the x-api-key header used across all CUFinder endpoints.

ParameterTypeRequiredMeaning
start_datestringYesStart of the range, YYYY-MM-DD
end_datestringYesEnd of the range, YYYY-MM-DD
typestringYesWhich kind of change to return (four values, below)
pageintegerNoPage number for paginated results

A complete call, using the documented example range:

curl -X POST https://api.cufinder.io/v2/jca \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"start_date": "2026-05-15", "end_date": "2026-08-15", "type": "promotion", "page": 1}'

One credit per request. Not per record, per request, which makes wide ranges surprisingly cheap to explore.

The four change types and what each is for

So which of the four types should you pull first? Here’s the enum exactly as the docs define it, with the play each one feeds:

TypeMeaningThe play
company_changePerson moved from company A to company BChampion tracking and new-buyer alerts
promotionSame company, seniority or title jumped upA known contact just gained authority
lateral_title_changeSame company, title changed, no seniority jumpRole scope shifted; re-map the account
no_company_to_companyPerson had no company and now has oneFirst jobs and returns to the workforce

company_change is the famous one: it powers every “your champion moved” motion, because a past buyer landing in a new seat is the single best account-entry point most teams own. But don’t sleep on promotion. When a manager you already know becomes the director who owns the budget, the relationship didn’t move companies. The authority did.

If you’re starting from zero, start with company_change against people you already know. It has the shortest path from record to reply, and it teaches you the data’s rhythms before you widen out.

lateral_title_change is quieter: same seniority, new scope. It’s how you notice the “Head of Sales Ops” who is now “Head of RevOps” and update who owns which conversation. And no_company_to_company catches people entering or re-entering the workforce, useful for recruiting motions and for relationship mapping at the edges of your network.

The response: from and to blocks

That’s the shape of the question. Here’s the shape of the answer, straight from the documented example (trimmed to one record, field names untouched):

{
  "status": 1,
  "data": {
    "confidence_level": 97,
    "query": { "start_date": "2026-05-15", "end_date": "2026-08-15", "type": "promotion", "page": 1 },
    "job_changes": [
      {
        "type": "promotion",
        "linkedin_url": "linkedin.com/in/sabmartinez",
        "detected_at": "2026-08-14T23:59:32.286Z",
        "from": {
          "company_linkedin_url": "linkedin.com/company/ryan",
          "company_linkedin_id": "11070",
          "company_name": "ryan",
          "title": "property tax consultant"
        },
        "to": {
          "company_linkedin_url": "linkedin.com/company/ryan",
          "company_linkedin_id": "11070",
          "company_name": "ryan",
          "title": "senior consultant property tax"
        }
      }
    ],
    "credit_count": 4694
  }
}

Three reading notes, learned the debugging way. First, detected_at is the detection timestamp: when the change was observed, not the person’s official start date. Someone who updates their profile late is detected late, and honest pipelines treat detected_at accordingly. Second, company_linkedin_id is your stable join key; match on it, not on company_name, when you compare against your account list. Third, titles arrive lowercase (you can see it in the example above). Normalize before you display, and you’ll save yourself a confused hour.

Date-range patterns that actually work

Quarterly manual checks miss people for months. A weekly overlapped sweep doesn’t. After a year of running this endpoint on a schedule, four patterns cover almost everything:

  • The rolling weekly sweep. A Monday cron job queries the last 8 days, not the last 7. Overlap the window by a day and dedupe, and boundary moves can’t slip through.
  • Monthly slices for backfills. Onboarding a new champions list? Walk backwards one 30-day window at a time, paginating each slice until the page comes back empty.
  • The quarter window. One 90-day pull per territory during planning season answers “who moved into or out of my patch?” in a single request per page.
  • Freshness triage. Sort by detected_at and work the newest first. The fresher the detection, the warmer the congratulations.
💡 Overlap and dedupe: query 8 days every 7 days, then dedupe on linkedin_url + detected_at. Exactly abutting ranges look tidy in code and drop the person who moved at 23:59.

If you’re wondering how long a detection stays meaningful, the signals docs’ detection timeframes page is the deeper read. Shorter windows usually mean warmer sends. One more habit worth stealing: write each sweep’s window and result count to a small state file. When a run fails, you’ll know exactly which days to re-query instead of guessing.

Recipe 1: the weekly champion sweep

The highest-value 40 lines of Python in most sales stacks. Keep a set of the linkedin_url values for every champion (past buyers, power users, warm relationships). Once a week, pull company_change and intersect:

import datetime, requests

API = "https://api.cufinder.io/v2/jca"
HEADERS = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}

champions = {line.strip().lower() for line in open("champions.txt") if line.strip()}

end = datetime.date.today()
start = end - datetime.timedelta(days=8)   # 8 days: overlap by one, dedupe later

alerts, page = [], 1
while True:
    body = {"start_date": str(start), "end_date": str(end),
            "type": "company_change", "page": page}
    data = requests.post(API, headers=HEADERS, json=body, timeout=30).json()["data"]
    changes = data.get("job_changes") or []
    if not changes:
        break
    for r in changes:
        if r["linkedin_url"].lower() in champions:
            alerts.append((r["linkedin_url"], r["to"]["company_name"],
                           r["to"]["title"], r["detected_at"]))
    page += 1

for url, company, title, seen in sorted(alerts, key=lambda a: a[3], reverse=True):
    print(f"{url} is now {title} at {company} (detected {seen[:10]})")

Each hit is a person who already trusts you, sitting in a new company. Next step: refresh their contact record through the Person Enrichment reference so the congratulations note lands at the new employer, not the old inbox. That last sentence is the entire Anna K. lesson.

Recipe 2: territory change alerts

Same sweep, different set. Instead of matching people you know, match companies you own. Build a set of company_linkedin_id values for your accounts, then flag any record where the to block (or the from block) lands in it.

Run it for company_change and you catch new decision-makers arriving in your patch: a new-buyer alert with zero list-building. Switch to promotion and you catch buying committees shifting inside open opportunities, which is exactly when a stalled deal changes owners. Route each alert to the account owner with the from/to context attached, because “she was at your competitor last month” changes how you open.

And keep the note itself human. Not “I saw you changed jobs, do you have 15 minutes?” More like: “Congrats on the new role. RevOps at a 400-person company is a different sport. No ask, just glad to see it.” Then wait. The reply rate on patience is embarrassingly good.

🧠 On the first send: never pitch the day the alert fires. Congratulate first, reference what their new role owns second, and ask for nothing until send two.

Job Changes API vs People Signals API

Movement-first or signal-first: that’s the whole split. jca asks “who moved in this window?” and returns typed from/to records. Its sibling, documented in the People Signals reference, asks “who works at companies where signal X just fired?” and filters by signal_name, time_frame, and bucket instead of dates.

They overlap at the edges, because the wider signal graph also watches moves (employee_joined and internal_promotion are signals in their own right). So here’s the practical rule: use jca when the PERSON is your unit of work, and the signal endpoints when the ACCOUNT is. Our buying signals API overview walks all four endpoints if you want the full map.

41 champions, 9 moves, 1 deal

In 2025 I finally did the thing I tell everyone to do. I exported 41 champions from our closed-won deals into a text file and ran the sweep quarterly, then weekly. The first quarterly pass found 9 people who had moved. Nine! Three had landed at companies already on our target list.

→ 41 champions → 9 moves found → 3 in-ICP landings → 1 deal closed in 6 weeks. Those are my numbers from one list, not a benchmark, but the first meeting from that sweep remains the easiest one I have ever booked. The opener wrote itself.

Honesty requires the other half too. Two of the nine moves were detected weeks after the fact, because the people updated their profiles late. That is the detected_at caveat, lived. Budget-wise the whole ritual costs a handful of credits per quarter: 1 credit per request, pagination included.

How this page was put together

Every parameter, the four-value type enum, the credit cost, and both JSON snippets were checked against the live Job Changes reference on August 22, 2026. The response is the documented example, trimmed with field names unchanged. Recipe numbers come from my own runs and are labeled as such. Tenure and decay claims carry their sources. And the limitation, stated plainly: detection follows public profile updates, so people who update late are detected late. Build your expectations, and your dedupe, around that.

FAQ

What is a job changes API?

An endpoint that returns people who changed roles within a date range, as structured records. Each result is typed (new company, promotion, lateral move, or first job) and carries the person’s profile URL plus from/to company and title blocks.

What is the difference between a job changes API and a jobs API?

A jobs API returns job postings: ads for open roles. Job changes APIs return people movement instead: actual humans who switched roles. Postings signal a company’s intent to grow; changes tell you a specific person landed somewhere new.

What does no_company_to_company mean?

It returns people who previously had no company on their profile and now have one: first jobs, returns from career breaks, founders going back into employment. Recruiting and relationship-mapping teams use it more than sales teams do.

How far back can a query go?

The endpoint accepts any YYYY-MM-DD range, so you choose the depth. For long backfills the working pattern is monthly slices, paginating each window until it returns an empty page, rather than one giant multi-year range.

How many credits does a request cost?

One credit per request, the cheapest call in the signals family. Pagination counts as one request per page, so a sweep that pulls four pages costs four credits.

Can I filter by company, title, or person?

Not in the request: the parameters are start_date, end_date, type, and page. Filtering happens client-side, and the records make it easy: match your people on linkedin_url and your accounts on company_linkedin_id.

Why do job changes matter for sales teams?

Because buyers move every few years (BLS puts median tenure at 3.9), and a past buyer in a new seat is the warmest entry point there is. Job-change data turns those moments from luck into a schedule.

Start with your closed-won list

Don’t build the platform version first. Export the champions from your closed-won deals into one text file. Run the sweep for the last 90 days. Read what comes back before you automate anything.

Because somewhere in that list, someone has probably already moved. And the note you send them this week, congratulations first, ask nothing, is the easiest pipeline you will create all quarter. Who turned up in yours? I’d genuinely love to hear.

How would you rate this article?
Bad
Okay
Good
Amazing
Comments (0)
Comments (0)
98% accuracy, GDPR & CCPA ready

Prefer to Explore on Your Own?

Skip the call and start free: 15 credits, no credit card required. Upgrade or talk to us whenever you’re ready.

Free plan available · 50 credits/month · no credit card required