---
title: "Using FirstSales in CI/CD & Scripts | FirstSales"
description: "Run the FirstSales CLI in pipelines — masked secrets, fail-fast auth, dry-run on PRs and apply on main, idempotent steps, and guarded destructive actions."
canonical: "https://firstsales.io/tutorial/firstsales-in-ci-cd/"
---

[Home](/)/[Tutorials](/tutorial/)/Using FirstSales in CI/CD & Scripts

Developer & CLI

# Using FirstSales in CI/CD & Scripts

Run the FirstSales CLI in pipelines — masked secrets, fail-fast auth, dry-run on PRs and apply on main, idempotent steps, and guarded destructive actions.

8 min read·Advanced·7 steps

1. 1  
## Prefer the CLI in pipelines  
For CI/CD jobs and local scripts, the docs recommend the **CLI** over raw HTTP — it centralizes credential handling, emits stable JSON your steps can parse, and keeps the destructive-operation safeguards you want in automation. One tool, one auth path, consistent output.
2. 2  
## Inject the key as a masked secret  
Store your Developer API key in your CI provider's secret store (GitHub Actions secrets, GitLab CI variables, etc.) and expose it as `FIRSTSALES_API_KEY`. Never hardcode it in the workflow file. Most CI systems mask secret values in logs automatically — keep it that way by never `echo`\-ing the key.  
```  
# GitHub Actions job step  
- name: FirstSales sync  
  env:  
    FIRSTSALES_API_KEY: ${{ secrets.FIRSTSALES_API_KEY }}  
  run: |  
    npm install -g @firstsales.io/cli  
    firstsales whoami --json  
```
3. 3  
## Fail fast on a bad key  
Start every job with `whoami` so the pipeline stops immediately if the key is wrong or revoked, instead of half-running a sync:  
```  
firstsales whoami --json > /dev/null || {  
  echo "FirstSales auth failed"; exit 1;  
}  
```
4. 4  
## Plan with --dry-run, then apply  
In automation you want a preview before you write. Gate real mutations behind a plan step using `--dry-run` (e.g. on pull requests), and only apply on the main branch:  
```  
# PR / plan: preview only  
firstsales contacts create --org "$ORG" --workspace "$WS" \  
  --data-file ./new-lead.json --dry-run  
# main / apply: real write, idempotent  
firstsales contacts create --org "$ORG" --workspace "$WS" \  
  --data-file ./new-lead.json \  
  --idempotency-key "lead-${GITHUB_SHA}"  
```
5. 5  
## Make retryable steps idempotent  
CI steps get retried — by flaky runners, by humans re-running a job. Derive an `--idempotency-key` from something stable (a commit SHA, a row ID, a run number) so a re-run doesn't double-create:  
```  
firstsales contact-imports create --org "$ORG" --workspace "$WS" \  
  --data-file ./import.json \  
  --idempotency-key "import-${CI_PIPELINE_ID}"  
```
6. 6  
## Guard destructive steps  
Deletes require `--confirm`, which is exactly what you want in a pipeline: a cleanup job can only remove data when the flag is explicitly present. Keep `--confirm` out of any step that shouldn't delete, and parse `--json` output to assert the result:  
```  
firstsales connectors delete --org "$ORG" --workspace "$WS" \  
  --connector "$STALE_ID" --confirm --json | jq -e '.deleted == true'  
```
7. 7  
## Parse JSON, don't scrape  
All output is JSON by default — pipe it to `jq` to make decisions, set job outputs, or gate later steps. Never scrape the app UI or hit private endpoints from CI; if a command returns `unsupported_operation`, fail the step and surface it rather than working around it.

## Pro tips

Hard-won shortcuts that keep warm-up on track.

1

### whoami as the first job step

A one-line auth check at the top of the job turns a bad/expired key into an instant, obvious failure instead of a partial, confusing one.

2

### Derive idempotency keys from CI variables

Commit SHA, pipeline ID, run number — any stable per-run value makes a retried step a safe no-op. Never use a timestamp or random value; those defeat the purpose.

3

### Two-phase: dry-run on PRs, apply on main

Mirror infra-as-code. --dry-run on pull requests shows the diff for review; the real write only runs after merge. Cheap safety for irreversible actions.

4

### Assert on JSON output

Pipe --json to jq -e so the step actually fails when the result isn't what you expected, instead of exiting 0 on a soft error.

## Frequently asked questions

Should CI use the CLI or the API?

The **CLI**. For CI/CD and scripts it's recommended over raw HTTP because it standardizes credential handling and output and keeps the destructive-op safeguards built in.

How do I handle the API key in CI?

Put it in your CI provider's secret store and expose it as `FIRSTSALES_API_KEY`. Don't hardcode it in the workflow, and never `echo` it — most CI systems mask secrets in logs only if you don't print them yourself.

How do I preview a change before it runs?

Use `--dry-run` to print the request without sending it. A common pattern is dry-run on pull requests and the real write only on the main branch.

How do I make a step safe to retry?

Pass `--idempotency-key` derived from a stable value like the commit SHA or pipeline ID. A retried step with the same key is a no-op instead of a duplicate write.

How do I stop a cleanup job from over-deleting?

Deletes require `--confirm`, so a step can only remove data when the flag is explicitly present. Keep it off non-destructive steps, and assert on the `--json` result with `jq -e`.

Can I make decisions from the output?

Yes — output is JSON by default. Pipe it to `jq` to gate later steps or set job outputs. Don't scrape the UI; on `unsupported_operation`, fail the step and report it.

## Ready to put this into practice?

Start your FirstSales trial and launch a warmed, authenticated mailbox in minutes.

[Start for $1](https://app.firstsales.io)

[All tutorials](/tutorial/)