---
title: "Start, Pause, Resume Campaigns From the CLI Safely"
description: "Start pause campaigns CLI guide: the exact FirstSales commands, the launch gate, and the one verb an agent can run alone."
date: "2026-09-07"
tags: "cli, campaigns, cold-email, claude-code, outbound-operations"
readTime: "24 min read"
author: "FirstSales Team"
slug: "start-pause-campaigns-cli"
canonical: "https://firstsales.io/blog/start-pause-campaigns-cli/"
---

# Start, pause and resume campaigns from the CLI

**TL;DR:** The FirstSales CLI gives you `campaigns start`, `campaigns pause` and `campaigns resume`, but the verbs are the easy part. Treat `pause` as the one lifecycle command an agent may run alone, and treat `start` as a human decision that needs a passing readiness check and a test email first.

---

## Table of contents

- [The one verb an agent can run alone](#the-one-verb-an-agent-can-run-alone)
- [What the CLI gives you for the campaign lifecycle](#what-the-cli-gives-you-for-the-campaign-lifecycle)
- [Set up the CLI with a key that can only do this job](#set-up-the-cli-with-a-key-that-can-only-do-this-job)
- [Read the state before you touch it](#read-the-state-before-you-touch-it)
- [The launch gate before start](#the-launch-gate-before-start)
- [Start a campaign with campaigns start](#start-a-campaign-with-campaigns-start)
- [Verify after the start](#verify-after-the-start)
- [Pause a campaign the right way](#pause-a-campaign-the-right-way)
- [Resume without repeating the original mistake](#resume-without-repeating-the-original-mistake)
- [Exit codes and scripts](#exit-codes-and-scripts)
- [Let Claude Code drive, with rails](#let-claude-code-drive-with-rails)
- [Where MCP fits](#where-mcp-fits)
- [Who may run which verb](#who-may-run-which-verb)
- [Common mistakes](#common-mistakes)
- [FAQs](#faqs)
- [Conclusion](#conclusion)

---

## The one verb an agent can run alone

If you let an AI agent run your outbound, one rule covers most of the risk: the agent may stop sending on its own, and it may never start sending on its own.

That asymmetry is the whole design of this post. Stopping a campaign costs you a few hours of delayed sends. Starting a bad campaign costs you domain reputation, and reputation takes weeks to repair.

The numbers behind that asymmetry are published. Google's [email sender guidelines](https://support.google.com/a/answer/81126) tell senders to keep the spam rate reported in Postmaster Tools below 0.10% and to prevent it from ever reaching 0.30% or higher.

At 0.30%, three complaints per thousand messages put you in the danger zone. One careless launch to a bad list can do that in a single morning.

The FirstSales command line interface (CLI) is a thin, JSON-first wrapper over the public FirstSales Developer API. Its README describes its own working method in one line: "Inspect first. Mutate deliberately. Verify after every action."

This post turns that line into a runbook for the three lifecycle commands: `campaigns start`, `campaigns pause` and `campaigns resume`. You will see the exact commands, the reasoning behind each step, and the points where a human must stay in the loop.

Everything here uses placeholder IDs such as `org_123`, `ws_123` and `camp_123`. Replace them with your own values, and never paste a real API key into a chat prompt or a script you commit.

## What the CLI gives you for the campaign lifecycle

The CLI package is `@firstsales.io/cli` and the binary is `firstsales`. You install it with `npm i -g @firstsales.io/cli`, and as of October 2026 the published version is 0.1.11.

The `campaigns` group contains the verbs you need for a full lifecycle. The table lists the ones this post uses.

| Command | What it does | Changes state? |
| --- | --- | --- |
| `campaigns list` | Lists campaigns in a workspace | No |
| `campaigns get` | Reads one campaign | No |
| `campaigns progress` | Reads sending progress | No |
| `campaigns analytics` | Reads performance numbers | No |
| `campaigns events` | Reads the campaign event stream | No |
| `campaigns readiness` | Runs the readiness check | No |
| `campaigns prepare-launch` | Prepares the launch request | No |
| `campaigns prepare-test-email` | Prepares a test send | No |
| `campaigns send-test-email` | Sends a test email | Sends one email |
| `campaigns start` | Starts the campaign | Yes |
| `campaigns pause` | Pauses the campaign | Yes |
| `campaigns resume` | Resumes a paused campaign | Yes |

The CLI does not hide anything behind magic. Each command maps to one public `/api/v1` endpoint, and the API enforces authentication, scopes, tenant isolation and rate limits.

If you want the authoritative list for your installed version, run `firstsales commands --json`. The package skill describes that local command registry as the authority, so trust it over any blog post, including this one.

The [CLI reference on the developer site](https://developer.firstsales.io/cli-reference/commands) documents the same surface.

## Set up the CLI with a key that can only do this job

Install the package and check the binary.

```bash
npm i -g @firstsales.io/cli
firstsales --help
```

Authentication uses a Developer API key. The CLI resolves the key in this order: the `--api-key` flag, then the `FIRSTSALES_API_KEY` environment variable, then a saved local profile.

Prefer the environment variable for agents and CI, because it keeps the key out of command history and out of the prompt.

```bash
export FIRSTSALES_API_KEY="<YOUR_FIRSTSALES_API_KEY>"
firstsales whoami --json
firstsales doctor --pretty
```

`whoami` is the equivalent of `npm whoami`. It validates the key and prints the user, key, organization and workspace context without exposing the raw or hashed key.

`doctor` checks your setup. Run both before anything else, because a wrong workspace is the cheapest mistake to catch and the most expensive one to discover after a send.

### Create a key with the least access

An agent that only needs to pause campaigns should not carry a key that can delete contacts. The `api-keys create` command takes a scopes list and an access level.

```bash
firstsales api-keys create \
  --org org_123 \
  --data '{"name":"Campaign operator","scopes":["campaigns:read"],"accessLevel":"workspace"}' \
  --json
```

The example above shows the shape from the README, with a scope list trimmed to read access. Which scope your pause and start calls need is defined by the OpenAPI contract at `developer.firstsales.io`, so check it before you create the key.

The raw key appears once, at creation. It is never retrievable later, so store it in a secret manager and not in a note or a chat window.

Do not pass `--idempotency-key` to `api-keys create`. The README states that raw keys are reveal-once, which is why that flag is unsupported for this command.

If you want a deeper look at scoping keys for agents, the post on [API keys for outbound agents](/blog/api-keys-for-outbound-agents) covers it in full.

## Read the state before you touch it

Every safe lifecycle change starts with a read. The recommended agent loop in the CLI README is seven steps long, and the first two are "verify identity" and "read the resource state."

Start with the list.

```bash
firstsales campaigns list --org org_123 --workspace ws_123 --json
```

Pick the campaign you mean, then read it.

```bash
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns progress --org org_123 --workspace ws_123 --campaign camp_123 --json
```

The output is compact JSON by default. Add `--pretty` when a human reads it, and `--output table` when you want a quick scan in a terminal.

Save the `get` output to a file before you change anything. It becomes your "before" snapshot, and it makes the post-change comparison a diff instead of a memory exercise.

```bash
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json > before.json
```

### Why the read matters more with an agent

A person who types `campaigns pause` knows which campaign they mean. An agent working from a long conversation can easily mix up two campaigns with similar names.

Reading first, and printing the campaign name back to the human, removes that class of error. The cost is one extra command.

The same habit protects you from acting on stale information. A campaign you paused yesterday may have been resumed by a teammate this morning.

## The launch gate before start

Two safety lines are non-negotiable in this runbook. Run `campaigns readiness` before `campaigns start`, and run `campaigns send-test-email` before `campaigns start`.

They are not formalities. Readiness catches configuration gaps that would otherwise surface as bounces, and the test email shows you what a real prospect will see.

```bash
firstsales campaigns readiness --org org_123 --workspace ws_123 --campaign camp_123 --json
```

Read the whole response. A passing check is the only green light, and a warning you do not understand is a reason to stop and ask.

The companion posts go deeper on each gate. [The pre-launch checklist from the terminal](/blog/campaign-readiness-check-cli) walks through a full preflight script, and [sending a test email from Claude Code first](/blog/send-test-email-claude-code) covers the test send.

### Prepare the launch request

The CLI also exposes `campaigns prepare-launch`, which prepares the launch request that `start` consumes.

```bash
firstsales campaigns prepare-launch --org org_123 --workspace ws_123 --campaign camp_123 --json
```

I cannot show you its response here, because the exact fields come from the live API and from your campaign. Run it against a campaign you own, read the output, and keep it next to the readiness output.

The reason this step exists is visible in the `start` body, which you will meet next. The start request must carry `savedVersionId` and `readinessVersion`, which means the launch is bound to a specific saved version of the campaign and a specific readiness result.

That binding is a good design. If someone edits the sequence after you ran readiness, a launch built from the old evidence should not go through.

### The test email

```bash
firstsales campaigns prepare-test-email --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns send-test-email --org org_123 --workspace ws_123 --campaign camp_123 --data-file test-email.json --json
```

The recipient and any other fields go in the request body. The CLI accepts a body with `--data` or `--data-file`, and the field names come from the OpenAPI contract, so check them there.

Run `--dry-run` first if you are unsure. Per the README, `--dry-run` prints the method, URL, redacted headers and body without sending the request, and it does not use auth or the network.

Open the received message in a real inbox. Check the sender name, the subject, the first line, the links and the unsubscribe mechanism.

## Start a campaign with campaigns start

Now the command everything else exists to protect.

```bash
firstsales campaigns start \
  --org org_123 \
  --workspace ws_123 \
  --campaign camp_123 \
  --data-file launch.json \
  --json
```

The README is explicit about the body. `campaigns start` requires a JSON body containing `savedVersionId`, `readinessVersion`, `idempotencyKey` and `confirmation`.

Without a body, the CLI returns a `missing_required_body` error. The troubleshooting table says to pass `--data` or `--data-file` with the required campaign launch evidence.

### Preview with a dry run

Use `--dry-run` with the same body before you execute. The README describes it as a way to inspect the tenant-bound request before execution.

```bash
firstsales campaigns start \
  --org org_123 \
  --workspace ws_123 \
  --campaign camp_123 \
  --data-file launch.json \
  --dry-run
```

Read the printed URL and body out loud, or paste them to the human who owns the launch decision. Check three things: the org and workspace in the URL, the campaign ID, and the version fields in the body.

### What goes in the four fields

I will be careful here, because the README names the fields and does not define their values. The names tell a clear story, though.

The names suggest that `savedVersionId` points at the saved version of the campaign you reviewed, and that `readinessVersion` points at the readiness result you accepted.

Likewise, `idempotencyKey` should make the start safe to retry, and `confirmation` should carry the explicit statement that a human approved the launch. These readings are inferences from the names, so check them against the OpenAPI contract.

Treat the first two as outputs of the commands you just ran, and copy them from those responses instead of typing them. Treat `confirmation` as the field only a person fills in.

If your agent builds `launch.json`, make it stop and show the file to you before the real call. That single pause is the cheapest guardrail in this entire workflow.

### Idempotency in practice

The `idempotencyKey` field matters because networks fail. If a start request times out, you do not know whether it landed.

Re-sending the same body with the same key lets the API recognise the retry instead of double-processing it. Pick a key that names the campaign and the launch, such as `start-camp_123-2026-10-08`, and reuse it only for retries of that exact launch.

The CLI also has a global `--idempotency-key` flag for supported retryable mutations. The README says the CLI never makes a key up for you, so you supply it.

## Verify after the start

The last step of the agent loop is "re-read state and verify the result." A successful exit code means the API accepted the request. It does not tell you the campaign behaves the way you expect.

```bash
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json > after.json
diff before.json after.json
firstsales campaigns progress --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns events --org org_123 --workspace ws_123 --campaign camp_123 --json
```

The diff should show a status change and nothing surprising. If other fields moved, someone else edited the campaign between your two reads.

Watch the first sends. `campaigns events` gives you the event stream, and `campaigns progress` shows how far through the audience the campaign is.

### The first hour checklist

In the first hour after a start, look for four things. Sends are leaving at the pace you configured, bounces are low, no alert has fired, and the first replies look like replies to the message you intended.

The workspace alerts are one command away.

```bash
firstsales alerts list --org org_123 --workspace ws_123 --json
```

The `alerts` group also has `ack` and `resolve`. Acknowledging an alert tells your team someone saw it, and resolving it should wait until the cause is fixed.

For benchmarks on what a healthy first hour looks like, the [cold email bounce rate](/blog/cold-email-bounce-rate) post lists the thresholds worth acting on.

## Pause a campaign the right way

Pausing is the verb you want to be fast, boring and reversible. The command is short.

```bash
firstsales campaigns pause --org org_123 --workspace ws_123 --campaign camp_123 --json
```

The README includes this as an agent example, and the pattern around it matters. Read the campaign, pause it, then read it again.

```bash
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns pause --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json
```

The second `get` proves the pause took effect. Without it, you are trusting an exit code.

### When to pause

Write the pause conditions down before the campaign starts, so nobody argues about them while the numbers are bad. Four triggers cover most incidents.

The first is a spam complaint rate approaching Google's limits. Google says to keep the rate below 0.10% and never let it reach 0.30%, and Postmaster Tools is where you read it.

The second is a bounce spike. A sudden jump usually means a bad list segment or a broken domain, and the [deliverability incident response](/blog/deliverability-incident-response) post describes the triage.

The third is an authentication failure. Outlook began rejecting non-compliant mail from high-volume senders on May 5, 2025, with the error "550; 5.7.515 Access denied, sending domain does not meet the required authentication level," according to the [Microsoft Defender for Office 365 blog](https://techcommunity.microsoft.com/blog/microsoftdefenderforoffice365blog/strengthening-email-ecosystem-outlook%E2%80%99s-new-requirements-for-high%E2%80%90volume-senders/4399730).

The fourth is a content error you discover after launch, such as a wrong merge field, a dead link or a claim you cannot support. A paused campaign costs hours. A wrong claim sent to a thousand people costs trust.

### The pause is also a legal tool

Opt-outs have a clock. The FTC's [CAN-SPAM compliance guide](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business) says you must honor an opt-out request within 10 business days, that the law makes no exception for business-to-business email, and that each separate violating email can carry penalties of up to $53,088.

If you find that opt-outs are not being honored, pause first and investigate second. Our post on [cold email compliance penalties](/blog/cold-email-compliance-penalties) covers the fines in more detail.

### Why the agent may pause alone

A pause is reversible, narrow and safe. It cannot send an email, delete data or spend money.

That makes it the right verb to delegate. Give the agent a clear rule, such as "pause any campaign whose bounce rate crosses the threshold in this file," and let it act.

What the agent must do after it pauses is tell you. A silent pause is a hidden delay, and a hidden delay is a missed meeting.

## Resume without repeating the original mistake

Resuming looks like the mirror image of pausing, and it is not. A pause stops the cause from growing. A resume restarts the cause unless you removed it.

```bash
firstsales campaigns resume --org org_123 --workspace ws_123 --campaign camp_123 --json
```

Before you run it, answer one question in writing: what changed since the pause? If the answer is "nothing," do not resume.

### A resume checklist

Re-run `campaigns readiness` and read it again, because the state of your domains, mailboxes or lists may have shifted while the campaign sat idle.

Check the cause. If the pause came from a bounce spike, confirm the bad segment is gone. If it came from authentication, run `firstsales email-auth status` and confirm the records pass.

Send a fresh test email if you edited any copy. Then resume, and watch the first hour exactly as you did after the original start.

### Resume in smaller steps

If the cause was a deliverability problem, restarting at full volume invites a repeat. Many teams resume with fewer sending accounts or a lower daily cap, then step back up over several days.

The [email warm-up statistics](/blog/email-warm-up-statistics) and [emails per inbox per day](/blog/emails-per-inbox-per-day) posts cover sensible ramps.

## Exit codes and scripts

The CLI is designed for scripts. It returns a stable exit code on every call, and it prints JSON errors on non-zero exits.

| Code | Meaning |
| --- | --- |
| `0` | ok |
| `1` | runtime error |
| `2` | usage error |
| `3` | auth error |
| `4` | not found |
| `5` | rate limited |

A pause wrapper script can act on those codes. This one retries once on a rate limit and stops loudly on everything else.

```bash
#!/usr/bin/env bash
set -u

ORG="org_123"; WS="ws_123"; CAMP="${1:?campaign id required}"

pause_once() {
  firstsales campaigns pause --org "$ORG" --workspace "$WS" --campaign "$CAMP" --json
}

pause_once
code=$?

if [ "$code" -eq 5 ]; then
  sleep 30
  pause_once
  code=$?
fi

if [ "$code" -ne 0 ]; then
  echo "pause failed with exit code $code for $CAMP" >&2
  exit "$code"
fi

firstsales campaigns get --org "$ORG" --workspace "$WS" --campaign "$CAMP" --json > "after-pause-$CAMP.json"
echo "paused $CAMP, state saved to after-pause-$CAMP.json"
```

The script uses only commands and exit codes from the README. It does not parse any field of the JSON, because I will not guess response shapes.

The exit codes changed in version 0.1.1, according to the README, so scripts written for the 0.1.0 scheme need a migration.

### Keep start out of cron

Do not schedule `campaigns start`. A scheduled start removes the human decision this whole post defends.

Scheduled reads are fine. A cron job that runs `campaigns progress` and `alerts list`, then pings you, is a good use of the CLI. The post on [weekly campaign reports with cron and the CLI](/blog/weekly-campaign-report-cron-cli) shows the pattern.

## Let Claude Code drive, with rails

Claude Code can run shell commands, so it can run the CLI directly. The package even ships three Claude Code skills: `firstsales-cli`, `firstsales-product-mcp` and `firstsales-developer-api`.

The `firstsales-cli` skill lists the operating rules the agent should follow. Verify identity first, read the current state before mutation, prefer `--json`, use `--dry-run` before risky writes, and use `--confirm` only for intentional destructive commands.

Those rules fit this runbook well. They are also a reminder that `--confirm` belongs to deletes, not to starts.

### Rails worth adding

First, restrict the permissions in your agent's settings so that `campaigns start` always prompts a human, while read commands and `campaigns pause` run freely. Most coding agents let you allow or deny shell commands by pattern.

Second, tell the agent the rule in plain words in your project instructions: "You may pause. You may never start. When a start is needed, build `launch.json`, run the dry run, show me both, and wait."

Third, make the agent print the campaign name and ID before every mutation. If the name looks wrong to you, you catch the mistake before the call.

The Model Context Protocol specification supports the same instinct. Its section on tools says that for trust and safety, "there SHOULD always be a human in the loop with the ability to deny tool invocations," as written in the [MCP tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).

For a broader treatment of guardrails, read [guardrails for email-sending agents](/blog/email-agent-guardrails).

### A sample prompt

You can give Claude Code a prompt like this. It names the allowed commands and the stop points.

```text
You are operating FirstSales for workspace ws_123 in org org_123.
Allowed without asking: whoami, campaigns list/get/progress/analytics/events, alerts list, campaigns pause.
Never run without my explicit yes: campaigns start, campaigns resume, any delete, any api-keys command.
Before any change, print the campaign name and ID, and save the current state to a file.
After any change, read the state again and tell me what differed.
Use the FIRSTSALES_API_KEY environment variable. Never print it.
```

The prompt contains no secret. The key lives in the environment, and the agent never sees its value.

## Where MCP fits

The FirstSales Product MCP server is a separate surface from the CLI. It is `firstsales-product`, a streamable HTTP server at `https://api.app.firstsales.io/mcp`, and it uses OAuth rather than a Developer API key.

The tool list depends on your tenant and your grant, so the live `tools/list` call is the authority. Check it for your account before you plan around any specific tool.

In a connected session, we confirmed tools for listing and reading campaigns, reading campaign analytics, progress and events, and reading or updating a campaign workflow. Creating a campaign through MCP is not confirmed, and this post does not rely on it.

That split gives you a clean division of labor. Use the CLI to create, start, pause and resume. Use MCP when you want an MCP-aware assistant to watch a campaign and talk about it.

The package skill says it directly: if a desired action is mutating, destructive, billing-related or approval-backed, switch to the CLI or Developer API instead of assuming MCP support. Developer API keys are not accepted by Product MCP, and MCP tokens are not Developer API credentials.

If you are new to the protocol, [what MCP is for sales teams](/blog/what-is-mcp-for-sales) explains it without jargon, and [which MCP tools an agent should get](/blog/mcp-tool-permissions-sales) helps you decide what to expose.

## Who may run which verb

The table below sums up the permission model this post argues for. It uses a check mark for "allowed alone" and a cross for "needs a human."

| Action | Agent alone | Human approval | Why |
| --- | --- | --- | --- |
| `whoami`, `doctor` | ✓ | Not needed | Read-only identity checks |
| `campaigns list`, `get`, `progress`, `analytics`, `events` | ✓ | Not needed | Read-only state |
| `alerts list` | ✓ | Not needed | Read-only signal |
| `campaigns readiness` | ✓ | Not needed | Check only, informs the human |
| `campaigns send-test-email` | ✗ | Required | Sends a real email, even if only one |
| `campaigns start` | ✗ | Required | Starts real outbound to real people |
| `campaigns pause` | ✓ | Notify after | Reversible and narrow |
| `campaigns resume` | ✗ | Required | Restarts the cause unless fixed |
| `contacts delete` | ✗ | Required | Cannot be undone |
| `api-keys create`, `revoke` | ✗ | Required | Changes who can act |

The only cell that looks asymmetric is pause, and that is deliberate. It is the single mutation where speed beats ceremony.

### Same idea as a state diagram

The diagram below is a mental model, not the API's status names. It shows which transitions need a person.

```mermaid
stateDiagram-v2
    [*] --> Drafted
    Drafted --> Ready: readiness passes and test email reviewed
    Ready --> Running: start (human approves)
    Running --> Paused: pause (agent or human)
    Paused --> Running: resume (human approves)
    Paused --> Drafted: edit copy or lists
    Running --> [*]: campaign finishes
```

Notice that every arrow into `Running` carries a human approval, and the only arrow out of it that anyone can take alone is `pause`.

## Common mistakes

Most lifecycle incidents come from a short list of habits. Each one is easy to avoid once you can name it.

### Starting without reading the dry run

The dry run exists so the request can be read before it matters. Skipping it turns a reviewable action into a leap.

### Trusting the exit code as proof

Exit code `0` means the API accepted the call. Only a second `get` shows the resulting state.

### Pausing without telling anyone

A silent pause is a hidden delay. Message the campaign owner the moment you pause.

### Resuming at full volume after a deliverability incident

The cause may still be present. Resume smaller, then ramp up.

### Putting the API key in the prompt

A key in a prompt lands in logs, transcripts and sometimes screenshots. Keep it in the environment and rotate it through `api-keys` if it ever leaks.

### Reusing one idempotency key for different launches

An idempotency key identifies one operation. Reuse across different launches can make the API treat your second launch as a retry of the first.

### Scheduling the start

Cron is excellent for reports and terrible for launches. Keep the human on the start.

For the wider pattern of automation going wrong, see [unsupervised AI outbound](/blog/unsupervised-ai-outbound), and for the cultural side, [human-in-the-loop cold email](/blog/human-in-the-loop-cold-email).

## A full session, end to end

Here is the whole flow in one place, using the commands from this post. It assumes the campaign was already created with `firstsales campaigns create`.

```bash
# 1. Identity and setup
firstsales whoami --json
firstsales doctor --pretty

# 2. Read state and keep a snapshot
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json > before.json

# 3. The gate
firstsales campaigns readiness --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns prepare-launch --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales campaigns send-test-email --org org_123 --workspace ws_123 --campaign camp_123 --data-file test-email.json --json

# 4. Human reviews the received email and the readiness output

# 5. Dry run, then start
firstsales campaigns start --org org_123 --workspace ws_123 --campaign camp_123 --data-file launch.json --dry-run
firstsales campaigns start --org org_123 --workspace ws_123 --campaign camp_123 --data-file launch.json --json

# 6. Verify
firstsales campaigns get --org org_123 --workspace ws_123 --campaign camp_123 --json > after.json
diff before.json after.json
firstsales campaigns progress --org org_123 --workspace ws_123 --campaign camp_123 --json
firstsales alerts list --org org_123 --workspace ws_123 --json
```

The sequence has two human checkpoints, the review before step 5 and the check after step 6. Everything else can be scripted.

If your campaign does not exist yet, the companion post on [building a campaign from Claude Code](/blog/claude-code-cold-email-campaign) shows the creation side.

## What to monitor in the first day

A start is not finished when the command returns. The first day tells you whether the gate did its job.

Check progress every hour for the first few hours. A stalled campaign points at a configuration issue, while a campaign that runs too fast points at a pacing setting you did not intend.

Check replies as they arrive. The first replies tell you whether the message landed as written, and the [reply handling playbook](/blog/reply-handling-playbook) helps you sort them.

Compare your early numbers against realistic benchmarks. The [cold email benchmarks](/blog/cold-email-benchmarks) post gives you a reference so a good day does not feel bad and a bad day does not feel normal.

If anything looks off, you already know the move: pause, read, fix, test, resume smaller.

## FAQs

### What does `firstsales campaigns start` need in its body?

The README says the body must contain `savedVersionId`, `readinessVersion`, `idempotencyKey` and `confirmation`. Pass it with `--data` or `--data-file`.

### Can I preview a start without sending it?

Yes. Add `--dry-run` with the same body, and the CLI prints the method, URL, redacted headers and body without sending the request.

### Which command stops a running campaign?

`firstsales campaigns pause --org <org> --workspace <workspace> --campaign <campaign>`. Read the campaign before and after to confirm the state changed.

### Does pausing delete anything?

No. Pausing stops further sends and leaves the campaign, its contacts and its history in place, and `campaigns resume` restarts it.

### Should an AI agent be allowed to start campaigns?

Not on its own. A start sends real email to real people, so a human should approve it after readiness and a test email pass.

### Should an AI agent be allowed to pause campaigns?

Yes, with a written rule and a notification. Pause is reversible and narrow, which makes it the safest verb to delegate.

### How do I know the pause worked?

Run `campaigns get` after the pause and compare it with the snapshot you saved earlier. The exit code alone does not prove the state.

### What should I check before I resume?

Re-run `campaigns readiness`, confirm the cause of the pause is fixed, and send a fresh test email if you changed copy. Then resume at a lower volume.

### What is the idempotency key for?

It makes a retry safe. If a request times out, sending the same body with the same key lets the API recognise the retry.

### Does the CLI generate idempotency keys for me?

No. The README states the CLI never makes a key up, so you supply one that names the operation.

### What do the CLI exit codes mean?

`0` is ok, `1` a runtime error, `2` a usage error, `3` an auth error, `4` not found and `5` rate limited. Scripts can branch on them.

### Can MCP start or pause a campaign?

Do not plan on it. The Product MCP tool list is per tenant and per grant, and creating campaigns through MCP is not confirmed, so use the CLI for lifecycle changes and check `tools/list` for your account.

### Where do I create the campaign first?

Use `firstsales campaigns create`, then read it with `campaigns get`. MCP can edit and watch the campaign afterwards.

### Do I need an API key to run these commands?

Yes. The CLI authenticates with a Developer API key, resolved from `--api-key`, then `FIRSTSALES_API_KEY`, then a saved profile.

### How should I store the API key?

In an environment variable or a secret manager. Never paste it into a prompt, a post or a committed file.

### What happens if I run a command against the wrong workspace?

The change lands in the wrong place. Run `whoami` first and print the workspace name back to a human before any mutation.

### Which spam rate should trigger a pause?

Google recommends keeping the rate below 0.10% and never letting it reach 0.30% or higher. Set your pause threshold well under 0.30%.

### Is the opt-out deadline different for B2B email?

No. The FTC guide says CAN-SPAM makes no exception for business-to-business email, and opt-outs must be honored within 10 business days.

### Can I schedule campaign starts with cron?

You can, but you should not. A scheduled start removes the human decision, so schedule reports and checks instead.

### Where can I see every command my installed version supports?

Run `firstsales commands --json`. The package skill calls that local registry the authority for what exists.

---

## Conclusion

Starting, pausing and resuming a campaign are three small commands wrapped in a lot of judgment. The CLI makes the commands boring on purpose, which leaves your attention free for the judgment.

Here are the takeaways to carry out of this post.

- Run `campaigns readiness` and `campaigns send-test-email` before every `campaigns start`, and read the results yourself.
- Build the start body from the evidence you just collected, preview it with `--dry-run`, and keep a human on the final call.
- Let an agent run `campaigns pause` alone, and require a notification every time it does.
- Treat resume as a new launch decision: name what changed, retest, and restart smaller.
- Verify every change with a second read, because an exit code is not a state.
- Keep API keys out of prompts, and give each agent a key with the least access it needs.
- Delete commands cannot be undone, so keep `--confirm` for deliberate cleanup and never for launches.

If you want to try this on your own account, install the CLI with `npm i -g @firstsales.io/cli`, run `firstsales whoami --json`, and read one campaign before you change anything. Then read the next post in this series, [the pre-launch checklist from the terminal](/blog/campaign-readiness-check-cli), and turn the gate into a script.