# Lead routing

> Read and publish your routes, switch Smart routing, try a lead, route the waiting leads, mark people away, and read why a lead was assigned.

The lead routing API decides who owns a new lead when it arrives. Your organization has an ordered list of routes. A route has conditions (which leads), a pool of people or live teams, and options (office hours, a daily cap, a reassign timer). For each lead Fondaro walks the routes from the top and **the first route that takes the lead and has someone who can get it wins**. A route that takes the lead but ends with nobody (an empty pool, everyone away or at their daily cap, or the lead arriving outside office hours) falls through to the next route. When no route works, Smart routing hands the lead to the active people on your team, those who list the lead's language on their profile first, otherwise everyone in turn. With Smart routing off, the lead waits in Unassigned.

The session routes on this page take a signed-in member's session token (`Authorization: Bearer <session-token>`). The same operations are open to API keys under [`/v1/lead-routing`](https://www.fondaro.com/docs/api/v1/lead-routing.md), with the `crm:read` and `crm:write` scopes. See [Authentication](https://www.fondaro.com/docs/api/authentication.md).

Reading Smart routing, and marking yourself away, are open to every member. Everything else under `/lead-routing` is for organization admins; a member gets `403`.

## Read the whole picture

`GET /lead-routing`

Admins only. Returns the routes in order, Smart routing, the problems found, who is away, and a `revision` you send back when you publish.

```bash
curl https://api.fondaro.com/lead-routing \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "routes": [
    {
      "id": "7f3c2a10-...",
      "name": "Idealista sellers",
      "conditions": {
        "type": "group",
        "combinator": "and",
        "children": [
          { "type": "condition", "field": "origin", "operator": "has_any_of",
            "value": { "kind": "strings", "values": ["portal:idealista"] } },
          { "type": "condition", "field": "leadType", "operator": "has_any_of",
            "value": { "kind": "strings", "values": ["seller"] } }
        ]
      },
      "strategy": "round_robin",
      "priority": 0,
      "isActive": true,
      "schedule": null,
      "dailyCap": 8,
      "reassignAfterMinutes": 30,
      "members": [
        { "userId": "user_2abc...", "teamId": null, "weight": 0, "sortOrder": 0 },
        { "userId": null, "teamId": "b21d94c0-...", "weight": 0, "sortOrder": 1 }
      ],
      "createdAt": "2026-10-08T09:00:00.000Z",
      "updatedAt": "2026-10-08T09:00:00.000Z"
    }
  ],
  "smart": { "enabled": true, "teamUserIds": ["user_2abc...", "user_2def..."] },
  "problems": [],
  "revision": "c4f1...",
  "away": { "user_2def...": "2026-10-20T00:00:00.000Z" }
}
```

`priority` is the route's place in the order, 0 first. Smart routing is not in `routes`.

## Publish the whole set

`PUT /lead-routing`

