Claude Code skill pack for Salesloft (18 skills)
Installation
Open Claude Code and run this command:
/plugin install salesloft-pack@claude-code-plugins-plus
Use --global to install for all projects, or --project for current project only.
What It Does
> 18 production-ready Claude Code skills for SalesLoft -- real REST API v2 code with OAuth, cadences, people, and activity tracking.
Skills (18)
'Set up CI/CD pipelines for SalesLoft integrations with GitHub Actions.
SalesLoft CI Integration
Overview
GitHub Actions workflows for testing SalesLoft API integrations: unit tests with mocked responses, integration tests against the live API, and OAuth token validation.
Instructions
Step 1: GitHub Actions Workflow
# .github/workflows/salesloft-ci.yml
name: SalesLoft Integration
on:
push:
branches: [main]
pull_request:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
with: { name: coverage, path: coverage/ }
integration-tests:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
SALESLOFT_API_KEY: ${{ secrets.SALESLOFT_TEST_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Verify SalesLoft connectivity
run: |
curl -sf -H "Authorization: Bearer $SALESLOFT_API_KEY" \
https://api.salesloft.com/v2/me.json | jq '.data.email'
- run: npm run test:integration
Step 2: Configure Secrets
# Store test API key (read-only scoped)
gh secret set SALESLOFT_TEST_API_KEY --body "your-test-token"
# For webhook testing
gh secret set SALESLOFT_WEBHOOK_SECRET --body "your-webhook-secret"
Step 3: Integration Test Structure
// tests/integration/salesloft.test.ts
import { describe, it, expect } from 'vitest';
import { createClient } from '../../src/salesloft/client';
const SKIP = !process.env.SALESLOFT_API_KEY;
describe.skipIf(SKIP)('SalesLoft Integration', () => {
const api = createClient();
it('authenticates and returns user', async () => {
const { data } = await api.get('/me.json');
expect(data.data.email).toBeTruthy();
});
it('lists people with pagination', async () => {
const { data } = await api.get('/people.json', {
params: { per_page: 5 },
});
expect(data.metadata.paging).toHaveProperty('total_count');
expect(data.data.length).toBeLessThanOrEqual(5);
});
it('lists cadences', async () => {
const { data } = await api.get('/cadences.json', {
params: { per_page: 5 },
});
expect(Array.isArray(data.data)).toBe(true);
});
it('handles rate limit headers', async () => {
const resp = await api.get('/people.json', { params: { per_page: 1 } });
expect(resp.headers).toHaveProperty('x-ratelimit-limit-per-minute');
});
}'Diagnose and fix SalesLoft API errors: 401, 403, 422, 429, and 5xx.
SalesLoft Common Errors
Overview
Quick reference for the most common SalesLoft REST API v2 errors. All errors return JSON with a message field. Rate limiting uses a cost-based system (600 cost/minute).
Error Reference
401 Unauthorized -- Invalid or Expired Token
{ "error": "Not authorized", "error_description": "The access token is invalid" }
Causes: Token expired, revoked, or wrong environment key.
Fix:
// Check if token works
const { data } = await api.get('/me.json').catch(err => {
if (err.response?.status === 401) {
console.error('Token invalid. Refreshing...');
return refreshAccessToken(storedRefreshToken);
}
throw err;
});
403 Forbidden -- Insufficient Scopes
{ "error": "Forbidden", "error_description": "Insufficient scope for this resource" }
Fix: Check app scopes in developer portal. Common issue: using user-level OAuth for team-level endpoints (cadences, team templates).
404 Not Found -- Wrong Endpoint or Deleted Resource
# Verify endpoint format -- all endpoints end with .json
curl -s -H "Authorization: Bearer $TOKEN" \
https://api.salesloft.com/v2/people/12345.json
# NOT /people/12345 (missing .json suffix)
422 Unprocessable Entity -- Validation Errors
{
"errors": [
{ "attribute": "email_address", "message": "is required" },
{ "attribute": "email_address", "message": "has already been taken" }
]
}
Common 422 causes:
| Field | Error | Solution |
|---|---|---|
email_address |
required | Must include when creating a person |
email_address |
already taken | Use GET /people.json?email_addresses[]=x first |
cadence_id |
not active | Cadence must have current_state: 'active' |
person_id |
already enrolled | Check existing cadence memberships |
429 Too Many Requests -- Rate Limit Exceeded
X-RateLimit-Limit-Per-Minute: 600
X-RateLimit-Remaining-Per-Minute: 0
Retry-After: 42
SalesLoft uses cost-based rate limiting:
- Default: each request costs 1 point
- Pages 101-150: 3 points per request
- Pages 151-250: 8 points per request
- Pages 251-500: 10 po
'Manage SalesLoft people, cadences, and email steps via the REST API.
SalesLoft Core Workflow A: People & Cadences
Overview
The primary SalesLoft workflow: manage people records, build cadences (multi-step outbound sequences), and enroll prospects. Uses REST API v2 endpoints: /people.json, /cadences.json, /cadence_memberships.json, /steps.json.
Prerequisites
- Completed
salesloft-install-authsetup - Understanding of SalesLoft cadence model (cadences contain steps, people are enrolled via memberships)
Instructions
Step 1: Search and Deduplicate People
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.salesloft.com/v2',
headers: { Authorization: `Bearer ${process.env.SALESLOFT_API_KEY}` },
});
// Search by email to avoid duplicates (supports array filter)
async function findOrCreatePerson(email: string, attrs: Record<string, any>) {
const { data: existing } = await api.get('/people.json', {
params: { email_addresses: [email] },
});
if (existing.data.length > 0) {
console.log(`Found existing: ${existing.data[0].id}`);
return existing.data[0];
}
const { data: created } = await api.post('/people.json', {
email_address: email, ...attrs,
});
console.log(`Created: ${created.data.id}`);
return created.data;
}
Step 2: List and Select Cadences
// List cadences -- filter by team_cadence, current_state
const { data: cadences } = await api.get('/cadences.json', {
params: { team_cadence: true, per_page: 50 },
});
// Each cadence has: id, name, current_state (draft|active|paused|archived)
// counts.people_count, cadence_framework_id, team_cadence
const activeCadences = cadences.data.filter(
(c: any) => c.current_state === 'active'
);
console.log(`Active cadences: ${activeCadences.length}`);
Step 3: Enroll Person in Cadence
// Create a cadence membership to enroll a person
async function enrollInCadence(personId: number, cadenceId: number) {
try {
const { data } = await api.post('/cadence_memberships.json', {
person_id: personId,
cadence_id: cadenceId,
});
console.log(`Enrolled person ${personId} in cadence ${data.data.cadence.name}`);
return data.data;
} catch (err: any) {
if (err.response?.status === 422) {
console.warn('Person already enrolled or cadence not active');
}
throw err;
}
}
Step 4: Bulk Import with Cadence Assignment
// Import a CSV of prospects and enroll in a cadence
async function bulkEnroll(prospects: Array<{ email: string; name: string }>, cadenceId: number) {
const results = { enrolled: 0, skipped: 0, errors: 0 };
for (const prospect of prospects) {
try {
'Track SalesLoft activities, emails, calls, and analytics via the REST.
SalesLoft Core Workflow B: Activities & Analytics
Overview
Track and analyze sales activities: emails sent/opened/clicked/replied, calls logged, and engagement metrics. Uses REST API v2 endpoints: /activities/emails.json, /activities/calls.json, /actiondetails/sentemails.json.
Prerequisites
- Completed
salesloft-core-workflow-a - People and cadences already configured
Instructions
Step 1: List Email Activities
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.salesloft.com/v2',
headers: { Authorization: `Bearer ${process.env.SALESLOFT_API_KEY}` },
});
// Get email activities for a specific person
const { data: emails } = await api.get('/activities/emails.json', {
params: {
person_id: 1234,
per_page: 50,
sort_by: 'updated_at',
sort_direction: 'DESC',
},
});
emails.data.forEach((email: any) => {
console.log(`${email.subject} | Status: ${email.status}`);
console.log(` Opens: ${email.counts.opens}, Clicks: ${email.counts.clicks}`);
console.log(` Replied: ${email.counts.replies > 0 ? 'Yes' : 'No'}`);
});
Step 2: Log Call Activities
// List calls with disposition data
const { data: calls } = await api.get('/activities/calls.json', {
params: { per_page: 50, sort_by: 'created_at', sort_direction: 'DESC' },
});
calls.data.forEach((call: any) => {
console.log(`${call.to} | Duration: ${call.duration}s`);
console.log(` Disposition: ${call.disposition}`);
console.log(` Sentiment: ${call.sentiment}`);
console.log(` Notes: ${call.notes || '(none)'}`);
});
Step 3: Build Engagement Dashboard
// Aggregate email engagement metrics across a cadence
async function cadenceEngagement(cadenceId: number) {
let page = 1;
const stats = { sent: 0, opened: 0, clicked: 0, replied: 0 };
while (true) {
const { data } = await api.get('/activities/emails.json', {
params: { cadence_id: cadenceId, per_page: 100, page },
});
for (const email of data.data) {
stats.sent++;
if (email.counts.opens > 0) stats.opened++;
if (email.counts.clicks > 0) stats.clicked++;
if (email.counts.replies > 0) stats.replied++;
}
if (page >= data.metadata.paging.total_pages) break;
page++;
}
return {
...stats,
openRate: ((stats.opened / stats.sent) * 100).toFixed(1) + '%',
clickRate: ((stats.clicked / stats.sent) * 100).toFixed(1) + '%',
replyRate: ((stats.replied / stats.sent) * 100).toFixed(1) + '%',
};
}
const metrics = await cadenceEngagement(500);
// { sent: 247, opened: 148, clicked: 32, replied: 19,
// openRate: '5'Optimize SalesLoft API costs by reducing request volume and deep pagination.
SalesLoft Cost Tuning
Overview
SalesLoft API cost is rate-limit-based (600 cost points/minute), not dollar-based. The primary cost driver is deep pagination -- pages beyond 100 cost 3-30x more. Optimize by using incremental sync, caching, and avoiding full table scans.
Cost Model
Rate Limit Cost Structure
| Page Range | Cost per Request | Notes |
|---|---|---|
| 1-100 | 1 point | Standard |
| 101-150 | 3 points | 3x multiplier |
| 151-250 | 8 points | 8x multiplier |
| 251-500 | 10 points | 10x multiplier |
| 501+ | 30 points | 30x multiplier |
Budget: 600 points/minute. This is the ceiling regardless of SalesLoft plan tier.
Cost Calculator
function calculateSyncCost(totalRecords: number, perPage = 100) {
const pages = Math.ceil(totalRecords / perPage);
let cost = 0;
for (let p = 1; p <= pages; p++) {
if (p <= 100) cost += 1;
else if (p <= 150) cost += 3;
else if (p <= 250) cost += 8;
else if (p <= 500) cost += 10;
else cost += 30;
}
const minutes = Math.ceil(cost / 600);
return { pages, cost, minutes, pointsPerMinute: 600 };
}
// Examples:
// 1,000 records = 10 pages = 10 points = instant
// 10,000 records = 100 pages = 100 points = instant
// 25,000 records = 250 pages = 100 + 150 + 800 = 1050 points = ~2 min
// 50,000 records = 500 pages = 100 + 150 + 800 + 2500 = 3550 points = ~6 min
Cost Reduction Strategies
Strategy 1: Incremental Sync (Biggest Win)
// Full sync of 25k people: 1050 points
// Incremental sync of last hour's changes: ~1-5 points
const { data } = await api.get('/people.json', {
params: {
updated_at: { gt: lastSyncTime }, // Only changed records
per_page: 100,
page: 1,
},
});
Strategy 2: Cache Frequently Accessed Data
// Cadence list rarely changes -- cache for 5 minutes
const cadences = await cachedGet('/cadences.json', { per_page: 100 }, 300_000);
// Person lookup by email -- cache for 1 minute
const person = await cachedGet('/people.json', { email_addresses: [email] }, 60_000);
Strategy 3: Webhook-Driven Instead of Polling
// EXPENSIVE: Poll every minute for changes
setInterval(() => api.get('/people.json', { params: { updated_at: { gt: lastCheck } }}), 60_000);
// FREE: Receive webhooks for changes (0 API cost)
app.post('/webhooks/salesloft', (req, res) => {
handlePersonUpdate(req.body.data);
res.status(200).send();
});
Strategy 4: Request Consolidation
'Collect SalesLoft debug evidence for support tickets and troubleshooting.
SalesLoft Debug Bundle
Overview
Collect diagnostic data for SalesLoft API issues: authentication state, rate limit usage, endpoint reachability, and API log entries. SalesLoft provides API Logs in the developer portal for request-level debugging.
Prerequisites
- SalesLoft API key or OAuth token
curlandjqavailable- Access to SalesLoft developer portal for API logs
Instructions
Step 1: Create Debug Script
#!/bin/bash
# salesloft-debug.sh
set -euo pipefail
BUNDLE="salesloft-debug-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BUNDLE"
TOKEN="${SALESLOFT_API_KEY:?Set SALESLOFT_API_KEY}"
echo "=== SalesLoft Debug Bundle ===" | tee "$BUNDLE/summary.txt"
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$BUNDLE/summary.txt"
Step 2: Check Authentication & Identity
echo "--- Auth Check ---" >> "$BUNDLE/summary.txt"
curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
https://api.salesloft.com/v2/me.json \
| jq '{id: .data.id, email: .data.email, name: .data.name, role: .data.role}' \
>> "$BUNDLE/auth.json" 2>&1
Step 3: Check Rate Limit State
echo "--- Rate Limits ---" >> "$BUNDLE/summary.txt"
curl -sI -H "Authorization: Bearer $TOKEN" \
https://api.salesloft.com/v2/people.json?per_page=1 \
| grep -iE '(ratelimit|retry-after|x-request-id)' \
>> "$BUNDLE/rate-limits.txt" 2>&1
Step 4: Test Key Endpoints
echo "--- Endpoint Health ---" >> "$BUNDLE/summary.txt"
for endpoint in people.json cadences.json activities/emails.json; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
"https://api.salesloft.com/v2/$endpoint?per_page=1")
echo "$endpoint: HTTP $STATUS" >> "$BUNDLE/endpoints.txt"
done
Step 5: Collect Environment Info
echo "--- Environment ---" >> "$BUNDLE/summary.txt"
echo "Node: $(node --version 2>/dev/null || echo 'N/A')" >> "$BUNDLE/env.txt"
echo "Python: $(python3 --version 2>/dev/null || echo 'N/A')" >> "$BUNDLE/env.txt"
echo "SALESLOFT_BASE_URL: ${SALESLOFT_BASE_URL:-default}" >> "$BUNDLE/env.txt"
echo "SALESLOFT_API_KEY: ${SALESLOFT_API_KEY:+[SET]}" >> "$BUNDLE/env.txt"
# Redact .env secrets
cat .env 2>/dev/null | sed 's/=.*/=***REDACTED***/' >> "$BUNDL'Deploy SalesLoft integrations to Vercel, Fly.
SalesLoft Deploy Integration
Overview
Deploy SalesLoft-powered applications to cloud platforms with proper secrets management, webhook endpoint configuration, and health checks. SalesLoft requires HTTPS webhook endpoints and OAuth tokens stored securely.
Instructions
Vercel Deployment
# Set secrets
vercel env add SALESLOFT_CLIENT_ID production
vercel env add SALESLOFT_CLIENT_SECRET production
vercel env add SALESLOFT_WEBHOOK_SECRET production
# Deploy
vercel --prod
// api/webhooks/salesloft.ts (Vercel serverless function)
import { verifyWebhookSignature } from '../../lib/salesloft';
export const config = { api: { bodyParser: false } }; // Raw body for HMAC
export default async function handler(req: Request) {
const body = await req.text();
const sig = req.headers.get('x-salesloft-signature')!;
const ts = req.headers.get('x-salesloft-timestamp')!;
if (!verifyWebhookSignature(Buffer.from(body), sig, ts, process.env.SALESLOFT_WEBHOOK_SECRET!)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
// Process event...
return new Response(JSON.stringify({ received: true }), { status: 200 });
}
Fly.io Deployment
# fly.toml
app = "salesloft-sync"
primary_region = "iad"
[env]
NODE_ENV = "production"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
[[services.http_checks]]
interval = "30s"
timeout = "5s"
path = "/health"
fly secrets set SALESLOFT_CLIENT_ID=xxx SALESLOFT_CLIENT_SECRET=xxx
fly secrets set SALESLOFT_WEBHOOK_SECRET=xxx
fly deploy
Cloud Run Deployment
# Store secrets in Secret Manager
echo -n "client-id" | gcloud secrets create salesloft-client-id --data-file=-
echo -n "client-secret" | gcloud secrets create salesloft-client-secret --data-file=-
# Deploy with secret mounts
gcloud run deploy salesloft-sync \
--image gcr.io/$PROJECT_ID/salesloft-sync \
--region us-central1 \
--set-secrets=SALESLOFT_CLIENT_ID=salesloft-client-id:latest \
--set-secrets=SALESLOFT_CLIENT_SECRET=salesloft-client-secret:latest \
--allow-unauthenticated
Health Check (All Platforms)
app.get('/health', async (req, res) => {
const start = Date.now();
try {
const { data } = await api.get('/me.json');
res.json({
status: 'healthy',
salesloft: { user: data.data.email, latencyMs: Date.now() - start },
});
} catch (err: any) {
res.status(503).json({
status: 'degraded',
salesloft: { error: err.message, latencyMs: Date."Create a minimal working SalesLoft example \u2014 list people and create\.
SalesLoft Hello World
Overview
List people and create a new person — the two fundamental SalesLoft API operations. Uses the REST API v2 at https://api.salesloft.com/v2/. All endpoints return JSON with a data wrapper and support pagination via page and per_page params.
Prerequisites
- Valid OAuth token or API key (see
salesloft-install-auth) SALESLOFTAPIKEYenvironment variable set
Instructions
Step 1: List People
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.salesloft.com/v2',
headers: { Authorization: `Bearer ${process.env.SALESLOFT_API_KEY}` },
});
// List people — returns paginated results
const { data } = await api.get('/people.json', {
params: { per_page: 25, page: 1 },
});
console.log(`Total people: ${data.metadata.paging.total_count}`);
data.data.forEach((person: any) => {
console.log(` ${person.display_name} <${person.email_address}>`);
});
Step 2: Create a Person
// Create a new person record
const { data: created } = await api.post('/people.json', {
email_address: 'prospect@example.com',
first_name: 'Alex',
last_name: 'Johnson',
title: 'VP Engineering',
company_name: 'Acme Corp',
phone: '+1-555-0100',
city: 'Austin',
state: 'TX',
custom_fields: {
lead_source: 'website',
},
});
console.log(`Created person: ${created.data.id} — ${created.data.display_name}`);
Step 3: Add Person to a Cadence
// First, list available cadences
const { data: cadences } = await api.get('/cadences.json', {
params: { per_page: 10 },
});
const cadenceId = cadences.data[0].id;
// Add person to cadence
const { data: membership } = await api.post('/cadence_memberships.json', {
person_id: created.data.id,
cadence_id: cadenceId,
});
console.log(`Added to cadence: ${membership.data.cadence.name}`);
Output
Total people: 1,247
Alex Johnson <prospect@example.com>
Created person: 98765 — Alex Johnson
Added to cadence: Q1 Outbound Sequence
Error Handling
| Error | Cause | Solution | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
422 Unprocessable Entity |
Missing required field (email) | Ensure email_address is provided |
||||||||||||||||||||||||||||||||||||||||||||||
409 Conflict |
Duplicate email address | Search existing people first with ?email_addresses[]= |
||||||||||||||||||||||||||||||||||||||||||||||
401 Unauthorized |
Invalid/expired token | Refresh OAuth token | ||||||||||||||||||||||||||||||||||||||||||||||
429 Too Many Requests |
| Operation | Typical | With Caching |
|---|---|---|
| GET /me.json | 80ms | N/A (auth) |
| GET /people.json (page 1) | 120ms | 1ms (cached) |
| POST /people.json | 200ms | N/A (write) |
| GET /activities/emails.json | 150ms | 1ms (cached) |
| Full sync (10k people) | ~20min | ~5min (incremental) |
Instructions
Step 1: Response Caching
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({ max: 5000, ttl: 60_000 });
async function cachedGet<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const key = `${endpoint}:${JSON.stringify(params || {})}`;
const hit = cache.get(key);
if (hit) return hit as T;
const { data } = await api.get(endpoint, { params });
cache.set(key, data);
return data;
}
// Cache people lookups (frequent during cadence enrollment)
const person = await cachedGet('/people.json', { email_addresses: ['alex@co.com'] });
Step 2: Incremental Sync with updated_at
// Only fetch records changed since last sync
async function incrementalSync(lastSyncTime: string) {
const updated: any[] = [];
let page = 1;
while (true) {
const { data } = await api.get('/people.json', {
params: {
updated_at: { gt: lastSyncTime }, // ISO 8601
per_page: 100,
page,
sort_by: 'updated_at',
sort_direction: 'ASC',
},
});
updated.push(...data.data);
if (page >= data.metadata.paging.total_pages) break;
page++;
}
return { updated, newSyncTime: new Date().toISOString() };
}
Step 3: Avoid Deep Pagination Cost
// Deep pages cost 3-30x. Instead of paginating all 25k records,
// use updated_at filter to get incremental changes
function shouldUseIncremental(totalCount: number): boolean {
// If total records > 1000, incremental sync is more efficient
// Full pagination of 250 pages = 910 cost points vs.
// incremental of last 50 changes = 1 page = 1 point
return totalCount > 1000;
}
Step 4: Connection Pooling
import { Agent } from 'https';
const agent = new Agent({
keepAlive: true,
maxSockets: 10, // Max concurrent connections
maxFreeSockets: 5, // Keep idle connections alive
timeout: 30_000'Production readiness checklist for SalesLoft API integrations.
SalesLoft Production Checklist
Overview
Go-live checklist for SalesLoft API integrations covering auth, error handling, monitoring, rate limits, and rollback procedures.
Pre-Launch Checklist
Authentication & Secrets
- [ ] Production OAuth app created (separate from dev/staging)
- [ ] Tokens stored in secret manager (AWS Secrets Manager, GCP Secret Manager, Vault)
- [ ] Token refresh logic tested (simulated expired token)
- [ ] Webhook signing secret rotated from dev value
Error Handling
- [ ] 401 triggers automatic token refresh (not crash)
- [ ] 429 handled with backoff using
Retry-Afterheader - [ ] 5xx retried with exponential backoff (max 3 attempts)
- [ ] 422 validation errors logged with request payload
- [ ] Circuit breaker prevents cascade during SalesLoft outages
Rate Limiting
- [ ] Cost-based budget calculated for expected volume
- [ ] Deep pagination avoided (page > 100 costs 3-30x)
- [ ] Bulk operations use
p-queueor similar throttle - [ ] Rate limit headers logged for capacity planning
Monitoring & Alerting
// Health check endpoint
app.get('/health', async (req, res) => {
try {
const start = Date.now();
await api.get('/me.json');
res.json({
status: 'healthy',
salesloft: { connected: true, latencyMs: Date.now() - start },
});
} catch {
res.status(503).json({ status: 'degraded', salesloft: { connected: false } });
}
});
- [ ] Health check includes SalesLoft connectivity
- [ ] Alert on 5xx error rate > 5/min (P1)
- [ ] Alert on 429 rate > 10/min (P2)
- [ ] Alert on auth failure (P1 -- token may be revoked)
- [ ] Latency p99 tracked (baseline: 300ms reads, 500ms writes)
Data Integrity
- [ ] Idempotency keys on all create/update operations
- [ ] Duplicate detection by email before person creation
- [ ] Webhook events deduplicated by event ID
- [ ] Audit log captures all API mutations
Rollback Procedure
# 1. Revert deployment
kubectl rollout undo deployment/salesloft-integration
# or: git revert HEAD && git push
# 2. Verify old version healthy
curl -f https://app.example.com/health
# 3. Pause any running cadence syncs
# 4. Notify sales team of rollback
Post-Launch Verification
# Smoke test production endpoints
curl -s -H "Authorization: Bearer $PROD_TOKEN" \
https://api.salesloft.com/v2/me.json | jq '.data.email'
curl -s -H "Authorization: Bearer $PROD_TOKEN" \
'https://api.salesloft.com/v2/people.json?per_page=1' | jq '.metadata.paging.total_count'
<'Handle SalesLoft cost-based rate limiting with backoff and request budgeting.
SalesLoft Rate Limits
Overview
SalesLoft uses cost-based rate limiting at 600 cost per minute. Each request costs 1 point by default, but deep pagination multiplies the cost. Rate limit state is returned in response headers.
Rate Limit Model
Cost per Request
| Page Range | Cost per Request | Example: 1000 records at 100/page |
|---|---|---|
| 1-100 | 1 point | Pages 1-10: 10 points |
| 101-150 | 3 points | N/A for this example |
| 151-250 | 8 points | N/A |
| 251-500 | 10 points | N/A |
| 501+ | 30 points | N/A |
Budget: 600 points/minute. A simple 10-page pagination costs 10 points. But paginating to page 500 costs 10 + 150 + 800 + 2500 = 3460 points (nearly 6 minutes of budget).
Response Headers
X-RateLimit-Limit-Per-Minute: 600
X-RateLimit-Remaining-Per-Minute: 487
Retry-After: 42 # Only present on 429 responses
X-Request-Id: abc-123 # For support tickets
Instructions
Step 1: Rate-Limit-Aware Client
import axios, { AxiosInstance } from 'axios';
class SalesloftRateLimiter {
private remaining = 600;
private resetAt = Date.now();
constructor(private client: AxiosInstance) {
client.interceptors.response.use(
(res) => {
this.remaining = parseInt(res.headers['x-ratelimit-remaining-per-minute'] || '600');
return res;
},
async (err) => {
if (err.response?.status === 429) {
const wait = parseInt(err.response.headers['retry-after'] || '60');
console.warn(`Rate limited. Waiting ${wait}s (X-Request-Id: ${err.response.headers['x-request-id']})`);
await this.sleep(wait * 1000);
return this.client.request(err.config);
}
throw err;
}
);
}
async throttledRequest<T>(fn: () => Promise<T>): Promise<T> {
if (this.remaining < 10) {
const waitMs = Math.max(0, this.resetAt - Date.now());
if (waitMs > 0) await this.sleep(waitMs);
}
return fn();
}
private sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); }
}
Step 2: Pagination Cost Calculator
function paginationCost(totalRecords: number, perPage: number = 100): number {
const totalPages = Math.ceil(totalRecords / perPage);
let cost = 0;
for (let p = 1; p <= totalPages; p++) {
if (p <= 100) cost += 1;
else if (p <= 150) cost += 3;
else if (p <= 250) cost += 8;
else if (p <= 500) cost += 10;
else cost += 30;
}
return cost;
}
// Budget check before large exports
const totalPeople = 25000; // from meta'Production architecture for SalesLoft API integrations with service.
SalesLoft Reference Architecture
Overview
Production architecture for SalesLoft API integrations: typed API client, service layer with caching, webhook processor, and background sync. Designed around SalesLoft's REST API v2 with cost-based rate limiting.
Project Structure
salesloft-integration/
├── src/
│ ├── salesloft/
│ │ ├── client.ts # Axios wrapper with rate-limit handling
│ │ ├── types.ts # Person, Cadence, Activity types
│ │ ├── paginator.ts # AsyncGenerator pagination
│ │ └── errors.ts # SalesloftApiError class
│ ├── services/
│ │ ├── people-sync.ts # Bidirectional people sync
│ │ ├── cadence-manager.ts # Cadence CRUD + enrollment
│ │ └── activity-tracker.ts # Email/call activity aggregation
│ ├── webhooks/
│ │ ├── handler.ts # Signature verification + routing
│ │ └── processors/ # Per-event-type processors
│ ├── jobs/
│ │ ├── incremental-sync.ts # Cron: sync changed records
│ │ └── engagement-report.ts# Cron: aggregate daily metrics
│ └── api/
│ ├── health.ts # /health endpoint
│ └── webhooks.ts # /webhooks/salesloft endpoint
├── config/
│ ├── default.json
│ ├── production.json
│ └── test.json
└── tests/
├── unit/
└── integration/
Architecture Diagram
┌─────────────────────────────────┐
│ API Layer │
│ /health /webhooks/salesloft │
├─────────────────────────────────┤
│ Service Layer │
│ PeopleSync CadenceManager │
│ ActivityTracker │
├─────────────────────────────────┤
│ SalesLoft Client │
│ Typed API Pagination Retry │
├─────────────────────────────────┤
│ Infrastructure │
│ Redis Cache BullMQ Jobs │
│ PostgreSQL Prometheus │
└─────────────────────────────────┘
│ ▲
▼ │
┌─────────────────────────────────┐
│ SalesLoft REST API v2 │
│ /people /cadences /webhooks │
│ Rate: 600 cost/min │
└─────────────────────────────────┘
Key Components
Typed API Models
// src/salesloft/types.ts
export interface SalesloftPerson {
id: number;
display_name: string;
email_address: string;
first_name: string;
last_name: string;
title: string | null;
company_name: string | null;
phone: string | null;
city: string | null;
state: string | null;
tags: string[];
created_at: string;
updated_at: string;
}
export interface SalesloftCadence {
id: number;
name: string;
current_state: 'draft' | 'active' | 'paused' | 'archived';
team_cadence: boolean;
counts: { people_count: number };
}
export interface SalesloftActivity {
id: number;
action_type: 'email' | 'phone' | 'other' | 'integratio'Apply production-ready SalesLoft API patterns for TypeScript and Python.
SalesLoft SDK Patterns
Overview
Production-ready patterns for the SalesLoft REST API v2. There is no official TypeScript/Python SDK -- build a typed wrapper around https://api.salesloft.com/v2/ with automatic pagination, retry, and error normalization.
Prerequisites
- Completed
salesloft-install-authsetup axiosornode-fetchinstalled- Familiarity with async/await and generics
Instructions
Step 1: Typed API Client Singleton
// src/salesloft/client.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
interface SalesloftPaging {
per_page: number;
current_page: number;
total_pages: number;
total_count: number;
}
interface SalesloftListResponse<T> {
data: T[];
metadata: { paging: SalesloftPaging };
}
interface SalesloftSingleResponse<T> {
data: T;
}
let instance: AxiosInstance | null = null;
export function getSalesloftClient(): AxiosInstance {
if (!instance) {
instance = axios.create({
baseURL: process.env.SALESLOFT_BASE_URL || 'https://api.salesloft.com/v2',
headers: { Authorization: `Bearer ${process.env.SALESLOFT_API_KEY}` },
timeout: 30_000,
});
// Add response interceptor for rate-limit headers
instance.interceptors.response.use(undefined, handleRateLimitError);
}
return instance;
}
Step 2: Automatic Pagination Iterator
// SalesLoft paginates with page/per_page params, max 100 per page
async function* paginate<T>(
endpoint: string,
params: Record<string, any> = {},
): AsyncGenerator<T> {
const client = getSalesloftClient();
let page = 1;
let totalPages = 1;
do {
const { data: response } = await client.get<SalesloftListResponse<T>>(
endpoint, { params: { ...params, per_page: 100, page } }
);
for (const item of response.data) yield item;
totalPages = response.metadata.paging.total_pages;
page++;
} while (page <= totalPages);
}
// Usage: iterate all people
for await (const person of paginate<Person>('/people.json')) {
console.log(person.display_name);
}
Step 3: Error Handling with Rate-Limit Awareness
// SalesLoft uses cost-based rate limiting: 600 cost/min
// High-page requests (page > 100) cost 3-30 points instead of 1
async function handleRateLimitError(error: AxiosError) {
if (error.response?.status === 429) {
const retryAfter = parseInt(
error.response.headers['retry-after'] || '60', 10
);
console.warn(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
return getSalesloftClient().request(error.config!);
}
throw error;
}
class SalesloftApiError extends Error {
construct'Secure SalesLoft OAuth tokens, API keys, and webhook signatures.
SalesLoft Security Basics
Overview
Secure SalesLoft API integrations: OAuth token management, webhook signature verification, secret storage, and scope-based access control. SalesLoft uses OAuth 2.0 bearer tokens and HMAC-SHA256 webhook signatures.
Instructions
Step 1: Secret Storage
# .gitignore -- NEVER commit credentials
.env
.env.local
.env.*.local
# .env
SALESLOFT_CLIENT_ID=app-client-id
SALESLOFT_CLIENT_SECRET=app-secret
SALESLOFT_WEBHOOK_SECRET=webhook-signing-secret
// Validate secrets at startup
const required = ['SALESLOFT_CLIENT_ID', 'SALESLOFT_CLIENT_SECRET'];
for (const key of required) {
if (!process.env[key]) throw new Error(`Missing required env: ${key}`);
}
Step 2: Token Lifecycle Management
// Store tokens securely with expiry tracking
interface TokenStore {
accessToken: string;
refreshToken: string;
expiresAt: number; // Unix timestamp
}
async function getValidToken(store: TokenStore): Promise<string> {
// Refresh 5 minutes before expiry
if (Date.now() > (store.expiresAt - 300) * 1000) {
const refreshed = await refreshAccessToken(store.refreshToken);
store.accessToken = refreshed.access_token;
store.refreshToken = refreshed.refresh_token;
store.expiresAt = Math.floor(Date.now() / 1000) + refreshed.expires_in;
await persistTokenStore(store); // Save to DB or secret manager
}
return store.accessToken;
}
Step 3: Webhook Signature Verification
import crypto from 'crypto';
function verifyWebhookSignature(
rawBody: Buffer,
signature: string,
timestamp: string,
secret: string,
): boolean {
// Reject stale webhooks (replay attack prevention)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) return false; // 5-minute window
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody.toString()}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(expected)
);
}
Step 4: OAuth Scope Minimization
| Use Case | Required Scopes | Avoid |
|---|---|---|
| Read-only dashboard | people:read, cadences:read |
*:write |
| Cadence enrollment | people:read, cadence_memberships:create |
admin |
| Full sync | people:, cadences:, activities:read |
Team admin scopes |
Step 5: Security Checklist
- [ ] OAuth tokens stored in secret manager (not env files in prod)
- [ ] Refresh tokens encrypted at
'Migrate between SalesLoft API versions and handle breaking changes.
SalesLoft Upgrade & Migration
Overview
SalesLoft REST API is versioned at v2 with no official SDK -- migrations involve endpoint changes, auth flow updates, and response schema changes. The Cadence Import/Export API was a major addition. Key migration: SalesLoft rebranded some endpoints and added OAuth client credentials flow.
Migration Scenarios
Legacy API Key to OAuth 2.0
// BEFORE: Static API key (being deprecated for partner apps)
const api = axios.create({
headers: { Authorization: `Bearer ${process.env.SALESLOFT_API_KEY}` },
});
// AFTER: OAuth 2.0 with token refresh
class SalesloftOAuthClient {
private tokenStore: { access: string; refresh: string; expiresAt: number };
async getClient() {
if (Date.now() > this.tokenStore.expiresAt * 1000 - 300_000) {
await this.refreshToken();
}
return axios.create({
baseURL: 'https://api.salesloft.com/v2',
headers: { Authorization: `Bearer ${this.tokenStore.access}` },
});
}
private async refreshToken() {
const { data } = await axios.post('https://accounts.salesloft.com/oauth/token', {
grant_type: 'refresh_token',
refresh_token: this.tokenStore.refresh,
client_id: process.env.SALESLOFT_CLIENT_ID,
client_secret: process.env.SALESLOFT_CLIENT_SECRET,
});
this.tokenStore = {
access: data.access_token,
refresh: data.refresh_token,
expiresAt: Math.floor(Date.now() / 1000) + data.expires_in,
};
}
}
Adding Client Credentials Flow
// Client credentials: server-to-server, no user interaction
// Recommended for background sync jobs
async function getServiceToken(): Promise<string> {
const { data } = await axios.post('https://accounts.salesloft.com/oauth/token', {
grant_type: 'client_credentials',
client_id: process.env.SALESLOFT_CLIENT_ID,
client_secret: process.env.SALESLOFT_CLIENT_SECRET,
});
return data.access_token; // No refresh token -- request new when expired
}
Cadence Import/Export API Adoption
// Export cadence (portable format -- can import into any SalesLoft instance)
const { data: exported } = await api.get(`/cadence_exports/${cadenceId}.json`);
// Returns agnostic content: steps, email templates, timing
// Import cadence into another instance
const { data: imported } = await api.post('/cadence_imports.json', {
cadence_content: exported.data,
settings: {
name: 'Imported: Q1 Outbound',
shared: false,
},
});
Migration Checklist
- [ ] Audit all endpoints used (search codebase for
/v2/) - [ ] Check response fields consumed (SalesLoft may add/remove fields)
- [ ] Test with staging OAuth app first
- [ ] Update error handling for any new error codes <
'Implement SalesLoft webhook handling with signature verification and.
SalesLoft Webhooks & Events
Overview
Handle SalesLoft webhook notifications for real-time data sync. SalesLoft sends webhooks for person updates, email events (sent, opened, clicked, replied, bounced), call completions, and cadence membership changes. Webhooks use HMAC-SHA256 signatures.
Instructions
Step 1: Register Webhook in SalesLoft
Configure webhooks in SalesLoft Settings > Integrations > Webhooks:
- URL:
https://your-app.com/webhooks/salesloft - Events: Select specific events (person.updated, email.sent, etc.)
- Copy the webhook signing secret
Step 2: Signature Verification
import crypto from 'crypto';
import express from 'express';
function verifySalesloftWebhook(
rawBody: Buffer,
signature: string,
timestamp: string,
): boolean {
const secret = process.env.SALESLOFT_WEBHOOK_SECRET!;
// Replay protection: reject webhooks older than 5 minutes
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody.toString()}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Step 3: Event Router
interface SalesloftWebhookEvent {
event_type: string; // e.g., 'person.created', 'email.sent', 'call.completed'
event_id: string;
data: Record<string, any>;
created_at: string;
}
const handlers: Record<string, (data: any) => Promise<void>> = {
'person.created': async (data) => {
console.log(`New person: ${data.email_address}`);
await syncToExternalCRM(data);
},
'person.updated': async (data) => {
await updateExternalCRM(data.id, data);
},
'email.sent': async (data) => {
await logActivity('email_sent', data);
},
'email.opened': async (data) => {
await logActivity('email_opened', data);
},
'email.clicked': async (data) => {
await logActivity('email_clicked', data);
},
'email.replied': async (data) => {
await logActivity('email_replied', data);
await notifySalesRep(data.person_id, 'Reply received!');
},
'email.bounced': async (data) => {
await markEmailInvalid(data.person_id);
},
'call.completed': async (data) => {
await logActivity('call', { ...data, duration: data.duration });
},
};
Step 4: Express Webhook Endpoint
const app = express();
app.post('/webhooks/salesloft',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['x-salesloft-signature'] as strinReady to use salesloft-pack?
Related Plugins
supabase-pack
Complete Supabase integration skill pack with 30 skills covering authentication, database, storage, realtime, edge functions, and production operations. Flagship+ tier vendor pack.
vercel-pack
Complete Vercel integration skill pack with 30 skills covering deployments, edge functions, preview environments, performance optimization, and production operations. Flagship+ tier vendor pack.
clay-pack
Complete Clay integration skill pack with 30 skills covering data enrichment, waterfall workflows, AI agents, and GTM automation. Flagship+ tier vendor pack.
cursor-pack
Complete Cursor integration skill pack with 30 skills covering AI code editing, composer workflows, codebase indexing, and productivity features. Flagship+ tier vendor pack.
exa-pack
Complete Exa integration skill pack with 30 skills covering neural search, semantic retrieval, web search API, and AI-powered discovery. Flagship+ tier vendor pack.
firecrawl-pack
Complete Firecrawl integration skill pack with 30 skills covering web scraping, crawling, markdown conversion, and LLM-ready data extraction. Flagship+ tier vendor pack.