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

# Create Job Offers Watchlist signal

`POST /api/v1/signals/job-offers-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-offers-watchlist`
- Auth: HTTP bearer

Watch a fixed list of companies and get their new job postings as leads - reach the hiring decision-maker the day a role opens at an account you already care about.

Where the Job Offers signal DISCOVERS companies from filters, this one starts from your list: your target accounts, your customers, or your competitors.

**Input:** `companies` (names, domains or LinkedIn company URLs, or objects `{name, domain, linkedinUrl}` carrying all three - send the LinkedIn URL whenever you have it, it identifies the company exactly where a name cannot separate two companies sharing one; failing that a name matches more reliably than a domain), `maxJobsPerCompanyPerDay` (default 3; total daily volume is roughly this x the number of companies watched), `cadenceHours` (default 24), `jobTitleInclude` / `jobTitleExclude`, `firstRunLookbackDays` (default 30 - how far back the first check of a company looks, for the initial list and, by default, for every company added later, so a company does not deliver its whole posting history at once), `addedCompaniesLookback` (`first_run` by default; `recent_only` limits the first check of a company added later to the postings of the last `cadenceHours`), and optional persona targeting (`enablePersonaTargeting`, `targetPersonas`, `maxLeadsPerCompany`, `companyOnlyWhenContactsRefused` - default false, deliver the company alone, no contact, when people were found there but every one was filtered out by your persona/location/size settings, instead of nothing, `requireContact` - default false; true = only leads with a contact are delivered: a company where nobody matching your personas is found is neither delivered nor charged, whatever `companyOnlyWhenContactsRefused` says), plus enrichment flags (`getEmails`, `getPhones`).

**Output:** each new posting at a watched company is pushed to your `webhookUrl`. With persona targeting off you receive one company-level lead per posting (`contact` empty); with it on you receive the hiring decision-makers as person-leads. A posting is delivered ONCE - later checks only surface roles newer than what you have already received.

`companies` 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}/companies`, which is also the only place that tells you which companies were actually matched to an employer, and add or remove companies with `POST` / `DELETE` on the same path.

**Use cases:**
- Account-based hiring alerts - the day a target account opens a role, reach the hiring manager while the need is fresh and no vendor has been chosen.
- Expansion triggers in your customer base - watch existing customers and treat a new role on a team you sell to as a signal that budget and headcount just moved.
- Competitor headcount watch - track competitors' postings to see which teams they are building, and time your differentiation play accordingly.

## 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` JobOffersWatchlistConfig, required: Config for job_offers_watchlist — the PUBLIC API shape. The in-app signal takes its companies from a CSV upload, which REST cannot express, so the API accepts them inline as `companies`. Everything else mirrors WatchlistJobOffersConfig (app/schemas/watchlist_configs.py); the create endpoint writes the list into watchlist_company 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
    - `companies` union[]: Companies to watch. Each entry is either a plain string — a name (`"Doctolib"`), a domain (`"doctolib.fr"`) or a LinkedIn company URL — or an object `{name, domain, linkedinUrl}` (all optional, at least one required) carrying everything you know about the company. Both forms can be mixed. Send every identifier you have: the LinkedIn URL identifies the company exactly, and a name alone cannot separate two companies that share one. Sent again in a `PATCH`, the entries are ADDED to the list (nothing is removed). **Accepted entry forms:** a company name (`"Doctolib"`); a domain or website URL (`"payfit.com"`, `"https://www.payfit.com/fr"`); its LinkedIn company page (`"https://www.linkedin.com/company/payfit"`, in any spelling: with or without `https://` and `www.`, a country subdomain, a trailing path or query; school and showcase pages too); its numeric LinkedIn company id (`"10426145"`) or a Sales Navigator company link (`"https://www.linkedin.com/sales/company/10426145"`), both read as that id; or an object `{name, domain, linkedinUrl}` combining several. Any other LinkedIn link names no company: a person's profile (`/in/`), a post or a group page is refused with 422 `INVALID_COMPANY_ENTRY`, which lists each offending entry, and nothing is saved.
      - union
        - string
        - WatchedCompanyObject: One company, with everything you know about it. The object form of a company-list entry. Send every identifier you have: each one is another way to recognise the company, and together they separate two companies that share a name.
          - `name` string, nullable: The company name as you know it, e.g. `"PayFit"`.
          - `domain` string, nullable: The company website, e.g. `"payfit.com"`. A full URL is accepted and reduced to its domain.
          - `linkedinUrl` string, nullable: The company LinkedIn page, e.g. `"https://www.linkedin.com/company/payfit"`. The most precise identifier of the three. A Sales Navigator company link (`/sales/company/<id>`) or the numeric LinkedIn company id is accepted too, and so are school and showcase pages; any other LinkedIn link (a person's profile, a post) is refused with 422 `INVALID_COMPANY_ENTRY`.
    - `maxJobsPerCompanyPerDay` integer: Most postings turned into leads per company per day. Total volume is roughly this x the number of companies watched.
    - `cadenceHours` integer: How often each company is checked, in hours.
    - `jobTitleInclude` string[]: Keywords the job title must include. Each keyword is matched anywhere in the title, not against the whole title: `sales` matches "Head of Sales EMEA". A posting is kept when its title contains ANY one of them. Case and accents are ignored.
    - `jobTitleExclude` string[]: Keywords the job title must not include: a posting is skipped when its title contains any of them anywhere (`intern` skips "Sales Intern").
    - `firstRunLookbackDays` integer: How far back the first check of a company looks: every company on the list when the signal starts, and by default every company added or re-added later too. Bounds the initial backfill so a company does not deliver its whole history at once. A posting already delivered is never sent again.
    - `addedCompaniesLookback` 'first_run' | 'recent_only': What the first check of a company ADDED to the list after the signal started (or re-added after a removal) looks at. `first_run` (default): the same `firstRunLookbackDays` window as the initial list, so a company you add today brings its recent openings with it. `recent_only`: only postings from the last `cadenceHours` (one day by default), for when you want added companies to report new openings only.
    - `enablePersonaTargeting` boolean: Off = one company-level lead per posting. On = resolve the hiring decision-makers behind each posting as person-leads.
    - `targetPersonas` string[]
    - `maxLeadsPerCompany` integer, nullable: Cap contacts per company when persona targeting is on.
    - `companyOnlyWhenContactsRefused` boolean: Deliver the company alone, with no contact, when people were found at it but every one was filtered out by your persona, location or company size settings. Off by default: that company produces nothing, the same as before this option existed. On: you still get the company — priced and delivered exactly like a company-only lead, the same as when nobody at all is found there.
    - `requireContact` boolean: Only deliver leads with a contact. Off (default): when the people search finds nobody matching your personas at a company, you still receive the company alone, with no contact, priced as a company-level lead. On: that company is neither delivered nor charged, and it does not count toward `dailyLeadLimit`. Takes precedence over `companyOnlyWhenContactsRefused` where both are set. Only applies when `enablePersonaTargeting` is on.
  - `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)
