# CRM · Leads

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /crm/leads/sla/sweep

**Run the stage-SLA sweep now**

`operationId: LeadsController_runSlaSweep`

Root admins only. Runs the hourly job that chases leads sitting in a stage past its SLA, instead of waiting for the next run.

#### Signature

```http
POST /crm/leads/sla/sweep () -> { ran: true, lastRun }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `RootAdmin`, `RootSystem`.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { ran: true, lastRun } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/import

**Import leads from spreadsheet rows**

`operationId: LeadsController_importLeads`

Creates a lead per row (at most 5,000 per call). Column names from a HubSpot export are recognised. A row with no usable data is skipped, and so is a row whose email is already a lead (or appears earlier in the same import). `pipelineId` / `stageId` place every new lead. Each row is created independently: one failing row is reported and the rest carry on.

#### Signature

```http
POST /crm/leads/import (body) -> { total, created, skipped, failed, errors: [{ row, error }], skippedRows: [{ row, reason }] }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | NO_ROWS | No rows to import | `rows` missing or empty. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "rows": [
    {
      "First Name": "Ada",
      "Last Name": "Lovelace",
      "Email": "ada@example.com",
      "Company": "Analytical Ltd"
    }
  ],
  "pipelineId": "PL-1"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { total, created, skipped, failed, errors: [{ row, error }], skippedRows: [{ row, reason }] } |
| `400` | No rows to import — `rows` missing or empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/leads/detail

**List leads**

`operationId: LeadsController_getLeads`

Lists leads with filtering, sorting and paging — the pipeline list view.

#### Signature

```http
GET /crm/leads/detail (pipeline?: string, status?: string, priority?: string, source?: string, assignedTo?: string, search?: string, page?: integer, pageSize?: integer, sortField?: string, sortDirection?: string) -> A page of leads
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `500` | OPERATION_FAILED | Failed to retrieve leads | An unexpected failure in the leads service. | Retry with backoff. The message is fixed text and does not indicate what went wrong. |

Plus the standard platform errors: `401`, `403`, `429`.

#### See also

- `GET /crm/leads/detail/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `status` | query | "new" \| "contacted" \| "qualified" \| "disqualified" \| "converted" | — |  |
| `priority` | query | string | — |  |
| `source` | query | string | — |  |
| `assignedTo` | query | string | — | Filter to one owner. |
| `search` | query | string | — | Free-text search across name, email and company. |
| `pipeline` | query | string | — | Pipeline id. |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |
| `sortField` | query | string | — | Field to sort by. |
| `sortDirection` | query | "asc" \| "desc" | — |  |
| `orgId` | query | any | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of leads |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | Failed to retrieve leads — An unexpected failure in the leads service. |

## POST /crm/leads/detail

**Create a lead**

`operationId: LeadsController_createLead`

Creates a lead. **Validation is deliberately minimal** — almost every field is optional, because the point is to capture whatever a form or a phone call produced and fill in the rest later.

Only two rules apply: an `email`, *if supplied*, must be well-formed; and `status` defaults to `new` when omitted. A lead with nothing but a first name is valid.

A duplicate on a unique field is reported as a `409` naming the field.

Lead errors carry a richer body than the platform default — `{ statusCode, message, error?, suggestion? }`. The `suggestion` is written for a person and is safe to show in a UI.

#### Signature

```http
POST /crm/leads/detail (body) -> The created lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Empty is better than wrong here — omit a field you are unsure of rather than guessing, and enrich it later.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_EMAIL | Invalid email format: "<email>" | An `email` was supplied and is not well-formed. | Fix the address, or leave it blank and add it later — a lead does not need one. |
| `409` | DUPLICATE_LEAD | A lead with this <field> already exists | A unique field collides with an existing lead. | Update the existing lead instead, or use different contact details. The message names the offending field. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/enrich/{id}`
- `GET /crm/leads/detail`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The lead to create. Nearly everything is optional.

```json
{
  "firstName": "Ada"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created lead |
| `400` | Invalid email format: "<email>" — An `email` was supplied and is not well-formed. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `409` | A lead with this <field> already exists — A unique field collides with an existing lead. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/leads/detail/{id}

**Get a lead**

`operationId: LeadsController_getLead`

Fetches one lead with its full record, including score and pipeline position.

#### Signature

```http
GET /crm/leads/detail/{id} (id: string) -> The lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /crm/leads/detail/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /crm/leads/detail/{id}

