---
title: "Create Job Change Watchlist signal"
method: POST
path: "/api/v1/signals/job-change-watchlist"
tags: ["Signals"]
---

# Create Job Change Watchlist signal

`POST /api/v1/signals/job-change-watchlist`

## Request

- Base URL: `https://api.rodz.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.rodz.io/api/v1/signals/job-change-watchlist`
- Auth: HTTP bearer

Watch a fixed list of people and get a lead the moment one of them changes job - reach your champion in their new seat while they still have a mandate to change things.

Where the Job Changes signal DISCOVERS movers matching a persona, this one starts from your list: your champions, your buyers, your lapsed prospects.

**Input:** `profiles` - the people to watch. Each entry is either a LinkedIn profile URL on its own, or an object `{url, currentTitle, currentCompany}` carrying the role you already have on file; both forms can be mixed. **Send the role when you have it.** Every check is billed, the first one included. With a role on file, the first check compares against it and reports any change straight away. With a bare URL, the first check can only report a move made in the last 6 months (it compares the current job with the one before it); anyone who moved earlier stays invisible - recorded as they stand today - and a later move is reported at the next check - one `cadenceHours` later, a week by default. A bare URL with `cadenceHours: 0` therefore only ever reports moves from the last 6 months. Then `detectionMode` (`company` - default - fires only on an employer change; `title` on a new role at the same employer; `both`), `cadenceHours` (how often each person is re-checked: 24, 168 - the default - 360, 720, 1080, 1440 or 2160; `0` checks the list once and never again), `addedProfilesLookback` (`first_run` - default - gives a bare URL added later the same 6-month look-back as the initial list; `recent_only` only records where they stand and reports moves from the next check on), plus enrichment flags (`getEmails`, `getPhones`).

**Output:** the create response carries a `watchlist` object telling you how many profiles are actually being watched and how many were rejected. From then on, each confirmed change is delivered to your `webhookUrl` as a person-lead: `signal` carries `previous_title` / `previous_company` alongside `new_title` / `new_company`, `new_job_started_on` (`"MM/YYYY"`, the date to test a change against - `previous_job_ended_on` differs whenever there was a gap between the two jobs), `new_job_start_month` (the same month as `"YYYY-MM"`, sortable) and `change_type` (`company`, `title` or `both`), while `contact` and `company` describe the person in their NEW role. The `previous_*` / `new_*` / `change_type` keys are a stable contract: present on every change, never renamed or null, `""` when a value is unknown - safe to map straight into your CRM. A change is delivered once, and the new role becomes the baseline for the next check.

`profiles` is consumed into the watchlist, so it is NOT echoed back in the `config` of this or any later response — the watched list has exactly one home. Read it with `GET /api/v1/watchlists/{signalId}/profiles`, and add or remove people with `POST` / `DELETE` on the same path, without recreating the signal.

**Use cases:**
- Follow your champions - when someone who already bought from you moves to a new company, you get a warm intro into an account with a decision-maker who has used your product before.
- Protect renewals - when your day-to-day contact leaves an account, you learn the day it happens instead of at renewal, in time to build a relationship with their replacement.
- Re-time a dead deal - a prospect who said no because of budget or an incumbent tool arrives somewhere new with a fresh stack and a mandate to change things; the objection left with the old job.

## Request body

