---
title: "Create Job Changes signal"
method: POST
path: "/api/v1/signals/job-changes"
tags: ["Signals"]
---

# Create Job Changes signal

`POST /api/v1/signals/job-changes`

## 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-changes`
- Auth: HTTP bearer

Track when the decision-makers you care about change jobs — get notified when someone matching your target personas starts a new position at a company in your ICP, and receive their enriched profile at your `webhookUrl`.

**Input:** `name`, `webhookUrl`, and a `config` object with: `jobTitles` (preset personas from GET /api/v1/personas, e.g. "CTO", "Sales Director"), `customTitles` (your own titles), `excludedTitles`, `companySizeFilters` / `excludedCompanySizeFilters`, `locations` / `excludedLocations`, `industryFilters` / `excludedIndustryFilters` (values from GET /api/v1/signals/industries), `companyTypeFilters`, `excludedCompanyUrls`, `maxJobAgeDays` (how recent the new position must be; minimum and default 180 days), and optional enrichments (`getEmails`, `getPhones`, `getSireneData`, `detectTechnologies`, `getHomepageContent`). Top-level `dailyLeadLimit` (1–999,999) and `status` (draft/active/paused, default draft) complete the request.

**How the filters are applied** — read this before sizing a list:
- **Titles are matched by meaning, not as text.** `jobTitles` and `customTitles` form ONE list; a person qualifies by matching ANY entry. Each entry is read as a function plus a seniority level, in any language ("Directeur Commercial" satisfies a "Sales Director" target), and a role at that level OR ABOVE qualifies: a Director target accepts VPs and heads of the function and refuses a plain Manager; a Manager target also accepts Directors. A custom title carries the level its own words state; one with no level word accepts every level. `excludedTitles` is applied first and matches whole words, case- and accent-insensitive (`"intern"` removes "Intern" and "Interns", never "International"; end a word with `*` to match a prefix).
- **Company size** is judged on the NEW employer: its declared band OR its employee count must fall in `companySizeFilters` (a count within 10% of a band edge counts for both bands; a company declaring 501+ employees is never placed in a smaller band by a low count). Exclusions are strict on either value. A company whose size cannot be determined is delivered only when your selection includes a band under 51 employees.
- **Location is the PERSON's profile location**, not the company's headquarters. One matching entry in `locations` is enough; a profile with no location is dropped when `locations` is set; a profile that states only a country does not match a city or region entry. `locationFilter` is not used — send `locations`.
- **Industry** is an exact-name match on the new employer's industry, except that a CATEGORY name accepts every industry inside it — and ten names ("Manufacturing", "Retail", "Wholesale", …) are both an industry and a category and are applied as the category. GET /api/v1/signals/industries gives each category's real `industryCount` and `?category=` lists its members. A value outside that list matches nothing.
- **Recency:** a position is delivered up to `maxJobAgeDays` days after its start date, never less than 180; a smaller value is applied as 180.

**Output:** `201 Created` returns the signal, its stored `config`, `applied` (the settings in force where they differ from `config`) and `warnings` (filter values that are probably not what you meant — a category sent with its own industries, an unknown industry). Each person matching your personas and filters who recently started a new job is pushed to your `webhookUrl` as a JSON lead payload (`WebhookPersonLeadPayload`): a `signal` object (id, name, type, human-readable label, detection date, plus the move itself — `previous_title`, `previous_company`, `previous_company_linkedin_url`, `previous_job_started_on`, `previous_job_ended_on`, `new_title`, `new_company`, `new_job_started_on` (`"MM/YYYY"`), `new_job_start_month` (the same month as `"YYYY-MM"`, sortable) and `change_type`: `company`, `title` or `both`. Test the freshness of a change against `new_job_started_on` / `new_job_start_month` — `previous_job_ended_on` is a different date whenever there was a gap between the two jobs. These ten `signal` keys are a stable contract: present on every job-change lead, never renamed or null, `""` when a value is unknown — safe to map straight into your CRM), a `contact` object (first/last name, title, location, LinkedIn and Sales Navigator profile URLs, matched persona, and email/phone when enrichment is enabled), and a `company` object (name, website, LinkedIn URL — plus detected tech stack and homepage title/description when those enrichments are on). Fields that aren't available are returned as empty strings.

**Use cases:**
- New-buyer outreach — reach decision-makers in their first 90 days, when they re-evaluate tools and vendors.
- Champion tracking — follow past champions to their new company for a warm re-intro.
- Territory alerts — notify the owning rep when a target persona lands at an account.

## Request body

