Skip to main content
POST
/
api
/
campaigns
/
{id}
/
launch
Launch a campaign
curl --request POST \
  --url https://app.puffle.ai/api/campaigns/{id}/launch \
  --header 'Authorization: Bearer <token>'
{ "success": true, "message": "Campaign launch started", "runId": "run_a1b2c3d4e5f6", "prospectsToProcess": 120 }

Overview

The call is synchronous about validation (returns 400 if the campaign isn’t ready) but asynchronous about sending — once it returns 200, a Trigger.dev task owns the work. Poll GET /api/campaigns/:id for progress. The status transitions atomically via compare-and-swap: draft → launching, then (inside the background task) launching → active. Only one launch can run at a time; a second concurrent call returns 409.

Error reference

StatusWhen
400Campaign is not in draft status — the message names the current status
400Campaign has no sender accounts, no pending prospects, or no sendable sequence nodes
400Sequence contains nodes with missing content — message lists the positions
400Email senders not in active status (warmup incomplete) or missing AgentMail inbox
400LinkedIn: connected account no longer exists in Unipile — reconnect required
400All prospects were skipped by dedup — nothing to send
401Missing or invalid Bearer token
404Campaign doesn’t exist or isn’t owned by the authenticated user
409A launch is already in progress (started within the last 2 min), or campaign state changed concurrently
500Message resolution or Trigger.dev dispatch failed. Status rolled back to draft; retry is safe
503LinkedIn: transient Unipile verification error. Retry after 30 s

AI agent notes

Before calling this endpoint, verify the campaign is ready:
  1. GET /api/campaigns/:idstatus must be "draft"
  2. At least one sender account is attached
  3. At least one prospect is in "pending" status
  4. Every sendable sequence node has non-empty content
  5. For email: senders are active (warmup complete) and have agentmail_inbox_id set
  6. For LinkedIn: the connected account resolves via Unipile
Running GET /api/campaigns/:id/validate first returns a specific blocker instead of waiting for a 400 here.
After a successful launch:
  • Status is "launching" immediately — do not re-launch (that’s the 409 path)
  • Within ~2 min the background task transitions to "active" and begins staggered sends
  • Poll GET /api/campaigns/:id every 30 s until status is "active", then at 1–5 min cadence for progress (read stats)
  • stats.total is set at launch; counters (sent, replied, bounced, etc.) increment as prospects execute
On 409 “launch in progress”: wait 2 min, then retry. A stuck-launching campaign older than 2 min is auto-reset to draft on the next attempt. Credits: launching itself is free. AI message regeneration at runtime and LinkedIn enrichment deduct credits — preview via POST /api/credits/preview. Idempotency: retrying the same call is safe — the background task is keyed by campaignId + updated_at, and on any failure the server rolls back all writes (status returns to draft; promoted message drafts revert; new pending messages are deleted). Stopping: use POST /api/campaigns/:id/pause to halt mid-flight; POST /api/campaigns/:id/resume to continue.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

id
string<uuid>
required

Campaign UUID. Must be in draft status and owned by the authenticated user.

Pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$

Response

Launch dispatched. Background task is running.

success
enum<boolean>
required
Available options:
true
message
string
required

Always "Campaign launch started" on success

runId
string
required

Trigger.dev run identifier. Keep for support — uniquely identifies this background execution across Vercel logs and the Trigger dashboard.

prospectsToProcess
integer
required

Number of prospects the background task will attempt to process. Equals stats.total once the task transitions the campaign to active.

Required range: 0 <= x <= 9007199254740991
warning
string

Present only when some prospects were auto-skipped. Describes the reason in human-readable form.

skippedProspects
object[]

LinkedIn launches only. Prospects cancelled because an invitation is already pending on LinkedIn. Each item identifies the cancelled prospect for UI feedback.

skippedCount
integer

Email launches only. Count of prospects cancelled because they're already active in another of the user's email campaigns (cross-campaign dedup).

Required range: 0 <= x <= 9007199254740991