- object
  - `name` string, required: Name for this signal
  - `webhookUrl` string, nullable: Where each lead is POSTed as it is produced. Optional (since 2026-09-23): omit it when you read leads with GET /api/v1/signals/{signalId}/leads or route them to a connected CRM or sequencer with `destinations` on PATCH /api/v1/signals/{signalId}.
  - `config` JobChangeWatchlistConfig, required: Config for job_change_watchlist — the PUBLIC API shape. Company sibling of ``JobOffersWatchlistConfig``: that one watches a list of COMPANIES for new postings, this one watches a list of PEOPLE for job changes. The in-app signal takes its people from a CSV upload or a CRM list, neither of which REST can express, so the API accepts them inline as `profiles`. Everything else mirrors WatchlistJobChangeConfig (app/schemas/watchlist_configs.py); the create endpoint writes the list into watchlist_profile and stores the rest on the signal.
    - `sourceType` 'csv' | 'hubspot_list' | 'pipedrive_filter' | 'hubspot_company_list' | 'pipedrive_org_filter', nullable: Where the watched list comes from. Omit it to use the list you send inline in this request. Set it to read a list that already exists in your connected CRM instead — the connection itself is made in the dashboard. Call `GET /api/v1/watchlists/sources` for the lists available on your account.
    - `sourceListId` string, nullable: Id of the CRM list or filter to read, as returned by `GET /api/v1/watchlists/sources`. Required when `sourceType` is set, ignored otherwise.
    - `sourceDynamicSync` boolean: Keep the watched list in step with the CRM list: entries added there start being watched, entries removed stop. Set false to take a one-off copy at creation and leave it frozen. Has no effect without `sourceType`.
    - `getEmails` boolean: Enable email enrichment (0.5 credit/lead, charged only on success)
    - `getPhones` boolean: Enable phone enrichment (5 credits/lead, charged only on success)
    - `getSireneData` boolean: Enrich with French SIRENE registry data (1 credit/lead, charged only on success)
    - `detectTechnologies` boolean: Detect technologies used on the company's public website (1 credit/lead, charged only on success)
    - `getHomepageContent` boolean: Fetch the company's homepage content (1 credit/lead, charged only on success)
    - `generateAiEmails` boolean: Generate AI-predicted emails
    - `profiles` union[]: The people to watch. Each entry is either a LinkedIn profile URL on its own (`"https://www.linkedin.com/in/marie-dupont"`), or an object `{url, currentTitle, currentCompany, currentCompanyUrl}` carrying the role you already have on file. Both forms can be mixed. Send the role when you have it. Every check is billed, the first one included. With a role on file, the first check compares against it and reports any change straight away. With a bare URL, the first check can only report a move made in the last 6 months (it compares the current job with the one before it); anyone who moved earlier stays invisible, recorded as they stand today, and a later move is reported at the next check, one `cadenceHours` later. Country subdomains and tracking query strings are accepted and normalized; anything that is not a personal profile URL is rejected and reported in `profilesRejected`.
      - union
        - string
        - WatchedProfileSeed: A person to watch, with the role you already have on file for them. `{url, currentTitle, currentCompany, currentCompanyUrl}` — only `url` is required. Send the role whenever your CRM has it: it is the comparison point for the FIRST check, so a move made at any time since you recorded that role is reported straight away. With a bare URL the first check can only report a move made in the last 6 months; anyone who moved earlier is recorded as they stand today and reported only on their next move.
          - `url` string, required: LinkedIn profile URL of the person to watch.
          - `currentTitle` string, nullable: Their job title as you have it on file — the comparison point.
          - `currentCompany` string, nullable: Their employer as you have it on file — the comparison point.
          - `currentCompanyUrl` string, nullable: Their employer's LinkedIn company URL, if you have it.
    - `detectionMode` 'title' | 'company' | 'both': What counts as a change. `company` (default) fires only when the person moves employer — the highest-intent event. `title` fires on an internal promotion or role change at the same employer. `both` fires on either.
    - `cadenceHours` 0 | 24 | 168 | 360 | 720 | 1080 | 1440 | 2160: How often each watched person is re-checked, in hours. 168 (weekly) is the default. `0` means check the list once and never again.
    - `addedProfilesLookback` 'first_run' | 'recent_only': What the first check of a person ADDED to the list after the signal started looks at, when you send a bare URL. `first_run` (default): the same look-back as the initial list, so a move made in the last 6 months is reported straight away. `recent_only`: the first check only records where the person stands, and a move is reported from the next check on. A person sent with their current role is compared against that role either way.
  - `dailyLeadLimit` integer: Max leads per day
  - `status` string, nullable: Initial status (default: draft).

## Response `201`

Signal created