**Update a lead**

`operationId: LeadsController_updateLead`

Updates a lead's fields. Use the dedicated lifecycle endpoints for qualification, conversion and assignment — those record the transition, this one just writes fields.

#### Signature

```http
PUT /crm/leads/detail/{id} (id: string, body) -> The updated lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Setting `status` directly here bypasses the qualify/disqualify/convert endpoints and records no transition.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |
| `500` | OPERATION_FAILED | Failed to update lead | An unexpected failure in the leads service. | Retry with backoff. The message is fixed text and does not indicate what went wrong. |

Plus the standard platform errors: `401`, `403`, `429`.

#### See also

- `POST /crm/leads/qualify/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Fields to change.

```json
{
  "phone": "+15551234567",
  "company": "Analytical Engines Ltd",
  "priority": "high"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | Failed to update lead — An unexpected failure in the leads service. |

## DELETE /crm/leads/detail/{id}

**Delete a lead**

`operationId: LeadsController_deleteLead`

Permanently deletes a lead. Disqualify it instead when you want to keep the record of why it went nowhere.

#### Signature

```http
DELETE /crm/leads/detail/{id} (id: string) -> Deletion result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `500` | OPERATION_FAILED | Failed to delete lead | An unexpected failure in the leads service. | Retry with backoff. The message is fixed text and does not indicate what went wrong. |

Plus the standard platform errors: `401`, `403`, `429`.

#### See also

- `POST /crm/leads/disqualify/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | Failed to delete lead — An unexpected failure in the leads service. |

## POST /crm/leads/qualify/{id}

**Qualify a lead**

`operationId: LeadsController_qualifyLead`

Marks a lead as qualified — worth pursuing. Notes are recorded with the transition.

#### Signature

```http
POST /crm/leads/qualify/{id} (id: string, body) -> The qualified lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/disqualify/{id}`
- `POST /crm/leads/convert/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Optional qualification notes.

```json
{
  "notes": "Budget confirmed, decision maker identified"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The qualified lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/disqualify/{id}

**Disqualify a lead**

`operationId: LeadsController_disqualifyLead`

Marks a lead as not worth pursuing, recording why. The record is kept, which is what makes source quality measurable later.

#### Signature

```http
POST /crm/leads/disqualify/{id} (id: string, body) -> The disqualified lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Prefer this to deleting — a disqualified lead with a reason is data; a deleted one is nothing.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/qualify/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Why the lead was disqualified.

