---
title: "Delete a campaign"
method: DELETE
path: "/v1/loyalty/campaigns/{id}"
tags: ["Loyalty"]
---

# Delete a campaign

`DELETE /v1/loyalty/campaigns/{id}`

## Request

- Base URL: `https://api.prod.keyban.io`
- URL: `https://api.prod.keyban.io/v1/loyalty/campaigns/{id}`
- Auth: API key in header `X-Api-Key`

Delete a draft campaign. Only draft campaigns can be deleted; sent campaigns are kept for history.

## Path parameters

- `id` string, uuid, required

## Response `200`

Campaign deleted successfully

- CampaignDto
  - `id` string, uuid, required: Entity UUID
  - `createdAt` string, date-time, required: Creation date (ISO 8601)
  - `updatedAt` string, date-time, required: Last update date (ISO 8601)
  - `application` string, uuid, required: Application (brand) the campaign belongs to.
  - `name` string, required: Campaign name shown in the back-office campaign list.
  - `description` string, nullable: Internal note describing the campaign intent. Never sent to customers.
  - `channels` string[], required: Wallet channels the campaign targets — a non-empty subset of google/apple.
  - `targeting` object, nullable: Audience filter. Null or empty targets the whole opted-in audience.
    - `minPoints` integer: Inclusive lower bound on the loyalty point balance, in points. Omit for no lower bound.
    - `maxPoints` integer: Inclusive upper bound on the loyalty point balance, in points. Omit for no upper bound.
    - `minVisits` integer: Minimum number of distinct visit days over the application visit window (LoyaltySettings.visitsWindowDays). Omit to ignore visit frequency.
    - `maxVisits` integer: Inclusive upper bound on distinct visit days over the application visit window (LoyaltySettings.visitsWindowDays). Set to 0 to target customers with no visit in the window (inactive/win-back). Omit for no upper bound.
    - `rewardTierIds` string[]: Restrict to customers whose effective reward tier is one of these tier IDs. Omit to target every tier.
  - `content` object, nullable: The single message (headline + body) delivered to every selected channel. May be absent while the campaign is a draft; its presence is validated at send time (LOYALTY_CAMPAIGN_CONTENT_INCOMPLETE otherwise).
    - `header` string, required: Short notification headline shown on the wallet pass. Kept under 64 chars to fit Google Wallet message limits.
    - `body` string, required: Notification body text (≤ 255 chars). On Apple Wallet it populates the changeMessage field that makes the update visible as a push.
  - `sendAt` string, date-time, nullable: Scheduled send time (RFC 3339). Null means immediate send when triggered.
  - `status` 'draft' | 'scheduled' | 'sending' | 'sent' | 'finished', required: Lifecycle status. Server-managed: a campaign is always created as draft.
  - `audienceSnapshotCount` integer, nullable: Audience size frozen when sending starts: the count of opted-in accounts matching the targeting, BEFORE the per-recipient consent and frequency-cap exclusions. It can therefore be higher than the number of actual deliveries. Null until the campaign starts sending.
  - `conversionWindowDays` integer, required: Days after sending during which conversions are measured. Defaults to 7.
  - `sentAt` string, date-time, nullable: Timestamp when sending started (anchors the conversion window). Null until sent.
  - `isStranded` boolean: Computed flag, set only on the campaign detail endpoint: true when the campaign is stuck in `sending` because its send job failed or vanished (no worker is draining it), so it can be resumed via POST /:id/resume. Absent on list responses and for any non-sending campaign.
  - `scheduledImmediately` boolean: Computed flag, set only on the POST /:id/schedule response: true when the requested sendAt was already in the past, so the campaign fires immediately instead of waiting. Absent (never false) in the nominal case where sendAt is in the future and the campaign is scheduled for later.
  - `scheduleJobLost` boolean: Computed flag, set only on the campaign detail endpoint: true when the campaign is stuck in `scheduled` past its due date because its delayed trigger vanished (no job will fire it). An hourly safety-net sweep re-arms it automatically, so this is informational and self-heals. Absent on list responses and for any non-scheduled or not-yet-due campaign.

## Other responses

- `409`: Campaign is not a draft and cannot be deleted

---

[API](https://skmtc.dev/keyban/apis/keyban-rest-apis.md) · [All operations](https://skmtc.dev/keyban/apis/keyban-rest-apis/llms.txt) · [OpenAPI document](https://skmtc.dev/keyban/apis/keyban-rest-apis/revisions/0bb79a1da961?raw)
