# CRM · Ads

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /crm/ads/campaigns

**List ad campaigns**

`operationId: AdsController_getCampaigns`

Lists campaigns with optional filters and paging.

#### Signature

```http
GET /crm/ads/campaigns (status?: string, platform?: string, page?: integer, limit?: integer) -> A page of campaigns
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Paging uses `limit`, not the platform-standard `pageSize`.

#### Errors

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

#### See also

- `GET /crm/ads/campaigns/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | "draft" \| "scheduled" \| "active" \| "paused" \| "completed" \| "failed" | — |  |
| `platform` | query | "facebook" \| "instagram" \| "google" \| "tiktok" \| "linkedin" \| "twitter" | — |  |
| `page` | query | integer | — |  |
| `limit` | query | integer | — | Page size. Note this is `limit`, not `pageSize` as elsewhere on the platform. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of campaigns |
| `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/ads/campaigns

**Create an ad campaign**

`operationId: AdsController_createCampaign`

Creates a campaign. It starts as a draft — creating does not spend money or push anything to a platform; `POST /crm/ads/campaigns/launch/{campaignId}` does that.

#### Signature

```http
POST /crm/ads/campaigns (body) -> The created campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Creating is free — nothing reaches a platform until it is launched.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PLATFORM_ERROR | The ad platform rejected the request. | The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. | The platform's own message is passed through. Check the integration is still connected with `GET /crm/ads/integrations/status`. |

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

#### See also

- `POST /crm/ads/campaigns/launch/{campaignId}`

### Request body

The campaign to create.

```json
{
  "name": "Summer sale — retargeting",
  "platforms": [
    "facebook",
    "instagram"
  ],
  "budget": 5000,
  "objective": "conversions",
  "startDate": "2026-09-01T00:00:00.000Z",
  "endDate": "2026-09-30T23:59:59.000Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created campaign |
| `400` | The ad platform rejected the request. — The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. |
| `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/ads/campaigns/{campaignId}

**Get an ad campaign**

`operationId: AdsController_getCampaign`

Fetches one campaign with its budget, targeting and current status.

#### Signature

```http
GET /crm/ads/campaigns/{campaignId} (campaignId: string) -> The campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `GET /crm/ads/campaigns/metrics/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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. |

## PUT /crm/ads/campaigns/{campaignId}

**Update an ad campaign**

`operationId: AdsController_updateCampaign`

Updates a campaign's settings. Changes to a live campaign are pushed to the platform, where some fields cannot be edited once running — the platform decides, and rejects with a `400` if not.

#### Signature

```http
PUT /crm/ads/campaigns/{campaignId} (campaignId: string, body) -> The updated campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Pause a running campaign before making structural changes — many platforms refuse edits to an active campaign.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `POST /crm/ads/campaigns/pause/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Request body

Fields to change.

