Skip to content
NewSee how

Manage Contacts with the FirstSales CLI

CRUD contacts, lists, and tags from the command line. Idempotent creates, bulk imports, exports, and deliberate deletes scoped to an org and workspace.

  1. List and read contacts

    Everything is scoped to an org and workspace. Start read-only: list contacts, then fetch one by ID:

    firstsales contacts list --org ORG_ID --workspace WS_ID --json
    firstsales contacts get --org ORG_ID --workspace WS_ID --contact CONTACT_ID
  2. Create and update a contact

    Create with an idempotency key so retries don't duplicate; update patches only the fields you send:

    firstsales contacts create --org ORG_ID --workspace WS_ID \
      --data '{"email":"[email protected]","first_name":"Jane","company_name":"Acme"}' \
      --idempotency-key jane-acme-2026-01
    
    firstsales contacts update --org ORG_ID --workspace WS_ID \
      --contact CONTACT_ID --data '{"job_title":"VP Sales"}'
  3. Organize with lists

    Contact lists are how you segment who a campaign sends to. Create, rename, and delete lists from the CLI:

    firstsales contact-lists list --org ORG_ID --workspace WS_ID
    firstsales contact-lists create --org ORG_ID --workspace WS_ID \
      --data '{"name":"Q1 SaaS founders"}'
    firstsales contact-lists update --org ORG_ID --workspace WS_ID \
      --list LIST_ID --data '{"name":"Q1 SaaS founders (US)"}'
  4. Manage tags in bulk

    Tags are free-form labels across contacts. You can rename (which merges) or delete a tag everywhere it appears: deletion is destructive, so it needs --confirm:

    firstsales contact-tags list --org ORG_ID --workspace WS_ID
    firstsales contact-tags rename --org ORG_ID --workspace WS_ID \
      --data '{"from":"founder","to":"founders"}'
    firstsales contact-tags delete --org ORG_ID --workspace WS_ID \
      --data '{"tag":"stale"}' --confirm
  5. Bulk import

    Push a batch of contacts with an import job. Point it at a list; the platform de-duplicates by email server-side, so re-importing the same people won't create copies:

    firstsales contact-imports create --org ORG_ID --workspace WS_ID \
      --data-file ./leads.json \
      --idempotency-key import-2026-01
  6. Export what you filtered

    List your export jobs / pull contacts back out as CSV for reporting or backup:

    firstsales contact-exports list --org ORG_ID --workspace WS_ID
  7. Delete deliberately

    Removing a contact is destructive and requires --confirm. Prefer letting cleaning auto-suppress bad addresses over hard-deleting: deletion is permanent:

    firstsales contacts delete --org ORG_ID --workspace WS_ID \
      --contact CONTACT_ID --confirm

Pro tips

  • Segment with lists, label with tags

    Lists decide who a campaign sends to; tags are cross-cutting labels for filtering. Use both: a contact can sit in one list and carry many tags.

  • Import is idempotent and de-duped

    Server-side dedup by email means a re-run of an import won't double your list. Still pass --idempotency-key so the job itself isn't created twice.

  • Don't hard-delete to suppress

    To stop emailing someone, let cleaning mark them dirty or rely on bounced/unsubscribed status: those auto-exclude. Reserve contacts delete --confirm for genuine removal.

  • rename merges tags

    Renaming a tag to an existing name merges them. Handy for cleaning up 'founder' vs 'founders': but it's not reversible, so check the target name first.

Frequently asked questions

How are contacts scoped?

To an organization and workspace. Every contact command takes --org and --workspace; the same contact IDs won't resolve in a different workspace.

How do I avoid duplicate contacts on import?

Imports de-duplicate by email server-side, so re-importing the same people adds them to the list without creating copies. Pass --idempotency-key so the import job itself is also safe to retry.

What's the difference between lists and tags?

Lists define the audience a campaign sends to; tags are free-form labels for filtering and segmentation. A contact belongs to lists and can carry many tags.

How do I remove a tag everywhere?

contact-tags delete --confirm removes it from all contacts, and contact-tags rename merges one tag into another. Both are destructive, so delete requires --confirm.

Can I export my contacts?

Yes: use contact-exports to pull contacts back out as CSV. In the app, export reflects your current filter/selection; via the API you work with export jobs.

Should I delete contacts that bounce?

Usually no. Bounced, unsubscribed, and cleaning-flagged (dirty) contacts are auto-excluded from sends, so deletion is rarely needed. Hard-delete only when you truly want the record gone, and it requires --confirm.

Developer & CLI

Outreach that keeps going after you log off.Leads, emails, follow-ups and replies, handled.

Connect unlimited mailboxes and let AI agents find leads, write each email and follow up. Or send your own templates in manual mode. From $29/mo.

$1 for 3 days · Cancel anytime · No contract

Install with AI

Your coding agent sets up the FirstSales CLI and skills for you.

Prompt for Claude Code

I'm setting up FirstSales (https://firstsales.io), an AI outbound sales platform: campaigns, contacts, a reply inbox, deliverability and lead signals. Help me get my workspace sending real, good emails end to end, from Claude Code.

Read these first so you know how FirstSales works:
- Developer docs and API: https://developer.firstsales.io
- CLI: https://www.npmjs.com/package/@firstsales.io/cli
- Skills for coding agents: https://github.com/firstsalesio/firstsales-skills (read README.md and GUIDE.md)

Setup
1. Install the CLI: npm install -g @firstsales.io/cli
2. Install the FirstSales skills:
   claude plugin marketplace add firstsalesio/firstsales-skills
   claude plugin install firstsales@firstsales
3. Ask me if I already have a FirstSales account. If not, send me to https://app.firstsales.io to sign up.
4. Ask me to create an API key under Settings → API → API Keys, with only the scopes we need.
   Never ask me to paste the key into this chat. Tell me to run this in my own terminal, so the key stays hidden:
   read -rs FS_KEY && firstsales auth login --api-key "$FS_KEY"; unset FS_KEY
5. Run `firstsales whoami` and tell me which workspace and organisation we are connected to.

Then ask me about my business: what I sell, who I sell to (roles, company size, countries), my best customers, and what I use today for outbound (tools, lists, mailboxes). Walk me through:
1. Teaching FirstSales my business: website, offer, proof points and case studies in the knowledge base, so the AI writes like someone who knows the product.
2. Connecting my sending mailboxes and checking my sending domain: SPF, DKIM, DMARC and warm-up. If anything is unhealthy, tell me the exact DNS record to add.
3. Bringing in contacts: import my CSV or CRM export into a clean list, remove duplicates and bad emails, and block domains I must never email (customers, competitors, my own).
4. Building my first campaign: the right list, senders, steps and timing. Write the sequence with me, show me the drafts, and do not launch until I say yes.
5. Setting up Signals if it fits: find people engaging with LinkedIn posts about topics my buyers care about, and turn them into leads.
6. Inviting my team and giving each person the right access.

Once campaigns run, show me how to work the reply inbox from here: which replies need an answer, draft responses in my voice, and move good conversations into deals. If I use Cal.com, connect it so follow-ups can offer my booking link (not the first email).

Throughout, as you learn about my business, proactively suggest and help create:
- Reply templates for the questions and objections my prospects are likely to raise, based on my product, site and codebase if you can see them.
- Campaign ideas and segments worth testing, with the reason for each.

Safety rules:
- Show every change as a dry run (--dry-run) first and wait for my yes. Never send emails, launch campaigns or delete anything without my yes.
- Tell me before anything that spends credits, and how many.

Be opinionated. If the docs give a sensible default, just use it. Don't make me choose between five options.