Admins only. Replaces the ordered set of routes in one atomic write: routes you leave out are deleted, routes you list with their `id` keep it, routes without an `id` are created. The order of the array is the order of the routes.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `revision` | Body, string | Yes | The `revision` from your last read |
| `routes` | Body, array | Yes | Up to 200 routes, in order (see [A route](#a-route)) |

```bash
curl -X PUT https://api.fondaro.com/lead-routing \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "revision": "c4f1...",
    "routes": [
      {
        "id": "7f3c2a10-...",
        "name": "Idealista sellers",
        "strategy": "round_robin",
        "members": [{ "userId": "user_2abc...", "weight": 0, "sortOrder": 0 }]
      }
    ]
  }'
```

The response is the new state, in the shape of `GET /lead-routing`. If someone published since your read, the call answers `409` with `code: "LEAD_ROUTING_REVISION_CONFLICT"` and the current `revision`; read again and merge before you retry.

## A route

Create, change and publish all use these fields.

| Field | Type | Description |
|---|---|---|
| `name` | string or null | Up to 80 characters, e.g. "Idealista sellers" |
| `conditions` | group or null | Which leads. Null or an empty group takes every lead (see [Conditions](#conditions)) |
| `strategy` | `round_robin`, `weighted` or `smart` | Take turns, split by share, or Smart routing limited to this route's people. A route of one person is "always this person" |
| `isActive` | boolean | `false` pauses the route; it is skipped |
| `schedule` | object or null | Office hours (see below). Null means any time |
| `dailyCap` | integer or null | 1 to 1000. The most leads one person gets from this route in a day |
| `reassignAfterMinutes` | integer or null | 5 to 10080. Give the lead to the next person when nobody touched it in this long |
| `members` | array | `{ userId, teamId, weight, sortOrder }`. Set `userId` for a person or `teamId` for a team, not both |

- A team is read live when a lead arrives: someone joining the team joins the route. On a `weighted` route, members must be people (no teams) and `weight` is a percentage; the weights must add up to 100. On other strategies `weight` is 0.
- Everyone in a pool who left the agency is skipped. Users, teams, tags and Growth areas must belong to your organization, or the write answers `400`.

**Office hours** are `{ "zone": "Europe/Madrid", "days": [{ "day": 1, "from": "09:00", "to": "17:30" }] }`. `day` is 1 (Monday) to 7 (Sunday), times are 24-hour `HH:MM`, `from` is included and `to` is not. A lead that arrives outside them falls through. `zone` is the IANA zone the hours are read in.

### Conditions

`conditions` is an AND/OR tree of conditions, nested up to four levels deep and holding up to 50 conditions. Each condition is `{ "type": "condition", "field", "operator", "value" }`; a group is `{ "type": "group", "combinator": "and" | "or", "children": [...] }`.

| `field` | `operator` | `value` |
|---|---|---|
| `source` | `has_any_of`, `has_none_of` | `{ "kind": "strings", "values": ["zapier"] }` |
| `origin` | `has_any_of`, `has_none_of` | Where the lead came from, e.g. `portal:idealista`, `meta_lead_form`, `meta_ads`, `google_ads`, `organic`, `message:whatsapp` |
| `leadType` | `has_any_of`, `has_none_of` | `buyer`, `seller`, `tenant`, `collaborator` |
| `preferredLanguage` | `has_any_of`, `has_none_of` | BCP 47 tags. A region (`nl-BE`) also matches its base language (`nl`) |
| `country` | `has_any_of`, `has_none_of` | Two-letter country of the lead's phone number |
| `tag` | `has_any_of`, `has_all_of`, `has_none_of` | Tag ids, read when the lead arrives |
| `growthArea` | `has_any_of`, `has_none_of` | Growth area (subscription) ids |
| `phone`, `email` | `is_present`, `is_missing` | `{ "kind": "empty" }` |

## Add, change, delete and reorder one route

These are the single-route writes. `PUT /lead-routing` is the better fit when you edit the whole canvas at once. All are admin-only and return the route.

### Create a route

`POST /lead-routing/rules`

Takes a route's fields (`strategy` and `members` are required) and an optional `position`, the 0-based place in the order (default last, just above Smart routing).

```bash
curl -X POST https://api.fondaro.com/lead-routing/rules \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Swedish buyers",
    "strategy": "weighted",
    "position": 0,
    "conditions": {
      "type": "group", "combinator": "and",
      "children": [{ "type": "condition", "field": "preferredLanguage",
        "operator": "has_any_of", "value": { "kind": "strings", "values": ["sv"] } }]
    },
    "members": [
      { "userId": "user_2abc...", "weight": 60, "sortOrder": 0 },
      { "userId": "user_2def...", "weight": 40, "sortOrder": 1 }
    ]
  }'
```

### Change a route

`PATCH /lead-routing/rules/:id`

Takes any subset of the route's fields, plus `position` to move it. Sending `members` replaces the whole pool.

```bash
curl -X PATCH https://api.fondaro.com/lead-routing/rules/7f3c2a10-... \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "isActive": false }'
```

### Delete a route

`DELETE /lead-routing/rules/:id`

```bash
curl -X DELETE https://api.fondaro.com/lead-routing/rules/7f3c2a10-... \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "deleted": true }
```

### Reorder the routes

`PUT /lead-routing/rules/order`

Send every route id in the new order. First is checked first.

```bash
curl -X PUT https://api.fondaro.com/lead-routing/rules/order \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["9a1c...", "7f3c2a10-..."] }'
```

## Try a lead

`POST /lead-routing/preview`

Admins only. Shows which route would take a lead and who would get it, without assigning anything. Give an existing `leadId`, or describe a lead in `facts`; with both, `facts` override the lead's own. Send `routes` (and `smartEnabled`) to try an unpublished draft; leave them out to try what is published.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `leadId` | Body, integer | No | An existing lead |
| `facts` | Body, object | No | `source`, `origin`, `leadType`, `language`, `country`, `tagIds`, `growthAreaId`, `hasPhone`, `hasEmail`, `arrivedAt` (ISO time; office hours are read against it) |
| `routes` | Body, array | No | A draft set of routes, in the shape of a publish |
| `smartEnabled` | Body, boolean | No | Try with Smart routing on or off |

```bash
curl -X POST https://api.fondaro.com/lead-routing/preview \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "facts": { "origin": "portal:idealista", "leadType": "seller", "language": "es-ES" } }'
```

```json
{
  "ruleId": "7f3c2a10-...",
  "smart": false,
  "userId": "user_2abc...",
  "reason": "next_in_turn",
  "steps": [
    { "ruleId": "7f3c2a10-...", "smart": false, "outcome": "assigned",
      "skipped": [{ "userId": "user_2def...", "reason": "away" }],
      "userId": "user_2abc...", "reason": "next_in_turn" }
  ],
  "facts": { "origin": "portal:idealista", "leadType": "seller", "language": "es-ES",
    "source": null, "country": null, "tagIds": [], "growthAreaId": null,
    "hasPhone": true, "hasEmail": true, "arrivedAt": "2026-10-08T10:00:00.000Z" }
}
```

`steps` lists every route checked, in order. A step's `outcome` is `assigned`, `no_match`, `paused`, `outside_hours` or `empty_pool`. `skipped[].reason` is `departed`, `away` or `capped`. `reason` is why the person was picked: `next_in_turn`, `share`, `speaks_language`, `least_recent` or `only_person`. With no person, `userId` is null and the lead would wait in Unassigned.

## Last 30 days

`GET /lead-routing/stats?days=30`

Admins only. `days` is 1 to 90 (default 30). Counts the leads each route and Smart routing assigned, and each person's share.

```json
{
  "days": 30,
  "byRoute": [{ "ruleId": "7f3c2a10-...", "count": 41, "lastAt": "2026-10-08T08:12:00.000Z", "lastUserId": "user_2abc..." }],
  "byUser": [{ "userId": "user_2abc...", "count": 24 }]
}
```

## Route the waiting leads

`POST /lead-routing/route-unassigned`

Admins only. Sends the leads waiting in Unassigned through your routes now: up to 500, oldest first, within a 25 second limit. A lead someone assigns meanwhile is left alone. Takes no body.

```bash
curl -X POST https://api.fondaro.com/lead-routing/route-unassigned \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "considered": 37, "assigned": 31, "leftWaiting": 6 }
```

## Mark someone away

`PUT /lead-routing/away`

Every route, and Smart routing, skip a person until the date. A member may set their own; setting someone else's is for admins.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `userId` | Body, string | No | Whose away date. Default: the caller |
| `until` | Body, ISO date or null | Yes | When they are back. `null` means back now |

```bash
curl -X PUT https://api.fondaro.com/lead-routing/away \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user_2def...", "until": "2026-10-20" }'
```

## Smart routing

### Read Smart routing

`GET /lead-routing/smart`

Open to every member. Returns whether Smart routing is on and who it hands leads to, in turn order.

```bash
curl https://api.fondaro.com/lead-routing/smart \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "enabled": true, "teamUserIds": ["user_2abc...", "user_2def..."] }
```

### Switch Smart routing on or off

`PUT /lead-routing/smart`

Admins only. Idempotent. Fondaro never switches it back on after you switch it off.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `enabled` | Body, boolean | Yes | `true` to switch on, `false` to switch off |

```bash
curl -X PUT https://api.fondaro.com/lead-routing/smart \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'
```

```json
{ "enabled": false }
```

Smart routing always sits after your own routes. It is changed only through this switch: it is not in `routes`, and `PATCH` or `DELETE` on its id returns `404`.

## Problems

`problems` in `GET /lead-routing` lists what is wrong with a route. Each has a `code` and the `ruleId` it concerns.

| `code` | Meaning |
|---|---|
| `unreachable` | An earlier route takes every lead this one would (`byRuleId` names it) |
| `empty_pool` | Nobody is left in the route |
| `departed_member` | Someone in the route left the agency (`userId`); they are skipped |
| `everyone_away` | Everyone in the route is away |
| `missing_tag` | The conditions name a deleted tag (`tagId`); the route is paused |
| `ended_growth_area` | The conditions name a Growth area that ended (`growthAreaId`) |
| `weights_not_100` | A split by share whose shares do not add up to 100 |
| `schedule_no_days` | Office hours with no day in them, so the route never runs |
| `archived_team` | A team in the route was archived (`teamId`) |

## Read why a lead was assigned

`GET /crm/leads/:id` returns `lead.assignmentReason` to every member who can see the lead.

```bash
curl https://api.fondaro.com/crm/leads/12345 \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "lead": {
    "id": 12345,
    "assignmentReason": {
      "assigneeIds": ["user_2abc..."],
      "at": "2026-10-08T10:14:03.000Z",
      "route": { "id": "7f3c2a10-...", "name": "Idealista sellers" },
      "smart": false
    }
  },
  "origin": {}
}
```

It is set when the newest change to the lead's owners was Fondaro routing it, and `null` once a person has changed the owners since, or when nothing routed it. `assigneeIds` are the user ids routing added. `route` names the route that assigned the lead, or is `null` for an older assignment or a route since deleted. `smart` is `true` when Smart routing assigned it.

## Errors

| Status | When |
|---|---|
| `400` | Validation failed: bad conditions, a person, team, tag or Growth area outside your organization, shares that do not add up to 100, or a cap or timer out of range |
| `401` | No valid session token |
| `403` | A member called an admin-only endpoint |
| `404` | The route does not exist in your organization, or it is the Smart routing rule |
| `409` | `LEAD_ROUTING_REVISION_CONFLICT`: someone published since your read. Read again and retry |

Source: https://www.fondaro.com/docs/api/lead-routing
