---
title: "Delete a campaign"
method: DELETE
path: "/v1/ads/campaigns/{campaignId}"
tags: ["Ad Campaigns"]
---

# Delete a campaign

`DELETE /v1/ads/campaigns/{campaignId}`

## Request

- Base URL: `https://zernio.com/api`
- URL: `https://zernio.com/api/v1/ads/campaigns/{campaignId}`
- Auth: HTTP bearer

Deletes a campaign on its platform, including a campaign that never got any ads.

Only the campaign itself is sent to the platform. Meta, Google, TikTok, LinkedIn
and X delete it; OpenAI, Reddit and Pinterest have no campaign delete and archive
it instead (see below). Locally, all Ad documents for this campaign are marked
`status: cancelled`.

**Empty campaigns.** A campaign with zero ads has no local Ad documents
to resolve, so it is invisible to `/v1/ads/tree` and this endpoint would
404. That state is produced by the two-step create flow (campaign, then
ads via `existingCampaignId`) whenever Meta rejects the ad step. To
delete such a shell, send `accountId` in the body or the query string:
we skip the local lookup entirely and forward the delete to the platform
of that connection. Google, TikTok, LinkedIn, Pinterest and Reddit take the
ad account from the synced campaign (OpenAI needs none); when the campaign
was never synced (or is synced under several ad accounts) the answer is 409
`invalid_resource_state` on `campaignId` and nothing is deleted: sync the ad
account and retry. Before deleting on Google, TikTok, LinkedIn, Pinterest,
OpenAI or Reddit, Zernio asks the platform whether the campaign still holds
ad groups or ads (Google asset groups count; on LinkedIn, campaigns inside
the campaign group). If it does, or the platform cannot confirm it has none
(a TikTok Smart+ campaign), the answer is 409 `invalid_resource_state` on
`campaignId` and nothing is deleted: sync the ad account and delete the
campaign again so its ads are cancelled with it, or delete them on the
platform first. On X the ad account comes
from the synced campaign, which wins over `adAccountId`; when the campaign
was never synced, send `adAccountId` too. Before deleting, Zernio asks X
whether the campaign still holds undeleted line items (ads not synced
yet); if it does the answer is 409 `invalid_field_value` on `campaignId`
and nothing is deleted. Sync the ad account and retry when those line items
hold promoted posts; line items without promoted posts never sync, so delete
them in X Ads Manager first. `accountId` is ignored when the campaign does have ads.

Reddit Ads has no delete: the campaign is archived instead (`configured_status`
ARCHIVED), which stops delivery for good. Its ad groups and ads keep their own
switches but cannot deliver under an archived campaign.

OpenAI Ads has no delete either: the campaign is archived (state `archive`).
Its ad groups and ads are not touched on OpenAI.

## Path parameters

- `campaignId` string, required

## Request body

- object
  - `platform` 'facebook' | 'instagram' | 'tiktok' | 'linkedin' | 'pinterest' | 'google' | 'twitter' | 'openai' | 'reddit', required
  - `accountId` string: Zernio SocialAccount id owning the ad account. Required only to delete an EMPTY campaign (zero ads), which has no local Ad documents to resolve a token from.
  - `adAccountId` string: X only, for an EMPTY campaign that was never synced: the X ad account id the campaign belongs to (lowercase letters and digits). The synced campaign's ad account wins when there is one. Also accepted on the query string.

## Response `200`

Campaign deleted

- object
  - `deleted` boolean
  - `adCount` integer: Number of local Ad docs marked cancelled

## Other responses

- `400`: Invalid request
- `401`: Missing or invalid API key. `code` is `missing_credentials` when no Authorization header was sent and `invalid_credentials` when the key is unknown, revoked or expired.
- `403`: The connection has selected ad accounts (its ad-account scope, set with the dashboard picker or GET /v1/connect/{platform}/ads with adAccountIds) and the ad account this request targets is not among them, so the connection may not act on it even though its token can reach it. Code `ad_account_not_selected`. `details` carries `accountId` (the connection), `adAccountId` (the refused ad account) and `selectedAdAccountIds`. Add the ad account to the selection (adAccountIds replaces the whole selection, so pass the ids already selected plus this one) or use a connection that has it selected. Connections without a selection keep reaching every ad account their token can manage.
- `404`: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
- `409`: Either the ads connection is inactive or needs reconnection (code `ads_connection_required`, param `accountId`): reconnect it, then read GET /v1/accounts for its current account ID before retrying. Or, on X only, the empty campaign still holds line items on X (code `invalid_field_value`, param `campaignId`) and nothing was deleted: sync the ad account when they hold promoted posts, otherwise delete them in X Ads Manager, then retry. Or, on Google, TikTok, LinkedIn, Pinterest, OpenAI or Reddit, the empty campaign has no synced record naming its ad account, or the platform still lists ad groups or ads under it (code `invalid_resource_state`, param `campaignId`) and nothing was deleted: sync the ad account, then retry.
- `501`: Operation not supported on this platform

## Changes

- **2026-10-20** `450ec6ec5b0e`: 1 info
  - added the non-success response with the status `403`
- **2026-10-13** `27c96850cc66`: 7 info
  - added the new optional request property `adAccountId`
  - added the new `linkedin` enum value to the request property `platform`
  - added the new `openai` enum value to the request property `platform`
  - added the new `pinterest` enum value to the request property `platform`
  - …3 more
- **2026-10-06** `f8c8f34fb41a`: 4 warning
  - added the new `account` enum value to the `details/budgetScope` response property for the response status `400`
  - added the new `account` enum value to the `details/budgetScope` response property for the response status `401`
  - added the new `account` enum value to the `details/budgetScope` response property for the response status `404`
  - added the new `account` enum value to the `details/budgetScope` response property for the response status `409`
- **2026-10-02** `d556591af3f6`: 10 info
  - added the optional property `code` to the response with the `401` status
  - added the optional property `details` to the response with the `401` status
  - added the optional property `docUrl` to the response with the `400` status
  - added the optional property `docUrl` to the response with the `401` status
  - …6 more
- …earlier changes not shown

[Operation history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/ads/campaigns/:campaignId/delete.md)

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) · [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc.dev/zernio/apis/zernio-api/revisions/4e74db179e73?raw)