```json
{
  "budget": 7500
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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. |

## DELETE /crm/ads/campaigns/{campaignId}

**Delete an ad campaign**

`operationId: AdsController_deleteCampaign`

Deletes a campaign. Pause it first if it is running — deleting does not guarantee the platform stops serving immediately.

#### Signature

```http
DELETE /crm/ads/campaigns/{campaignId} (campaignId: string) -> Deletion result
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Historical spend and metrics may be lost with the campaign — export analytics first if you need them.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `POST /crm/ads/campaigns/pause/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/campaigns/launch/{campaignId}

**Launch a campaign**

`operationId: AdsController_launchCampaign`

Pushes a campaign to its platforms and starts it running.

**This starts spending money.** From here the budget is consumed by the ad platforms, and stopping it requires an explicit pause.

#### Signature

```http
POST /crm/ads/campaigns/launch/{campaignId} (campaignId: string) -> The launched campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Spend begins immediately. A campaign that is rejected on one platform may still run on the others — check the result per platform.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `POST /crm/ads/campaigns/pause/{campaignId}`
- `POST /crm/ads/campaigns/schedule/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The launched campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/campaigns/pause/{campaignId}

**Pause a campaign**

`operationId: AdsController_pauseCampaign`

Stops a running campaign from serving and spending. The campaign and its history are kept, and it can be resumed.

#### Signature

```http
POST /crm/ads/campaigns/pause/{campaignId} (campaignId: string) -> The paused campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Platforms may take a few minutes to stop serving after a pause is accepted.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `POST /crm/ads/campaigns/resume/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The paused campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/campaigns/resume/{campaignId}

**Resume a campaign**

`operationId: AdsController_resumeCampaign`

Restarts a paused campaign. Spending resumes against the remaining budget.

#### Signature

```http
POST /crm/ads/campaigns/resume/{campaignId} (campaignId: string) -> The resumed campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `POST /crm/ads/campaigns/pause/{campaignId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The resumed campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/campaigns/schedule/{campaignId}

**Schedule a campaign**

`operationId: AdsController_scheduleCampaign`

Sets a campaign to start automatically at a future date rather than launching it now. Give an `endDate` to have it stop on its own.

#### Signature

```http
POST /crm/ads/campaigns/schedule/{campaignId} (campaignId: string, body) -> The scheduled campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Without an `endDate` the campaign runs until it is paused or the budget runs out.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `GET /crm/ads/scheduled`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Request body

When the campaign should run.

```json
{
  "startDate": "2026-09-01T00:00:00.000Z",
  "endDate": "2026-09-30T23:59:59.000Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The scheduled campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/campaigns/duplicate/{campaignId}

**Duplicate a campaign**

`operationId: AdsController_duplicateCampaign`

Copies a campaign into a new draft, optionally onto different platforms, with a different name or budget. The fast path for running a proven campaign again or extending it to another channel.

The duplicate is a **draft** — it does not inherit the original's running state.

#### Signature

```http
POST /crm/ads/campaigns/duplicate/{campaignId} (campaignId: string, body) -> The new draft campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- The copy starts as a draft and must be launched separately.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `POST /crm/ads/campaigns/from-template/{templateId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |

### Request body

What to change in the copy.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The new draft campaign |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/campaigns/bulk/launch

**Launch several campaigns**

`operationId: AdsController_bulkLaunchCampaigns`

Launches many campaigns in one call.

**This starts spending on every campaign named.** Individual failures are reported per campaign rather than failing the batch, so check the response — a `200` means the batch ran, not that everything launched.

#### Signature

```http
POST /crm/ads/campaigns/bulk/launch (body) -> Per-campaign launch results
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Starts real spend across every campaign in the list. Inspect each result.

#### Errors

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

#### See also

- `POST /crm/ads/campaigns/bulk/pause`

### Request body

Which campaigns to launch.

```json
{
  "campaignIds": [
    "CAMP-4821",
    "CAMP-4822"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Per-campaign launch 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. |

## POST /crm/ads/campaigns/bulk/pause

**Pause several campaigns**

`operationId: AdsController_bulkPauseCampaigns`

Pauses many campaigns at once — the emergency stop when spend needs to halt across the board. Results are reported per campaign.

#### Signature

```http
POST /crm/ads/campaigns/bulk/pause (body) -> Per-campaign pause results
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Check every result — a campaign that failed to pause is still spending.

#### Errors

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

#### See also

- `POST /crm/ads/campaigns/bulk/launch`

### Request body

Which campaigns to pause.

```json
{
  "campaignIds": [
    "CAMP-4821",
    "CAMP-4822"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Per-campaign pause 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/ads/campaigns/metrics/{campaignId}

**Get campaign metrics**

`operationId: AdsController_getCampaignMetrics`

Live performance figures for one campaign, pulled from its platforms — impressions, clicks, spend and conversions.

#### Signature

```http
GET /crm/ads/campaigns/metrics/{campaignId} (campaignId: string, startDate?: string, endDate?: string, platforms?: string, groupBy?: string, metrics?: string) -> Campaign metrics
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Figures come from the ad platforms and lag real time — most report on their own delay.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CAMPAIGN_NOT_FOUND | Campaign not found | No campaign in the org has that id. | Check the id with `GET /crm/ads/campaigns`. **Note this is a `400`, not a `404`** — this controller never returns 404. |

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

