---
title: "Resume a stranded campaign"
method: POST
path: "/v1/loyalty/campaigns/{id}/resume"
tags: ["Loyalty"]
---

# Resume a stranded campaign

`POST /v1/loyalty/campaigns/{id}/resume`

## Request

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

Resume a campaign stuck in sending after its send job failed terminally or vanished (no worker draining it). Re-enqueues the idempotent send job: recipients already delivered are skipped and any re-push stays bounded by the per-card frequency cap. Returns 202 with the campaign still in sending — the deliveries resume in the background. Allowed only when the campaign is genuinely stranded (sending with a failed/absent job); a send still in progress is rejected.

## Path parameters

- `id` string, uuid, required

## Response `202`

Campaign accepted for resuming

- 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 stranded and cannot be resumed

---

[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)