```json
{
  "reason": "No budget this financial year"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The disqualified lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/convert/{id}

**Convert a lead**

`operationId: LeadsController_convertLead`

Converts a lead into a customer. Pass `customerId` to link it to an existing customer, or omit it to have one created.

`conversionValue` records what the conversion was worth, which is what makes the analytics endpoint able to report value by source rather than just counts.

#### Signature

```http
POST /crm/leads/convert/{id} (id: string, body) -> The converted lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Always send `conversionValue` where you know it — without it, conversion analytics only counts leads, not revenue.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/leads/analytics`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Conversion details.

```json
{
  "conversionValue": 4500
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The converted lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/assign/{id}

**Assign a lead**

`operationId: LeadsController_assignLead`

Assigns a lead to a user, making it appear in their queue. Assigning an already-assigned lead reassigns it.

#### Signature

```http
POST /crm/leads/assign/{id} (id: string, body) -> The assigned lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/pipelines/{id}/route-unassigned`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Who to assign it to.

```json
{
  "assignedTo": "sales@acme.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The assigned lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/follow-up/{id}

**Schedule a follow-up**

`operationId: LeadsController_scheduleFollowUp`

Sets a follow-up date on a lead so it resurfaces at the right time. Recording *why* in `notes` is what makes the reminder useful when it arrives.

#### Signature

```http
POST /crm/leads/follow-up/{id} (id: string, body) -> The updated lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/activities/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Request body

When to follow up, and why.

```json
{
  "followUpDate": "2026-09-15T09:00:00.000Z",
  "notes": "Check whether the new budget was approved"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/leads/forecast

**Pipeline forecast**

`operationId: LeadsController_getForecast`

A weighted forecast by expected close month and by owner. Open leads count at their stage's probability; a deal is a lead at a won stage. Months run from this month for `months` (1–24, default 6); earlier dates fall into `overdue`, later ones into `later`, undated into `unscheduled`. Totals flag leads with no value (`unpriced`) and stages with no probability.

#### Signature

```http
GET /crm/leads/forecast (pipelineId?: string, months?: integer) -> { months, byMonth, byOwner, totals: { openCount, openValue, weighted, wonCount, wonValue, lostCount, unpriced, withoutProbability }, pipelines }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `pipelineId` | query | string | — |  |
| `months` | query | integer | — |  |
| `endDate` | query | any | — | End date for analytics |
| `startDate` | query | any | — | Start date for analytics |
| `orgId` | query | any | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { months, byMonth, byOwner, totals: { openCount, openValue, weighted, wonCount, wonValue, lostCount, unpriced, withoutProbability }, pipelines } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/leads/analytics

**Get lead analytics**

`operationId: LeadsController_getLeadAnalytics`

Aggregate lead figures — volume by status and source, conversion rates and value. Conversion value is only meaningful where `conversionValue` was recorded at conversion time.

#### Signature

```http
GET /crm/leads/analytics (startDate?: string, endDate?: string) -> Aggregate lead analytics
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `500` | OPERATION_FAILED | Failed to retrieve lead analytics | An unexpected failure in the leads service. | Retry with backoff. The message is fixed text and does not indicate what went wrong. |

Plus the standard platform errors: `401`, `403`, `429`.

#### See also

- `POST /crm/leads/convert/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `startDate` | query | string | — | ISO date — start of the range. |
| `endDate` | query | string | — | ISO date — end of the range. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Aggregate lead analytics |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | Failed to retrieve lead analytics — An unexpected failure in the leads service. |

## POST /crm/leads/enrich/batch

**Enrich several leads**

`operationId: LeadsController_enrichLeadsBatch`

Enriches many leads in one call. Enrichment usually costs money per lead at the provider, so send only the leads that need it.

#### Signature

```http
POST /crm/leads/enrich/batch (body) -> Per-lead enrichment results
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Usually billed per lead by the enrichment provider.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/enrich/auto`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Which leads to enrich.

```json
{
  "leadIds": [
    "LEAD-4821",
    "LEAD-4822"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Per-lead enrichment results |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/leads/enrichment/status/{id}

**Get enrichment status**

`operationId: LeadsController_getLeadEnrichmentStatus`

Reports where a lead's enrichment has got to. Poll this after starting enrichment rather than assuming it completed synchronously.

#### Signature

```http
GET /crm/leads/enrichment/status/{id} (id: string) -> The enrichment status
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/enrich/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The enrichment status |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/enrich/auto

**Enrich leads automatically**

`operationId: LeadsController_autoEnrichLeads`

Runs enrichment across leads that qualify for it automatically, rather than naming them individually. Like the batch form, this can incur per-lead provider costs.

#### Signature

```http
POST /crm/leads/enrich/auto (limit?: string, body) -> The enrichment result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Selects leads on its own — check what it would touch before running it on a large org.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/enrich/batch`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `limit` | query | string | — | Maximum rows. |
| `orgId` | query | any | yes | Organization ID |

### Request body

Optional enrichment settings.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The enrichment result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/enrich/{id}

**Enrich a lead**

`operationId: LeadsController_enrichLead`

Fills in missing detail on a lead from external data sources — the counterpart to the deliberately thin creation rules. Capture what you have, then enrich.

#### Signature

```http
POST /crm/leads/enrich/{id} (id: string) -> The enrichment result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Enrichment may run asynchronously — poll `GET /crm/leads/enrichment/status/{id}` rather than assuming the response is final.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/leads/enrichment/status/{id}`
- `POST /crm/leads/enrich/batch`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |
| `orgId` | query | any | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The enrichment result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/leads/activities/{id}

**Get a lead's activities**

`operationId: LeadsController_getActivities`

The full interaction history for a lead.

#### Signature

```http
GET /crm/leads/activities/{id} (id: string) -> The lead's activities
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/activities/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The lead's activities |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/activities/{id}

**Add an activity to a lead**

`operationId: LeadsController_addActivity`

Records something that happened with a lead — a call, an email, a meeting. This is the interaction history a salesperson reads before picking up the phone.

#### Signature

```http
POST /crm/leads/activities/{id} (id: string, body) -> The recorded activity
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/leads/activities/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |

### Request body

The activity to record.

```json
{
  "type": "call",
  "notes": "Discussed pricing, sending a proposal",
  "date": "2026-08-29T14:00:00.000Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The recorded activity |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/score/{id}

**Calculate a lead score**

`operationId: LeadsController_calculateScore`

Scores one lead. Pass a rules array to score against specific criteria, or omit it to use the org's configured rules.

#### Signature

```http
POST /crm/leads/score/{id} (id: string, body) -> The lead with its computed score
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The body is a bare array, not an object wrapping one.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | LEAD_NOT_FOUND | Lead not found | No lead in the org has that id. | Check the id with `GET /crm/leads/detail`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/score/batch`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Lead id. |

### Request body

Scoring rules. Omit to use the configured ones.

```json
[
  {
    "field": "company",
    "condition": "exists",
    "points": 10
  }
]
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The lead with its computed score |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Lead not found — No lead in the org has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/score/batch

**Score all leads**

`operationId: LeadsController_batchCalculateScores`

Recomputes scores across **every** lead in the org. Run it after changing scoring rules; it is a heavy operation, not something to call per page view.

#### Signature

```http
POST /crm/leads/score/batch (body) -> The batch scoring result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Touches every lead in the org — treat it as an administrative operation.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/score/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Scoring rules. Omit to use the configured ones.

```json
[
  {
    "field": "company",
    "condition": "exists",
    "points": 10
  }
]
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The batch scoring result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/import/facebook

**Import leads from Facebook**

`operationId: LeadsController_importFromFacebook`

Queues a sync of Facebook lead-ad submissions into the CRM.

**The work is queued, not performed.** The response confirms the job was accepted — `{ success: true, message: "Facebook lead sync queued" }` — and says nothing about how many leads arrived. Check the lead list afterwards.

#### Signature

```http
POST /crm/leads/import/facebook (body) -> Confirmation that the sync was queued
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Asynchronous — a `200` means queued, not imported.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/import/contacts`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Optional import settings.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Facebook leads imported successfully |
| `201` | Confirmation that the sync was queued |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/import/contacts

**Import leads from contacts**

`operationId: LeadsController_importFromContacts`

Turns existing CRM contacts into leads, optionally dropping them straight into a pipeline stage.

Set `skipDuplicates` to leave contacts that are already leads alone — without it, importing the same contacts twice can produce duplicates.

#### Signature

```http
POST /crm/leads/import/contacts (body) -> The import result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Omitting `contactIds` imports every contact — check the scope before running it.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/bulk-import/contacts`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which contacts to import, and where to put them.

```json
{
  "contactIds": [
    "cus_4821",
    "cus_4822"
  ],
  "pipelineId": "PIPE-4821",
  "stageId": "STG-1",
  "source": "contact-import",
  "skipDuplicates": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The import result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/convert/contact/{contactId}

**Convert one contact into a lead**

`operationId: LeadsController_convertContactToLead`

Creates a lead from a single existing contact — the one-at-a-time counterpart to the import endpoints, for when someone in the contact book turns into an opportunity.

#### Signature

```http
POST /crm/leads/convert/contact/{contactId} (contactId: string, body) -> The created lead
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/import/contacts`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `contactId` | path | string | yes | Contact id. |

### Request body

Where to place the new lead.

```json
{
  "pipelineId": "PIPE-4821",
  "stageId": "STG-1",
  "source": "referral"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created lead |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/leads/bulk-import/contacts

**Bulk import contacts as leads**

`operationId: LeadsController_bulkImportContacts`

The bulk form of the contact import, for larger sets. `contactIds` is required here rather than optional, so it cannot accidentally import the whole contact book.

#### Signature

```http
POST /crm/leads/bulk-import/contacts (body) -> The import result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/leads/import/contacts`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The contacts to import.

```json
{
  "contactIds": [
    "cus_4821",
    "cus_4822"
  ],
  "pipelineId": "PIPE-4821",
  "stageId": "STG-1"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The import result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