#### See also

- `GET /crm/ads/analytics/overview`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaignId` | path | string | yes | Campaign id. |
| `startDate` | query | string | — | Start of the reporting period. |
| `endDate` | query | string | — | End of the reporting period. |
| `platforms` | query | string | — | Restrict to specific platforms. Repeat the parameter for several. |
| `groupBy` | query | "platform" \| "date" \| "campaign" | — | How results are aggregated. |
| `metrics` | query | string | — | Which metrics to return. Repeat for several. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Campaign metrics |
| `400` | Campaign not found — No campaign in the org has that id. |
| `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/ads/analytics/overview

**Get an advertising overview**

`operationId: AdsController_getAnalyticsOverview`

Headline advertising figures across every campaign and platform for a period — total spend, reach and conversions.

#### Signature

```http
GET /crm/ads/analytics/overview (startDate?: string, endDate?: string, platforms?: string, groupBy?: string, metrics?: string) -> Overview metrics
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `GET /crm/ads/analytics/performance`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `startDate` | query | string | — | Start of the reporting period. |
| `endDate` | query | string | — | End of the reporting period. |
| `platforms` | query | string | — | Restrict to specific platforms. Repeat the parameter for several. |
| `groupBy` | query | "platform" \| "date" \| "campaign" | — | How results are aggregated. |
| `metrics` | query | string | — | Which metrics to return. Repeat for several. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Overview metrics |
| `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/ads/analytics/performance

**Get performance analytics**

`operationId: AdsController_getPerformanceAnalytics`

Detailed performance breakdown — cost per result, conversion rates and efficiency by whatever `groupBy` selects.

#### Signature

```http
GET /crm/ads/analytics/performance (startDate?: string, endDate?: string, platforms?: string, groupBy?: string, metrics?: string) -> Performance analytics
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `GET /crm/ads/analytics/comparison`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `startDate` | query | string | — | Start of the reporting period. |
| `endDate` | query | string | — | End of the reporting period. |
| `platforms` | query | string | — | Restrict to specific platforms. Repeat the parameter for several. |
| `groupBy` | query | "platform" \| "date" \| "campaign" | — | How results are aggregated. |
| `metrics` | query | string | — | Which metrics to return. Repeat for several. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Performance 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` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/ads/analytics/comparison

**Compare platforms**

`operationId: AdsController_getPlatformComparison`

Puts platforms side by side over the same period — the read behind "is TikTok outperforming Facebook for us".

#### Signature

```http
GET /crm/ads/analytics/comparison (startDate?: string, endDate?: string, platforms?: string, groupBy?: string, metrics?: string) -> Platform comparison
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Platforms define metrics like a conversion differently, so treat cross-platform comparisons as directional.

#### Errors

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

#### See also

- `GET /crm/ads/analytics/trends`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `startDate` | query | string | — | Start of the reporting period. |
| `endDate` | query | string | — | End of the reporting period. |
| `platforms` | query | string | — | Restrict to specific platforms. Repeat the parameter for several. |
| `groupBy` | query | "platform" \| "date" \| "campaign" | — | How results are aggregated. |
| `metrics` | query | string | — | Which metrics to return. Repeat for several. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Platform comparison |
| `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/ads/analytics/trends

**Get advertising trends**

`operationId: AdsController_getTrends`

Performance over time, for spotting drift in cost or effectiveness before it shows up in a monthly total.

#### Signature

