# How do you create a Rodz signal and route it to lemlist, entirely through the API?

> Source: https://www.rodz.io/en/guides/api/create-and-route-a-signal/
> Updated on 2026-10-12 · Peter Cools · API recipe · Job changes · API REST

## Short answer

Create the signal as a draft with POST /api/v1/signals/job-changes, read applied and warnings in the response, connect lemlist with PUT /api/v1/integrations/lemlist, get the campaign id, then route the signal with a PATCH on destinations. A last PATCH sets the status to active: from then on each lead costs 2 credits, plus 0.5 credit per email found.

## Key facts

- A signal created without `status` stays a **draft**: it produces nothing and costs nothing until it is active.
- A job change costs **2 credits** per lead, plus 0.5 credit per email found (`getEmails`), charged on success only.
- The daily limit ranges from **1 to 999,999** leads (`dailyLeadLimit`, default 100): it is the signal’s spending cap.
- A `PATCH` **merges** `config` into the stored configuration: send only the fields you change.

Cost: 2 credits per lead (+0.5 credit per email found)

## Before you start

- A Rodz API key (**Integrations → API** in the dashboard), exported as `RODZ_API_KEY`.
- A lemlist API key and a lemlist campaign with at least one step.
- `curl` and, to read responses more easily, `jq`.

## Steps

### 1. Pick the signal type and its allowed values

`GET /api/v1/signal-types` is the entry point: each type gives its `createEndpoint`, `configParameters`, `pricingModel` and `outputFields`. For job changes, roles come from `GET /api/v1/personas` (the `name` field) and industries from `GET /api/v1/signals/industries` (copy them verbatim: matching is exact).

`Catalog, personas and industries`

```bash
curl -s "https://api.rodz.io/api/v1/signal-types" -H "Authorization: Bearer $RODZ_API_KEY" \
  | jq '.signalTypes[] | {signalTypeId, createEndpoint, creditsPerLead}'
curl -s "https://api.rodz.io/api/v1/personas" -H "Authorization: Bearer $RODZ_API_KEY"
curl -s -G "https://api.rodz.io/api/v1/signals/industries" -H "Authorization: Bearer $RODZ_API_KEY" \
  --data-urlencode "search=software"
```

### 2. Create the signal as a draft

Send `name`, `config`, `dailyLeadLimit` and `duplicateHandling`. Without `status`, the signal is created as `draft`. Turn on `getEmails`: an email sequence needs the address.

The `201` response returns the signal with two fields to read: `applied` (the settings actually in force where they differ from `config`) and `warnings` (filter values that are probably not what you meant). They are never errors: the signal is created regardless.

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

```bash
curl -s -X POST "https://api.rodz.io/api/v1/signals/job-changes" \
  -H "Authorization: Bearer $RODZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "New sales directors, SaaS, France",
    "dailyLeadLimit": 30,
    "duplicateHandling": "refuse",
    "config": {
      "jobTitles": ["Sales Director"],
      "customTitles": ["Head of Revenue"],
      "companySizeFilters": ["51-200", "201-500"],
      "industryFilters": ["Software Development"],
      "locations": ["France"],
      "getEmails": true
    }
  }' | tee signal.json
export SIGNAL_ID=$(jq -r .id signal.json)
```

### 3. Connect lemlist

`PUT /api/v1/integrations/lemlist` saves your lemlist key; it is validated before it is stored, then encrypted. `GET /api/v1/integrations` then shows `connected: true`. HubSpot, Pipedrive and Google Sheets connect once through OAuth in the dashboard (the API answers `400 USE_DASHBOARD_OAUTH`).

`PUT /api/v1/integrations/lemlist`

```bash
curl -s -X PUT "https://api.rodz.io/api/v1/integrations/lemlist" \
  -H "Authorization: Bearer $RODZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"apiKey\": \"$LEMLIST_API_KEY\", \"label\": \"Sales team\"}"
```

### 4. Get the campaign id

`GET /api/v1/integrations/lemlist/campaigns` returns `items[]` with `id`, `name` and `status`. The `id` is the `campaignId` of the next step. A `409` means lemlist is not connected.

`GET /api/v1/integrations/lemlist/campaigns`

```bash
curl -s "https://api.rodz.io/api/v1/integrations/lemlist/campaigns" \
  -H "Authorization: Bearer $RODZ_API_KEY" | jq '.items[] | {id, name, status}'
```

### 5. Route the signal to the campaign, then activate it

A `PATCH` with `destinations.lemlist.campaignId` plugs the signal into the campaign. If several lemlist accounts are connected, add `integrationId`. A second `PATCH` sets the signal to `active`: from then on, every lead delivered is charged and pushed into the campaign.

`PATCH /api/v1/signals/{signal_id}`

