Saleshandy API: Create Sequences & Import Prospects Programmatically

By Joey T · April 9, 2026 · 10 min read

Saleshandy's API documentation is scattered across multiple pages with inconsistent examples. I spent hours figuring out what works. Here's everything in one place — authentication, prospect import, email accounts, and the specific gotchas that trip up every developer.

Authentication

⚠️ Three things that will waste your time:
  1. The header is x-api-key (lowercase) — not api-key, not Authorization: Bearer
  2. The base URL is https://open-api.saleshandy.com — NOT api.saleshandy.com
  3. GET requests that include Content-Type return 406. Don't set it for GETs.
# Correct authentication
curl "https://open-api.saleshandy.com/v1/sequences" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"

Getting Your API Key

Go to my.saleshandy.com → Settings → API Key → Create API Key. Label it anything. Copy the key immediately — you can't view it again.

💡 Note: The dashboard URL is my.saleshandy.com, not app.saleshandy.com. The latter is a different/legacy URL that shows an empty state. I wasted 2 hours on this.

Finding the OpenAPI Spec

The Swagger UI at open-api.saleshandy.com/api-doc/ is JavaScript-rendered and hard to scrape. The raw JSON spec is at:

https://open-api.saleshandy.com/api-doc-json

This gives you every endpoint, schema, and parameter in one JSON file.

Core Endpoints

List Sequences

GET /v1/sequences

# Response includes sequence IDs, titles, and step IDs
{
  "payload": [
    {
      "id": "LGz3m8KNPr",
      "title": "My Sequence",
      "steps": [
        {"id": "gOwE4X1Mzb", "name": "Step 1"}
      ]
    }
  ]
}

Import Prospects

The most important endpoint. Use the field-name version — it's simpler:

POST /v1/sequences/prospects/import-with-field-name

{
  "prospectList": [
    {
      "First Name": "John",
      "Last Name": "Smith",
      "Email": "john@example.com",
      "Company": "ACME Inc",
      "Job Title": "CEO"
    }
  ],
  "stepId": "YOUR_STEP_ID",
  "verifyProspects": true,
  "conflictAction": "noUpdate"
}
⚠️ conflictAction values: Must be exactly overwrite, noUpdate, or addMissingFields. Anything else (like "skip") returns a 400 error with an unhelpful message.

Import is async. You get a requestId back and can check status:

GET /v1/prospects/import-status/{requestId}

Email Accounts

# List all email accounts
POST /v1/email-accounts
Body: {"page": 1, "pageSize": 20}

# Add accounts to a sequence
POST /v1/sequences/{sequenceId}/email-accounts/add
Body: {"emailAccountIds": ["id1", "id2"]}

Get Step Content

GET /v1/sequences/{sequenceId}/steps/{stepId}

# Returns step variants with subject and HTML content

Activate/Pause Sequences

POST /v1/sequences/status
Body: {"sequenceIds": ["id"], "status": "active"}

What the API Can't Do

As of April 2026, these operations require the web UI:

This means you need to create your sequence structure in the UI first, then use the API for everything else (importing prospects, managing accounts, activating/pausing).

Complete Import Script

import json, subprocess, csv, time

API_KEY = "YOUR_KEY"
BASE = "https://open-api.saleshandy.com"

def api(method, endpoint, data=None):
    cmd = ['curl', '-s', '-X', method, f'{BASE}{endpoint}',
           '-H', f'x-api-key: {API_KEY}',
           '-H', 'Accept: application/json']
    if data:
        cmd += ['-H', 'Content-Type: application/json',
                '-d', json.dumps(data)]
    r = subprocess.run(cmd, capture_output=True, text=True)
    return json.loads(r.stdout)

# List sequences to get step IDs
seqs = api('GET', '/v1/sequences')
for s in seqs['payload']:
    print(f"{s['title']}: {s['id']}")
    for step in s['steps']:
        print(f"  {step['name']}: {step['id']}")

# Import from CSV
with open('leads.csv') as f:
    prospects = []
    for row in csv.DictReader(f):
        name = row['name'].split(' ', 1)
        prospects.append({
            "First Name": name[0],
            "Last Name": name[1] if len(name) > 1 else "",
            "Email": row['email'],
            "Company": row.get('organization', ''),
            "Job Title": row.get('title', '')
        })

# Import in batches of 50
STEP_ID = "YOUR_STEP_ID"
for i in range(0, len(prospects), 50):
    batch = prospects[i:i+50]
    result = api('POST',
        '/v1/sequences/prospects/import-with-field-name',
        {"prospectList": batch, "stepId": STEP_ID,
         "verifyProspects": True, "conflictAction": "noUpdate"})
    print(f"Batch {i}: {result['payload']['requestId']}")
    time.sleep(1)

Skip the setup pain

Get production-ready scripts for Saleshandy + Apollo integration.

Get the Skill Pack — $9

Written by Joey T. Follow @JoeyTbuilds for more.