Campaigns and targets

GET /api/campaigns lists call campaigns only (type = "call"). Email campaigns are at Email campaign endpoints.

Campaigns

GET /api/campaigns

Returns call campaigns with aggregated target counts.

[
  {
    "id": "uuid",
    "name": "Campaign Name",
    "type": "call",
    "prefered_phone": "+1234567890",
    "status": "running",
    "max_concurrent_calls": 10,
    "window_start": "09:00:00",
    "window_end": "17:00:00",
    "allow_weekends": false,
    "max_attempts": 4,
    "wait_hours_to_recall": 24,
    "pending_count": 100,
    "in_progress_count": 5,
    "completed_count": 50,
    "failed_count": 10,
    "total_count": 165,
    "created_at": "2026-01-01T00:00:00.000Z",
    "updated_at": "2026-01-01T00:00:00.000Z"
  }
]

Note prefered_phone — the misspelling matches the Prisma field.

POST /api/campaigns

Body

name, max_concurrent_calls, prefered_phone, window_start, window_end, allow_weekends, max_attempts, wait_hours_to_recall

Response

201 with a single campaign object.

GET /api/campaigns/{id}

Single campaign with target counts, including type. The dashboard at /campaigns/{id} is voice-only, so a type of email causes a client-side redirect to /email-campaigns/{id}.

PATCH /api/campaigns/{id}

All fields optional; only supplied fields are updated. status accepts draft, running, paused, stopped, completed.

Set max_attempts explicitly — its fallback differs between code paths. See Call campaigns.

Targets

GET /api/campaigns/{id}/targets

Paginated targets. page (default 1), pageSize (default 50, max 200).

Filters

search (phone, partial), states (repeatable), isAmazonSeller, validConversation, meetsMonthlyThreshold, stopCalling, disposition, nextAction, hubspotSyncStatus, riskLevel, maxAttempts

When the target is linked to a TargetPhoneNumbers row, registry fields are included: target_carrier_name, target_line_type, target_risk_level, target_is_blocked, target_phone_registry_updated_at.

{
  "data": [
    {
      "id": "uuid",
      "campaign_id": "uuid",
      "phone_number": "+1234567890",
      "target_carrier_name": "Example Carrier",
      "target_line_type": "mobile",
      "target_risk_level": "low",
      "target_is_blocked": false,
      "target_phone_registry_updated_at": "2026-01-01T00:00:00.000Z",
      "state": "pending",
      "attempts": 0,
      "last_error": null,
      "isAmazonSeller": null,
      "meetsMonthlyThreshold": null,
      "interested_in_daily_payments": null,
      "stop_calling": false,
      "disposition": null,
      "last_call_recording_url": null,
      "next_action": null,
      "hubspot_contact_id": null,
      "hubspot_engagement_id": null,
      "hubspot_sync_status": null,
      "valid_conversation": null,
      "last_attempt_at": null,
      "next_earliest_attempt_at": null,
      "lease_expires_at": null,
      "payload": {},
      "tz": "America/New_York",
      "created_at": "2026-01-01T00:00:00.000Z",
      "updated_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "total": 100,
  "page": 1,
  "pageSize": 50
}

isAmazonSeller and meetsMonthlyThreshold are emitted in camelCase here, unlike the rest of the payload.

POST /api/campaigns/{id}/targets

Single target:

{ "phone_number": "+1234567890", "state": "pending", "payload": { "first_name": "John" } }

Bulk:

{ "targets": [ { "phone_number": "+1234567890", "payload": {} } ] }
Response

201 with the created targets.

Behaviour:

  • Phone numbers are trimmed and validated.

  • tz is auto-detected from the area code.

  • A HubSpot contact id may be supplied in payload under any of Record ID, record_id, RecordId or hubspot_id — the variants accommodate different HubSpot CSV exports.

  • payload is stored as payloadJson and is passed to Retell as dynamic variables, so its keys are addressable from the agent prompt.

PATCH /api/campaigns/{id}/targets/{targetId}

Update a single target, e.g. { "state": "completed", "disposition": "Interested" }.

DELETE /api/campaigns/{id}/targets/{targetId}

Remove a target from the campaign.

PATCH /api/campaigns/{id}/targets/bulk-update

Apply the same change to many targets in one request — used for bulk state resets and cancellations.

Campaign calls

GET /api/campaigns/{id}/calls

Same filters as GET /api/calls minus campaignId (fixed by the URL), plus targetId. See Call endpoints.

Coverage

GET /api/campaigns/{id}/coverage

Reports how well the workspace’s outbound numbers cover the campaign’s target geography — which states hold targets but have no local source number. Backed by lib/services/campaign-coverage-service.ts; types in lib/types/campaign-coverage.ts.

POST /api/campaigns/{id}/coverage/recommendations

Asks an LLM (lib/ai/services/coverage-recommendation-service.ts) which area codes to acquire next, given the coverage gaps.

Broadcast

POST /api/campaigns/{id}/broadcast

Internal only — requires Authorization: Bearer <INTERNAL_API_SECRET>. Called by pg_cron.

Body (optional)

{ "campaignId": "uuid" } — falls back to the URL parameter.

The route passes a lease of 86 400 seconds, then delegates to PhoneNumberCallingService.processBroadcast. Response is the BroadcastResult:

{
  "callsInitiated": 5,
  "callsSkipped": 1,
  "hasAvailableTargets": true,
  "message": "Campaign A - Throttled: 20/20 in-flight runs. Will retry on next broadcast tick."
}

Errors: 401 without the secret; 400 with no campaign id; 500 on an unexpected failure. A campaign that is not running raises CallInitiationError and surfaces as 500.

See Call campaigns for the full loop.