```bash
curl -s -X PATCH "https://api.rodz.io/api/v1/signals/$SIGNAL_ID" \
  -H "Authorization: Bearer $RODZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destinations": {"lemlist": {"campaignId": "cam_aBc123XyZ"}}}'

curl -s -X PATCH "https://api.rodz.io/api/v1/signals/$SIGNAL_ID" \
  -H "Authorization: Bearer $RODZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'
```

## Example payload

`201 Created` response of `POST /api/v1/signals/job-changes` (fictitious data). An empty `applied` means everything runs as stored.

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "New sales directors, SaaS, France",
  "status": "draft",
  "signalTypeId": "job_changes",
  "webhookUrl": null,
  "config": {
    "jobTitles": ["Sales Director"],
    "customTitles": ["Head of Revenue"],
    "companySizeFilters": ["51-200", "201-500"],
    "industryFilters": ["Software Development"],
    "locations": ["France"],
    "getEmails": true
  },
  "dailyLeadLimit": 30,
  "leadsToday": 0,
  "leadsTotal": 0,
  "duplicateHandling": "refuse",
  "createdAt": "2026-10-12T08:00:00Z",
  "updatedAt": "2026-10-12T08:00:00Z",
  "lastLeadAt": null,
  "applied": {},
  "warnings": []
}
```

## The calls, in order

| #   | Call                                                     | Purpose                                                   |
| --- | -------------------------------------------------------- | --------------------------------------------------------- |
| 1   | `GET /api/v1/signal-types`                               | Pick the type, read `configParameters` and `pricingModel` |
| 2   | `GET /api/v1/personas`, `GET /api/v1/signals/industries` | Allowed filter values                                     |
| 3   | `POST /api/v1/signals/job-changes`                       | Create the signal as a draft                              |
| 4   | `PUT /api/v1/integrations/lemlist`                       | Connect lemlist (once per account)                        |
| 5   | `GET /api/v1/integrations/lemlist/campaigns`             | Find the `campaignId`                                     |
| 6   | `PATCH /api/v1/signals/{signal_id}`                      | Route to the campaign                                     |
| 7   | `PATCH /api/v1/signals/{signal_id}`                      | Set to `active`                                           |

Steps 4 and 5 are not repeated for each signal: once lemlist is connected, every new signal only needs steps 3, 6 and 7.

## The variables lemlist receives

Each lead enters the campaign with its variables: `firstName`, `lastName`, `email`, `companyName`, `title`, `linkedinUrl`, `companyWebsite`, `signalName`, `matchedPersona`. Use them in your messages, for example "Congratulations on joining {{companyName}}".

## Troubleshooting

- **`422` on create**: a `config` field does not exist for this type. The message names it; the exact list is in `configParameters` of `GET /api/v1/signal-types`.
- **No lead in lemlist**: the campaign needs at least one step, and a lead without an email cannot enter an email sequence.
- **Quiet signal**: `GET /api/v1/signals/{signal_id}/funnel` shows how many candidates each filter removed.

## Frequently asked questions

### Why create the signal as a draft?

A draft produces no lead and costs nothing. You can read `applied` and `warnings`, plug in the destination, then activate. Activating a signal with no destination and no webhook also works: its leads are then read with `GET /api/v1/signals/{signal_id}/leads`.

### What do `applied` and `warnings` mean?

`applied` lists the settings applied differently from how they are stored (for example `maxJobAgeDays` raised to its 180 minimum). `warnings` flags filter values that are probably wrong, such as an unknown industry or a category sent along with its own industries. The signal is created either way.

### How do I change one filter without resending everything?

`PATCH /api/v1/signals/{signal_id}` merges `config` into the stored configuration: send only the fields you change. To clear a list, send it empty, for example `"customTitles": []`.

### Can the same signal feed a CRM and lemlist?

Yes. `destinations` takes several destinations at once, for example `hubspot` (`enabled: true`) and `lemlist` (`campaignId`). HubSpot and Pipedrive must first be connected through OAuth in the dashboard.

### How do I test without waiting for a real lead?

If the signal has a `webhookUrl`, `POST /api/v1/signals/{signal_id}/test-webhook` sends a sample lead flagged `signal.is_test: true`. For lemlist, check the first real lead in the campaign after activation.

## Sources

- [Rodz API reference: Create Job Changes signal, Update signal, Integrations](https://api.rodz.io/docs)
- [Rodz API OpenAPI specification](https://api.rodz.io/openapi.json)

## Related guides

- https://www.rodz.io/en/guides/api/poll-signal-leads/
- https://www.rodz.io/en/guides/api/webhook-receiver-hmac/
- https://www.rodz.io/en/guides/use-cases/job-changes-to-lemlist/