```http
GET /crm/ads/analytics/trends (startDate?: string, endDate?: string, platforms?: string, groupBy?: string, metrics?: string) -> Trend data
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `GET /crm/ads/analytics/overview`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `startDate` | query | string | — | Start of the reporting period. |
| `endDate` | query | string | — | End of the reporting period. |
| `platforms` | query | string | — | Restrict to specific platforms. Repeat the parameter for several. |
| `groupBy` | query | "platform" \| "date" \| "campaign" | — | How results are aggregated. |
| `metrics` | query | string | — | Which metrics to return. Repeat for several. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Trend data |
| `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/ads/campaigns/cross-platform

**Create a cross-platform campaign**

`operationId: AdsController_createCrossPlatformCampaign`

Creates one campaign that runs across several platforms with a split budget and platform-specific settings.

`platformSpecific` holds the per-platform overrides, keyed by platform name; `distribution.budgetSplit` divides the budget between them, and `distribution.priority` orders which platforms are set up first.

#### Signature

```http
POST /crm/ads/campaigns/cross-platform (body) -> The created cross-platform campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- `budgetSplit` is not validated against the total budget — the two can disagree.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PLATFORM_ERROR | The ad platform rejected the request. | The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. | The platform's own message is passed through. Check the integration is still connected with `GET /crm/ads/integrations/status`. |

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

#### See also

- `POST /crm/ads/campaigns`

### Request body

The cross-platform campaign definition.

```json
{
  "name": "Summer sale — cross platform",
  "budget": 10000,
  "platformSpecific": {
    "facebook": {
      "objective": "conversions"
    },
    "tiktok": {
      "objective": "traffic"
    }
  },
  "distribution": {
    "budgetSplit": {
      "facebook": 6000,
      "tiktok": 4000
    },
    "priority": [
      "facebook",
      "tiktok"
    ]
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created cross-platform campaign |
| `400` | The ad platform rejected the request. — The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. |
| `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/ads/templates

**List campaign templates**

`operationId: AdsController_getCampaignTemplates`

The saved campaign templates, optionally filtered by category.

#### Signature

```http
GET /crm/ads/templates (category?: string) -> Campaign templates
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/templates`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `category` | query | string | — | Filter by category. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Campaign templates |
| `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/ads/templates

**Create a campaign template**

`operationId: AdsController_createCampaignTemplate`

Saves a reusable campaign setup, so a recurring campaign shape does not have to be rebuilt each time.

#### Signature

```http
POST /crm/ads/templates (body) -> The created template
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/campaigns/from-template/{templateId}`

### Request body

The template to save.

```json
{
  "name": "Seasonal retargeting",
  "description": "Standard retargeting setup",
  "category": "retargeting",
  "defaultSettings": {
    "objective": "conversions",
    "platforms": [
      "facebook",
      "instagram"
    ]
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created template |
| `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/ads/campaigns/from-template/{templateId}

**Create a campaign from a template**

`operationId: AdsController_createCampaignFromTemplate`

Builds a new draft campaign from a saved template. Like duplication, the result is a draft and must be launched separately.

#### Signature

```http
POST /crm/ads/campaigns/from-template/{templateId} (templateId: string, body) -> The created draft campaign
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `GET /crm/ads/templates`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `templateId` | path | string | yes | Template id. |

### Request body

Overrides for the template defaults.

```json
{
  "name": "Autumn sale — retargeting",
  "budget": 3000
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created draft campaign |
| `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/ads/accounts

**List connected ad accounts**

`operationId: AdsController_getAdAccounts`

The ad accounts connected to this org across platforms — what campaigns can actually be published to.

#### Signature

```http
GET /crm/ads/accounts (platform?: string) -> Connected ad accounts
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/accounts/sync`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `platform` | query | string | — | Ad platform key, e.g. `facebook`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Connected ad accounts |
| `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/ads/accounts/sync

**Sync ad accounts**

`operationId: AdsController_syncAdAccounts`

Refreshes the connected account list from the platforms, picking up accounts added or removed on their side since the last sync.

#### Signature

```http
POST /crm/ads/accounts/sync () -> The sync result
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PLATFORM_ERROR | The ad platform rejected the request. | The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. | The platform's own message is passed through. Check the integration is still connected with `GET /crm/ads/integrations/status`. |

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

#### See also

- `GET /crm/ads/accounts`

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The sync result |
| `400` | The ad platform rejected the request. — The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. |
| `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/ads/creatives

**List creatives**

`operationId: AdsController_getCreatives`

The registered ad creatives, optionally filtered by type or platform.

#### Signature

```http
GET /crm/ads/creatives (type?: string, platform?: string) -> Creatives
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/creatives`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `type` | query | string | — | Filter by creative type. |
| `platform` | query | "facebook" \| "instagram" \| "google" \| "tiktok" \| "linkedin" \| "twitter" | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Creatives |
| `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/ads/creatives

**Upload a creative**

`operationId: AdsController_uploadCreative`

Registers an ad creative — an image, video or copy variant — for use across the named platforms. Supply either a `url` or an uploaded `file`.

#### Signature

```http
POST /crm/ads/creatives (body) -> The registered creative
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Platforms enforce their own size and aspect-ratio rules — a creative accepted here can still be rejected at launch.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PLATFORM_ERROR | The ad platform rejected the request. | The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. | The platform's own message is passed through. Check the integration is still connected with `GET /crm/ads/integrations/status`. |

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

#### See also

- `GET /crm/ads/creatives`

### Request body

The creative to register.

```json
{
  "platforms": [
    "facebook",
    "instagram"
  ],
  "url": "https://cdn.appmint.io/ads/summer-hero.jpg",
  "metadata": {
    "width": 1200,
    "height": 628,
    "format": "jpg"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered creative |
| `400` | The ad platform rejected the request. — The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. |
| `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/ads/audiences

**List audiences**

`operationId: AdsController_getAudiences`

The defined targeting audiences, optionally filtered by platform.

#### Signature

```http
GET /crm/ads/audiences (platform?: string) -> Audiences
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/audiences`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `platform` | query | "facebook" \| "instagram" \| "google" \| "tiktok" \| "linkedin" \| "twitter" | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Audiences |
| `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/ads/audiences

**Create an audience**

`operationId: AdsController_createAudience`

Defines a targeting audience and pushes it to the named platforms, so the same definition can be reused across campaigns instead of being rebuilt per campaign.

#### Signature

```http
POST /crm/ads/audiences (body) -> The created audience
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Platforms impose minimum audience sizes and may reject one that is too small to protect privacy.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PLATFORM_ERROR | The ad platform rejected the request. | The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. | The platform's own message is passed through. Check the integration is still connected with `GET /crm/ads/integrations/status`. |

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

#### See also

- `GET /crm/ads/audiences`

### Request body

The audience to create.

```json
{
  "name": "Cart abandoners — 30 days",
  "platforms": [
    "facebook",
    "instagram"
  ],
  "targetingCriteria": {
    "event": "cart_abandoned",
    "withinDays": 30
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created audience |
| `400` | The ad platform rejected the request. — The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. |
| `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/ads/automation/rules

**List automation rules**

`operationId: AdsController_getAutomationRules`

The automation rules configured for this org — worth checking first when a campaign changed state without anyone doing it.

#### Signature

```http
GET /crm/ads/automation/rules () -> Automation rules
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/automation/rules`

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Automation rules |
| `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/ads/automation/rules

**Create an automation rule**

`operationId: AdsController_createAutomationRule`

Defines a rule that acts on campaigns automatically — pausing one whose cost per result climbs too high, or shifting budget toward a performer.

A rule is a `trigger` (what to watch), `conditions` (when it applies) and `actions` (what to do). Because the actions change live campaigns and therefore spend, test a rule on one campaign before applying it broadly.

#### Signature

```http
POST /crm/ads/automation/rules (body) -> The created rule
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Rules act on live campaigns without confirmation. A badly-scoped rule can pause everything you are running.

#### Errors

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

#### See also

- `GET /crm/ads/automation/rules`

### Request body

The rule to create.

```json
{
  "name": "Pause on high CPA",
  "platforms": [
    "facebook"
  ],
  "trigger": {
    "metric": "cpa",
    "check": "hourly"
  },
  "conditions": [
    {
      "metric": "cpa",
      "operator": ">",
      "value": 50
    }
  ],
  "actions": [
    {
      "type": "pause_campaign"
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created rule |
| `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/ads/reports/generate

**Generate an advertising report**

`operationId: AdsController_generateReport`

Builds a report over a period and set of campaigns. Reports are retrieved afterwards from `GET /crm/ads/reports`.

#### Signature

```http
POST /crm/ads/reports/generate (body) -> The generated report, or a handle to it
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Generation may be asynchronous — check the reports list rather than assuming the response is the finished report.

#### Errors

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

#### See also

- `GET /crm/ads/reports`

### Request body

What the report should cover.

```json
{
  "startDate": "2026-08-01",
  "endDate": "2026-08-31",
  "platforms": [
    "facebook",
    "tiktok"
  ],
  "groupBy": "platform"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The generated report, or a handle to it |
| `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/ads/reports

**List advertising reports**

`operationId: AdsController_getReports`

The reports generated for this org.

#### Signature

```http
GET /crm/ads/reports () -> Reports
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/reports/generate`

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Reports |
| `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/ads/activity

**Get advertising activity**

`operationId: AdsController_getAdsActivity`

The activity log across advertising — launches, pauses, edits and automation-rule firings. The audit trail for "why did this campaign stop".

#### Signature

```http
GET /crm/ads/activity (platform?: string, type?: string, limit?: integer, offset?: integer) -> Activity entries
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Paging is offset-based here, unlike the campaign list which pages by number.

#### Errors

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

#### See also

- `GET /crm/ads/automation/rules`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `platform` | query | "facebook" \| "instagram" \| "google" \| "tiktok" \| "linkedin" \| "twitter" | — |  |
| `type` | query | string | — | Filter by activity type. |
| `limit` | query | integer | — |  |
| `offset` | query | integer | — | Offset paging — this endpoint uses `offset`, not `page`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Activity entries |
| `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/ads/integrations/status

**Get platform integration status**

`operationId: AdsController_getIntegrationsStatus`

Whether each ad platform integration is connected and healthy. Check this first when campaigns fail to launch — an expired token presents as a platform error at launch time.

#### Signature

```http
GET /crm/ads/integrations/status () -> Integration status per platform
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/integrations/test/{platform}`

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Integration status per platform |
| `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/ads/integrations/test/{platform}

**Test a platform integration**

`operationId: AdsController_testIntegration`

Makes a live call to one platform to confirm the credentials still work. Nothing is published — this only verifies the connection.

#### Signature

```http
POST /crm/ads/integrations/test/{platform} (platform: string) -> The test result
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.
- Read-only against the platform — safe to run at any time.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PLATFORM_ERROR | The ad platform rejected the request. | The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. | The platform's own message is passed through. Check the integration is still connected with `GET /crm/ads/integrations/status`. |

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

#### See also

- `GET /crm/ads/integrations/status`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `platform` | path | string | yes | Platform to test — one of `facebook`, `instagram`, `google`, `tiktok`, `linkedin`, `twitter`. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The test result |
| `400` | The ad platform rejected the request. — The upstream ad platform returned an error — a policy rejection, an expired token, or an invalid targeting spec. |
| `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/ads/scheduled

**List scheduled campaigns**

`operationId: AdsController_getScheduledCampaigns`

Campaigns waiting to start automatically — what is about to begin spending, and when.

#### Signature

```http
GET /crm/ads/scheduled () -> Scheduled campaigns
```

#### Access

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

#### Notes

- This controller takes **no `orgid` header**. The organization comes from `user.orgId` on the authenticated token, so a token scoped to the wrong org silently operates on the wrong data.

#### Errors

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

#### See also

- `POST /crm/ads/campaigns/schedule/{campaignId}`

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Scheduled campaigns |
| `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. |

