> ## Documentation Index
> Fetch the complete documentation index at: https://docs.datamagnet.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Job Change Search

> Search job-change events — people starting a new role, leaving a company, or being promoted — filtered by company, title, location and recency.

# Job Change Search

**Cost:** `10` credits per page. A page with no matches costs `0`.

Job changes are the highest-intent signal in B2B: a new VP of Engineering buys differently in week three than in month nine, and the person who just left your customer is the reason that account is about to churn. This endpoint returns those events as a feed you can poll.

Each event says what happened (`joined`, `left`, `title_change`), when it happened, when we detected it, and who it happened to — with the person and the company already attached, so a single call gives you something you can act on without a second lookup.

## Freshness, and why staleness is ordered rather than hidden

Results are sorted by freshness. The feed opens with same-day detections; events we detected long after the fact surface as you page deeper.

Every real dated event is included — a stale one is ordered later, never dropped. Each event carries `detection_lag_days`, the number of days between the change happening and our detection of it. Lower is fresher.

<Tip>
  Poll page `1` on a schedule and stop as soon as `detection_lag_days` exceeds what you have already processed. That gives you the new events without paging through history every run.
</Tip>

## Request body

<ParamField body="event_type" type="string">
  Restrict to one kind of event. Omit to receive all three.

  | Value          | Meaning                                           |
  | -------------- | ------------------------------------------------- |
  | `joined`       | The person started a new position at the company. |
  | `left`         | The person is no longer at the company.           |
  | `title_change` | The person's title changed at the same company.   |
</ParamField>

<ParamField body="organization_ids" type="string | string[]">
  Filter to specific companies, by the `organization_id` returned on every event. Exact match, and more precise than a name search.

  Accepts an array or a comma-separated string. Up to 50 companies per request, counted across this field and `company_urls` together.

  ```json theme={null}
  "organization_ids": ["dm_org_edmab4ei56y3w6cgktcgegllpqrxdbnlt4aqk"]
  ```
</ParamField>

<ParamField body="company_urls" type="string | string[]">
  Filter to specific companies by LinkedIn company URL, or by the slug at the end of one. Use this the first time you track a company, when you do not have its `organization_id` yet — the response gives you the id to use from then on.

  ```json theme={null}
  "company_urls": ["https://www.linkedin.com/company/stripe/", "figma"]
  ```

  A company we do not yet track returns `400`. Resolving a URL is slower than passing an id, so prefer the id once you have it.
</ParamField>

<ParamField body="title" type="string">
  Word-match on the new job title. Minimum 3 characters.

  Example: `"Engineering"` matches `VP of Engineering` and `Engineering Manager`.
</ParamField>

<ParamField body="geo_city" type="string">
  Partial match on the person's current city. Minimum 3 characters.

  Example: `"Berlin"`
</ParamField>

<ParamField body="geo_country_code" type="string">
  Two-letter country code for the person's current location. Case-insensitive.

  Example: `"DE"`
</ParamField>

<Note>
  The two geo filters narrow the search but are not echoed back on the events — they filter on a location the event record does not carry. If you need the person's location, fetch their profile with `person.linkedin_url`.
</Note>

<ParamField body="days_ago" type="integer">
  Recency window on the **detection** time, in days. Range `1`–`365`. Omit it to search all events.

  Example: `7` returns everything detected in the last week, whenever the change itself happened.
</ParamField>

<ParamField body="page" type="integer" default={1}>
  Page number, 1-based. Maximum `10000`; anything higher returns `400`.
</ParamField>

<ParamField body="limit" type="integer" default={25}>
  Events per page. Range `1`–`50`.
</ParamField>

<Note>
  This endpoint also accepts `GET` with the same attributes as query parameters, e.g.
  `GET /api/v1/job-changes/search?event_type=joined&days_ago=7&title=Engineering`.

  In a query string, list fields are comma-separated: `?organization_ids=dm_org_abc,dm_org_def`.
</Note>

***

## Company filter options

Two ways to name a company — use whichever you have:

| Field              | What you provide                              | Cost                                                    |
| ------------------ | --------------------------------------------- | ------------------------------------------------------- |
| `organization_ids` | An `organization_id` from a previous response | Fastest; no extra work                                  |
| `company_urls`     | A LinkedIn company URL or slug                | One resolution step the first time each company is seen |

Both can be combined in the same request. A company named by both counts once.

***

## Working with ids

Every event carries two Datamagnet ids:

* `person.profile_id` — `dm_prsn_…`
* `organization.organization_id` — `dm_org_…`

They are stable: the same person and the same company are the same id in every response, for ever, so they work as keys in your own database. `organization_id` is Datamagnet's own identifier for that company — the same one our company records are keyed by — so an id you store today keeps working as we add more about that company.

To pull the full record behind an event, use the LinkedIn URL that comes with it:

* `person.linkedin_url` → [Person](/api-reference/endpoints/people)
* `organization.linkedin_url` → [Company](/api-reference/endpoints/company)

***

## Response

<RequestExample>
  ```json Request theme={null}
  {
    "event_type": "joined",
    "title": "Engineering",
    "geo_country_code": "DE",
    "days_ago": 7,
    "page": 1,
    "limit": 25
  }
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true,
    "message": "Job change events retrieved successfully",
    "data": [
      {
        "event_type": "title_change",
        "detected_at": "2026-05-01T17:20:04.389921Z",
        "changed_at": "2026-05-01T00:00:00Z",
        "detection_lag_days": 0,
        "title": {
          "current": "Data Engineer",
          "previous": "Data Analyst"
        },
        "person": {
          "profile_id": "dm_prsn_tnd66ysw4lmt4yighsalfnmf2tcanlpmp3exh6q",
          "handle": "lecomte-quentin",
          "full_name": "Quentin Lecomte",
          "first_name": "Quentin",
          "last_name": "Lecomte",
          "headline": "Data Engineer @ Aktivco | Camusat Group",
          "linkedin_url": "https://linkedin.com/in/lecomte-quentin"
        },
        "organization": {
          "organization_id": "dm_org_33u32yb3sh56d6jxiw3lzljselgver2p4ons6",
          "name": "AktivCo",
          "slug": "activco",
          "linkedin_url": "https://www.linkedin.com/company/activco"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 25,
      "has_more": true
    },
    "credit_used": 10
  }
  ```
</ResponseExample>

Every event returns the same set of keys. A field an event does not have comes back as `null` rather than being omitted, so your parsing code never has to branch.

### Dates, and the two of them

`changed_at` is when the change took effect. `detected_at` is when we saw it. `detection_lag_days` is the gap, in days.

`title.previous` is only meaningful on `title_change` and `left`; a `joined` event has no title to have replaced, so it comes back `null`.

### Pagination

`pagination.has_more` tells you whether another page exists. Keep incrementing `page` while it is `true`, up to the `10000` page cap.

An empty `data` array with `has_more: false` means you have reached the end of the feed for those filters — and costs `0` credits.

***

## Errors

| Status | Meaning                                                                                                                                                                                                      | What to do                                                                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | A filter was invalid — an unknown `event_type`, a text filter under 3 characters, `days_ago` outside 1–365, `limit` above 50, a page above 10000, or a company we do not track. `message` names the problem. | Fix the request. Nothing is charged.                                                                                                                              |
| `401`  | Missing or invalid API key.                                                                                                                                                                                  | Send your key as `Authorization: Bearer YOUR_API_KEY` — see [Authentication](/api-reference/authentication).                                                      |
| `403`  | Your plan does not include job change data.                                                                                                                                                                  | Contact us at [signals@datamagnet.co](mailto:signals@datamagnet.co).                                                                                              |
| `429`  | Rate limit for your plan exceeded.                                                                                                                                                                           | Back off and retry; see the `X-RateLimit-*` response headers.                                                                                                     |
| `503`  | Job change search is temporarily unavailable — upstream capacity, not a fault in your request.                                                                                                               | Retry after the number of seconds in the `Retry-After` header: `60` when the search is being rate limited, longer when capacity is exhausted. Nothing is charged. |

Nothing is charged on any error.