- JobChangesCreate: Create a Job Changes signal.
  - `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` JobChangesConfig, required: Config for job_changes.
    - `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)
    - `companySizeFilters` string[]: Company size bands to keep, judged on the NEW employer. Values: "Self-employed", "1-10", "11-50", "51-200", "201-500", "501-1000", "1001-5000", "5001-10000", "10001+". A company passes when its declared size band OR its employee count falls in one of these bands; a count within 10% of a band edge counts for both bands (48 employees passes "51-200"). A company declaring a band of 501 or more employees is never placed in a smaller band by a low count. A company whose size cannot be determined is delivered only when this selection includes a band under 51 employees. Empty = every size.
    - `jobTitles` string[]: Preset personas to track - the `name` values from GET /api/v1/personas (e.g. "CTO", "Sales Director"). A preset is a function plus a seniority level, matched by MEANING in any language, not as text: a person qualifies when their current role is in that function at that level OR ABOVE. A Director-level preset accepts VPs and heads of the function and refuses a plain Manager; a Manager-level preset also accepts Directors. `jobTitles` and `customTitles` form ONE list - matching any single entry is enough.
    - `customTitles` string[]: Your own titles, added to `jobTitles` (the two lists are a union, not an intersection). A custom title is matched by meaning like a preset and carries the seniority its own words state: "Marketing Director" is Director-level and accepts that level or above; a title with no level word ("Growth Marketing") accepts every professional level. A broad custom title therefore WIDENS the signal - it never narrows the presets. Keep a level out with `excludedTitles`.
    - `locationFilter` string: Not used by this signal - accepted for compatibility only and filters nothing. Put the places you want in `locations`.
    - `locations` string[]: Places the PERSON must be in - judged on their profile location, not on the company's headquarters (e.g. ['France', 'Lyon']). One matching entry is enough. A profile with no location is dropped when this is set. A profile that states only a country does not match a city or region entry: add the country itself if you want those people. Names are validated on write (400 INVALID_LOCATION); check ambiguous ones with GET /api/v1/enrichment/validate-location first.
    - `excludedLocations` string[]: Places to drop, judged on the person's profile location. With no `locations`, a profile without a location is kept.
    - `industryFilters` string[]: Only return companies in these LinkedIn industries. Two kinds of value work: a single industry (e.g. 'Software Development') or a broad category that covers many (e.g. 'Technology and Media'). Matching is exact, so a near-miss like 'Software development' matches nothing and the signal quietly produces no leads. Empty = all industries. Call GET /api/v1/signals/industries for the valid list.
    - `companyTypeFilters` string[]: Only return companies of these legal/ownership types. Valid values: 'Privately Held', 'Public Company', 'Nonprofit', 'Educational Institution', 'Partnership', 'Sole Proprietorship', 'Government Agency', 'Self-Employed'. Empty = all types. A company whose type is unknown is KEPT, not dropped, so this narrows results only where the type is known.
    - `excludedIndustryFilters` string[]: LinkedIn industries to exclude. Same vocabulary as `industryFilters` (single industry or category name); an unrecognised value excludes nothing. Call GET /api/v1/signals/industries for the valid list.
    - `excludedCompanySizeFilters` string[]: Company size bands to drop, judged strictly on the declared band or on the employee count - the 10% tolerance does not widen an exclusion. Same nine values as `companySizeFilters`.
    - `excludedTitles` string[]: Titles to keep out, matched as whole words on the current title, case- and accent-insensitive ("manager" removes "Sales Manager" and "Managers", not "Management"; end a word with * to match a prefix), applied before the inclusion lists - use it to remove a level the "or above" rule would otherwise admit.
    - `excludedCompanyUrls` string[]: LinkedIn company URLs to exclude from results
    - `maxJobAgeDays` integer: How recent the new position must be, in days from its start date. Minimum and default 180: a smaller value - 0 included - is accepted and applied as 180 (the response's `applied` field shows the value in force). Positions are detected within about a year of the change, so values above 365 add nothing.
    - `publishedDate` string, nullable: Deprecated and ignored — kept only so existing integrations keep validating. Job recency for this signal is controlled entirely by `maxJobAgeDays`, and every job change we deliver started within the last 180 days at minimum. Setting this has no effect on which leads you receive.
  - `dailyLeadLimit` integer: Max leads per day
  - `duplicateHandling` string, nullable: Duplicate lead handling: 'refuse' (never accept), 'allow' (always accept), or 'Xd' (accept a duplicate after X days, e.g. '7d'). Optional — defaults to 'allow', so a contact who triggers this signal a second time is delivered a second time.
  - `status` string, nullable: Initial status (default: draft). Accepts draft, active, or paused. If active, mandatory config fields are validated.

## Response `201`

Signal created

- object: A single signal: create, GET /signals/{id} and PATCH. It carries two fields the list does not. A customer reads their stored `config` back and assumes it is the one in force; on `job_changes` three fields were not (`maxJobAgeDays` 90 stored / 180 applied, `publishedDate` stored and never read, `allowUnknownCompanySize` true and applied false), with nothing anywhere saying 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.

## Other responses

- `400`: Invalid request: missing required config (Job Title Presets), or an unrecognized location
- `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)