- object: Create response for the two watchlist signals. A separate model rather than an optional field on ``PublicSignalResponse``: this spec inlines the response schema at every signal endpoint (36 of them), so carrying the extra object on the shared model documented a phantom ``watchlist`` field on 34 endpoints that never return one — and doubled the size of the published spec to say so.
  - `id` string, required
  - `name` string, required
  - `status` string, required
  - `signalTypeId` string, required
  - `webhookUrl` string, nullable
  - `config` object, nullable: Signal-specific configuration
  - `dailyLeadLimit` integer, required
  - `leadsToday` integer, required
  - `leadsTotal` integer, required
  - `duplicateHandling` string, required: Duplicate lead handling: 'refuse' (never accept), 'allow' (always accept), or 'Xd' (accept after X days)
  - `createdAt` string, required
  - `updatedAt` string, required
  - `lastLeadAt` string, nullable
  - `applied` object: Settings this signal runs with where they differ from `config`, keyed like `config`. `{}` when every stored setting is applied as stored. A `null` value means the stored field is not used by this signal. Every entry is explained by a warning naming the same field.
  - `warnings` SignalConfigWarning[]: What you should know about this configuration: values applied differently from what is stored, and filter values that are probably not what you meant. Computed from the stored configuration, so they appear on GET as well as on create and update - a signal created before these fields existed reports them too. Never an error: the signal is created or updated regardless.
    - `code` string, required: Stable identifier. `VALUE_BELOW_MINIMUM` (a value under the documented minimum is applied as the minimum), `VALUE_ADJUSTED` (a value the search cannot use as written is applied as the nearest one it can, e.g. a `publishedDate` of 14d searches 15 days), `FIELD_NOT_USED` (a stored field this signal ignores), `UNKNOWN_SIZE_NOT_ACCEPTED`, `CATEGORY_WITH_OWN_INDUSTRIES` (a category listed together with industries it already covers), `UNKNOWN_INDUSTRY` (a value matching no published industry), `UNKNOWN_FIELD` (a config key this signal does not have), `UNKNOWN_TOOL` / `UNKNOWN_CATEGORY` (a tracked tool or category name that is not in the catalog of GET /api/v1/signals/technographic-tools; on a GMB Discovery signal, a `category` that is not a Google Maps category, so the signal matches no business), `UNKNOWN_CITY` (GMB Discovery: cities in `cities` that are not in the signal's `country` — `values` lists them; when none is, the signal matches no business until `cities` or `country` is changed; never an error, nothing stored is changed), `NO_FOLLOWER_SOURCE` (a Company Followers signal with no follower extraction set up yet, so it produces nothing), `DAILY_LIMIT_PENDING` (a job signal's `dailyLeadLimit` changed less than a day after its search was last rebuilt: the search keeps its previous volume until the time in `values`, while the leads delivered per day already follow the new limit), `WATCHED_LIST_EMPTY` (a watchlist reading its list from your CRM holds no company or person — the CRM is not connected, the list could not be read, or it is empty — so it produces nothing until the list fills; cleared on its own once it does), `QUOTE_REQUIRED` (an `active` Company Followers signal in `icp` mode whose follower extraction has not been priced and accepted, so it produces nothing yet: `values[0]` says why — `not_requested`, `awaiting_acceptance` (then `values[1]` is the quoted price in credits), `expired`, `declined`, `failed`, or, after an automatic quote, `auto_quote_refused` / `auto_accept_refused` (then `values[1]` is the error code); the message names the call to make — see POST /api/v1/signals/{signal_id}/extraction/quote; cleared once a quote is accepted), `WATCHED_LIST_IMPORTING` (a company watchlist whose HubSpot list is still being read: companies are added as they arrive; `values[0]` is the number of members read so far, `values[1]` the list id; cleared once the whole list is read), `MATCHING_CONFIRMATION_REQUIRED` (a New Decision Maker or Account watchlist whose companies must be matched to their LinkedIn company page before they can be watched, at a cost above the amount we match without asking: nothing is matched or charged until you confirm with POST /api/v1/watchlists/{signalId}/companies/confirm-matching; `values[0]` is the maximum cost, `values[1]` the number of companies to match; cleared once confirmed), `DESTINATION_UNAVAILABLE` (the outreach campaign this signal sends leads to cannot receive them — not found, archived, ended or without any step — so leads are not reaching it: `values[0]` is `campaign_not_found`, `campaign_archived`, `campaign_ended` or `campaign_no_steps`; cleared once a lead reaches the campaign or the signal's destination is changed), `DESTINATION_NEEDS_CONTACT` (the signal delivers companies with no contact and a destination in `destinations` only accepts people, so none of its leads reach that destination), `DESTINATION_SKIPS_COMPANY_ONLY` (some leads are companies with no contact, e.g. no one matching your personas was found, and a destination only accepts people), `DESTINATION_NEEDS_EMAIL` (a destination only accepts leads with an email and `getEmails` is off) — for these three, `values[0]` is the destination key in `destinations`, the next values are the fixes: `add_persona` (turn on `enablePersonaTargeting` with `targetPersonas`), `require_contact` (turn on `requireContact`), `add_company_destination` (add a destination that accepts companies: `hubspot`, `pipedrive`, `googleSheets` or `customWebhook`), `enable_email_finding` (turn on `getEmails`); severity `warning` when those leads reach no destination at all, `info` when another destination still receives them; never blocks the save; `COMPANY_PAGE_UNAVAILABLE` (a Company Followers signal whose extraction stopped because a page in `linkedinCompanyUrls` does not exist on LinkedIn: nothing was extracted or charged; `values` lists the pages; replace them with PATCH /api/v1/signals/{signal_id}, then start the extraction again; cleared once the pages are replaced or a new extraction runs), `COMPANY_PAGE_UNVERIFIED` (create / PATCH responses only: a page in `linkedinCompanyUrls` could not be verified at that moment and was accepted; `values` lists them — if one does not exist, its extraction stops without using credits and the signal carries `COMPANY_PAGE_UNAVAILABLE`).
    - `severity` string, required: `info`: nothing to do. `warning`: probably not what you meant.
    - `field` string, required: The `config` key concerned, camelCase (or `dailyLeadLimit`).
    - `message` string, required
    - `values` string[]: The offending values, when there are some.
  - `watchlist` object, nullable: What the watchlist create actually took in. Returned only for watchlist signals. The rejected/unresolved counts are surfaced rather than swallowed: a watchlist that quietly dropped half its list looks identical to a healthy one until the leads never arrive.
    - `watched` integer, required: Entries now being watched by this signal (people, or companies).
    - `rejected` integer: Entries that could not be used — a LinkedIn URL that is not a personal profile, or a company row with neither a name nor a domain.
    - `alreadyWatched` integer: Entries already on this signal's list, left unchanged.

## Other responses

- `400`: Invalid request: missing required config, or an unrecognized location filter
- `401`: Missing or invalid API key
- `402`: Account has no credits
- `422`: Validation Error
- `429`: Rate limit exceeded. Retry after the number of seconds in the `Retry-After` response header.

---

[API](https://skmtc.dev/rodz/apis/rodz-api.md) · [All operations](https://skmtc.dev/rodz/apis/rodz-api/llms.txt) · [OpenAPI document](https://skmtc.dev/rodz/apis/rodz-api/revisions/3003505ff896?raw)
