---
title: "Add Campaign Accounts"
method: POST
path: "/campaigns/{campaign_id}/accounts"
tags: ["campaigns"]
---

# Add Campaign Accounts

`POST /campaigns/{campaign_id}/accounts`

## Request

- Base URL: `https://dev.whispa.net` (the document declares no server; this is the origin it was published from)
- URL: `https://dev.whispa.net/campaigns/{campaign_id}/accounts`
- Auth: one of:
  - OAuth 2
    - Password
    - Token URL: `auth/bearer/login`
  - API key in cookie `access_token`

Add provider accounts (by external id) to a campaign (admin only).

Accounts are cached into the working set on the way in; on an active
campaign the new accounts are scheduled immediately (gate at enqueue).

## Path parameters

- `campaign_id` string, uuid, required

## Request body

- AddCampaignAccountsRequest: Add accounts to a campaign by explicit ref list, or by filter. The two variants are mutually exclusive. ``external_ids`` is the by-ref path: provider row ids or account numbers, resolved one at a time and cached on the way in, so a pasted batch list works even for accounts the working set has never seen. ``filter`` is the "add everything I'm looking at" path: the operator's accounts-table filter is re-evaluated server-side over the working set, which is the only way a selection of thousands can be added in one request (and the only way the set added matches the set the operator was shown).
  - `external_ids` string[], nullable
  - `filter` AccountFilter: The accounts working-set predicate, defined once. Read as query params by ``/accounts/search`` and ``/accounts/export``, and posted verbatim by the campaign add-by-filter variant. One model means the rows an operator sees, exports and adds can never be three different sets. Amount bounds are inclusive and match the columns the table renders: ``balance`` is outstanding, ``arrears_amount`` is arrears. Accounts with a NULL amount are excluded by any bound on it — "over $500" cannot honestly include an account whose balance nobody knows.
    - `demo_market` 'au' | 'us', nullable
    - `q` string, nullable: Free-text search (case-insensitive) across display name, account number, email, external id and phone.
    - `status` string[]: Provider status snapshot values to include (case-insensitive). Empty = every status.
    - `campaign_id` string, uuid, nullable: Only accounts with a non-removed membership in this campaign. Includes terminal membership history.
    - `campaign_assignment` 'assigned' | 'unassigned' | 'multiple' | 'review', nullable: Filter by current non-terminal campaign assignments. 'multiple' means two or more overlapping assignments; 'review' means an assignment is waiting on an operator decision.
    - `timezone_status` 'unknown' | 'manual', nullable: 'unknown': no timezone could be established, so the compliance gate will not call the account. 'manual': an operator set the timezone by hand.
    - `called` 'account' | 'customer' | 'never', nullable: Filter by linked calls. 'account': a call is linked to this account. 'customer': a call is linked to this account or to another account of the same customer. 'never': neither.
    - `min_balance` number, nullable: Inclusive lower bound on outstanding amount.
    - `max_balance` number, nullable: Inclusive upper bound on outstanding amount.
    - `min_arrears` number, nullable: Inclusive lower bound on arrears amount.
    - `max_arrears` number, nullable: Inclusive upper bound on arrears amount.
  - `exclude_account_ids` string[]
  - `skip_assigned_elsewhere` boolean

## Response `200`

Successful Response

- AddCampaignAccountsResponse
  - `added` integer, required
  - `already_present` integer, required
  - `assigned_elsewhere` integer
  - `waiting` integer
  - `not_found` string[]

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/whispa/apis/whispa-api.md) - [All operations](https://skmtc.dev/whispa/apis/whispa-api/llms.txt) - [OpenAPI document](https://skmtc.dev/whispa/apis/whispa-api/revisions/12c2e4aca43c?raw)
