@saltfish-ai/cli
v0.1.5
Published
Saltfish CLI & SDK — programmatic access to the Saltfish platform
Maintainers
Readme
@saltfish-ai/cli
The official Saltfish CLI and TypeScript SDK. Manage flows, demos, analytics, and A/B tests from the command line or programmatically in your own code.
Installation
npm install @saltfish-ai/cliOr install globally for CLI access:
npm install -g @saltfish-ai/cliQuickstart
CLI
# Authenticate with your API key
saltfish auth login sk_live_abc123
# List your flows
saltfish flows list
# Get a flow with expanded nodes
saltfish flows get FLOW_ID --expand nodes,edges
# Deploy a flow to live
saltfish flows deploy FLOW_ID
# View analytics
saltfish analytics overview --start 2026-01-01SDK (TypeScript)
import { SaltfishClient } from '@saltfish-ai/cli';
const saltfish = new SaltfishClient({ apiKey: 'sk_live_...' });
// List all flows
const { data: flows } = await saltfish.flows.list();
// Get a flow with expanded nodes and edges
const { data: flow } = await saltfish.flows.get('flow_id', {
expand: ['nodes', 'edges'],
});
// Create and deploy a flow
const { data: newFlow } = await saltfish.flows.create({ title: 'Onboarding' });
await saltfish.flows.deploy(newFlow.id);Authentication
API keys are created in the Saltfish dashboard:
- Go to app.saltfish.ai → Settings → Developer
- Click Create API Key
- Copy the key (shown once) — it starts with
sk_live_orsk_test_
CLI auth
# Save your API key (verified on save)
saltfish auth login sk_live_abc123
# Check current config
saltfish auth status
# Remove stored credentials
saltfish auth logoutSDK auth
const saltfish = new SaltfishClient({
apiKey: 'sk_live_abc123',
});Environment variables
| Variable | Description |
|----------|-------------|
| SALTFISH_API_KEY | API key (overrides stored key) |
| SALTFISH_API_URL | API base URL (default: https://studio-api.saltfish.ai/studio/api) |
Environment variables take precedence over stored config, making it easy to use in CI/CD:
SALTFISH_API_KEY=sk_live_abc123 saltfish flows listSDK Reference
Client configuration
import { SaltfishClient } from '@saltfish-ai/cli';
const saltfish = new SaltfishClient({
apiKey: 'sk_live_...', // Required
baseUrl: 'https://...', // Optional — custom API endpoint
maxRetries: 2, // Optional — retries on 5xx/network errors (default: 2)
timeout: 30000, // Optional — request timeout in ms (default: 30000)
});Resource namespaces
The client exposes four resource namespaces:
| Namespace | Description |
|-----------|-------------|
| saltfish.flows | Interactive demo flows |
| saltfish.demos | Demo recordings |
| saltfish.analytics | Engagement analytics |
| saltfish.abTests | A/B test management |
Flows
// List flows (with optional filters)
const flows = await saltfish.flows.list({ status: 'live', limit: 10 });
// Get a single flow
const flow = await saltfish.flows.get('flow_id');
// Get with expanded resources
const flow = await saltfish.flows.get('flow_id', { expand: ['nodes', 'edges'] });
// Create a flow
const flow = await saltfish.flows.create({ title: 'My Flow' });
// Update a flow
const flow = await saltfish.flows.update('flow_id', { title: 'New Title' });
// Deploy to live
await saltfish.flows.deploy('flow_id');
// Unpublish
await saltfish.flows.unpublish('flow_id');
// Delete (soft delete)
await saltfish.flows.delete('flow_id');Demos
// List demos (with optional folder filter)
const demos = await saltfish.demos.list({ folderId: 'folder_id', limit: 10 });
// Get a demo with steps expanded
const demo = await saltfish.demos.get('demo_id', { expand: ['steps'] });
// Update metadata
await saltfish.demos.update('demo_id', { title: 'New Title', folderId: 'folder_id' });
// Duplicate
const copy = await saltfish.demos.duplicate('demo_id');
// Get shareable link
const { data: { url } } = await saltfish.demos.getShareLink('demo_id');
// Delete (soft delete)
await saltfish.demos.delete('demo_id');Analytics
// Overview — all flows and demos
const overview = await saltfish.analytics.overview({
startDate: '2026-01-01',
endDate: '2026-03-31',
});
// Per-flow analytics
const flowStats = await saltfish.analytics.flow('flow_id', {
startDate: '2026-01-01',
});
// Per-demo analytics
const demoStats = await saltfish.analytics.demo('demo_id');A/B Tests
// List all A/B tests
const tests = await saltfish.abTests.list();
// Get users in a userList test
const { userIds, count } = await saltfish.abTests.getUsers('test_id');
// Add users to a test
await saltfish.abTests.addUsers('test_id', ['[email protected]', '[email protected]']);
// Remove users from a test
await saltfish.abTests.removeUsers('test_id', ['[email protected]']);CLI Reference
saltfish auth
| Command | Description |
|---------|-------------|
| saltfish auth login <api-key> | Save and verify your API key |
| saltfish auth status | Show current config |
| saltfish auth set-url <url> | Point CLI at a custom API endpoint |
| saltfish auth logout | Remove stored credentials |
saltfish flows
| Command | Description |
|---------|-------------|
| saltfish flows list | List all flows |
| saltfish flows get <id> | Get flow details |
| saltfish flows create -t "Title" | Create a new flow |
| saltfish flows update <id> -t "Title" | Update flow metadata |
| saltfish flows deploy <id> | Deploy flow to live |
| saltfish flows unpublish <id> | Take a flow offline |
| saltfish flows delete <id> | Soft-delete a flow |
Options:
--status <live|draft> Filter by status (list only)
--expand <fields> Expand nested resources: nodes, edges (get only)
--limit <n> Max results (default: 25)
--cursor <cursor> Pagination cursor
-o, --output <format> Output format: json | table (default: table)saltfish demos
| Command | Description |
|---------|-------------|
| saltfish demos list | List all demo recordings |
| saltfish demos get <id> | Get demo details |
| saltfish demos update <id> -t "Title" | Update demo metadata |
| saltfish demos duplicate <id> | Duplicate a demo |
| saltfish demos share <id> | Get shareable link |
| saltfish demos delete <id> | Soft-delete a demo |
Options:
--folder <folderId> Filter by folder (list only), use "none" to unset (update)
--expand <fields> Expand nested resources: steps (get only)
--limit <n> Max results (default: 25)
--cursor <cursor> Pagination cursor
-o, --output <format> Output format: json | table (default: table)saltfish analytics
| Command | Description |
|---------|-------------|
| saltfish analytics overview | Aggregated engagement metrics |
| saltfish analytics flow <id> | Per-flow analytics |
| saltfish analytics demo <id> | Per-demo analytics |
Options:
--start <YYYY-MM-DD> Start date filter
--end <YYYY-MM-DD> End date filter
-o, --output <format> Output format: json | table (default: table)Pagination
All list endpoints use cursor-based pagination:
CLI
# First page
saltfish flows list --limit 10
# Next page (use the last ID from the previous response)
saltfish flows list --limit 10 --cursor LAST_FLOW_IDSDK
// First page
const page1 = await saltfish.flows.list({ limit: 10 });
console.log(page1.data); // Flow[]
console.log(page1.hasMore); // true
console.log(page1.totalCount); // 42
// Next page
if (page1.hasMore) {
const lastId = page1.data[page1.data.length - 1].id;
const page2 = await saltfish.flows.list({ limit: 10, cursor: lastId });
}Expandable Resources
By default, nested resources are omitted to keep responses fast. Use expand to include them:
CLI
# Include node data in flow response
saltfish flows get FLOW_ID --expand nodes,edges
# Include step data in demo response
saltfish demos get DEMO_ID --expand stepsSDK
const flow = await saltfish.flows.get('flow_id', {
expand: ['nodes', 'edges'],
});
// flow.data.nodes is now populated
// flow.data.edges is now populated| Resource | Expandable fields |
|----------|-------------------|
| Flow | nodes, edges |
| Demo | steps |
Error Handling
The API returns structured errors:
{
"error": {
"type": "not_found",
"message": "Flow xyz not found",
"code": "resource_not_found"
},
"requestId": "req_abc123"
}SDK error handling
import { SaltfishClient, SaltfishApiError } from '@saltfish-ai/cli';
try {
await saltfish.flows.get('nonexistent');
} catch (err) {
if (err instanceof SaltfishApiError) {
console.log(err.status); // 404
console.log(err.type); // "not_found"
console.log(err.code); // "resource_not_found"
console.log(err.detail); // "Flow nonexistent not found"
console.log(err.requestId); // "req_abc123"
console.log(err.param); // undefined (set for validation errors)
}
}Error types
| Type | Description |
|------|-------------|
| invalid_request | Malformed request, missing required fields, invalid parameters |
| authentication_error | Invalid or missing API key |
| not_found | Resource doesn't exist or isn't accessible |
| rate_limit | Too many requests — back off and retry |
| api_error | Internal server error |
Rate Limiting
The API enforces rate limits per API key (default: 100 requests/minute). Every response includes rate limit headers:
| Header | Description |
|--------|-------------|
| X-RateLimit-Limit | Max requests per window |
| X-RateLimit-Remaining | Requests remaining in current window |
| X-RateLimit-Reset | Unix timestamp when the window resets |
The SDK handles rate limiting automatically — when a 429 response is received, it waits for the Retry-After duration and retries the request (up to maxRetries).
// Access rate limit info from any response
const response = await saltfish.flows.list();
console.log(response._rateLimit);
// { limit: 100, remaining: 97, reset: 1741564800 }Retries
The SDK automatically retries on:
- 5xx errors — server errors
- 429 errors — rate limit exceeded (waits for
Retry-After) - Network errors — connection failures, timeouts
Retries use exponential backoff (500ms → 1s → 2s, capped at 5s). Client errors (4xx, except 429) are never retried.
const saltfish = new SaltfishClient({
apiKey: 'sk_live_...',
maxRetries: 3, // Default: 2
timeout: 60000, // Default: 30000ms
});Response Format
Single object
{
"object": "flow",
"requestId": "req_abc123",
"data": {
"id": "abc123",
"title": "Onboarding Flow",
"status": "live",
"nodesCount": 5,
"deployedAt": "2026-03-01T12:00:00Z",
"createdAt": "2026-02-15T10:00:00Z",
"updatedAt": "2026-03-01T12:00:00Z"
}
}List
{
"object": "list",
"requestId": "req_def456",
"data": [
{ "id": "abc123", "title": "Onboarding Flow", "status": "live", "..." : "..." },
{ "id": "def456", "title": "Product Tour", "status": "draft", "..." : "..." }
],
"hasMore": true,
"totalCount": 42
}Error
{
"error": {
"type": "not_found",
"message": "Flow xyz not found",
"code": "resource_not_found"
},
"requestId": "req_ghi789"
}Idempotency
Mutation endpoints accept an Idempotency-Key header to safely retry requests without duplicate side effects:
// SDK — pass idempotency key to create operations
const flow = await saltfish.flows.create(
{ title: 'My Flow' },
'unique-key-12345' // idempotency key
);# cURL — include the header directly
curl -X POST https://studio-api.saltfish.ai/studio/api/v1/flows \
-H "X-API-Key: sk_live_abc123" \
-H "Idempotency-Key: unique-key-12345" \
-H "Content-Type: application/json" \
-d '{"title": "My Flow"}'API Documentation
Interactive API docs (Swagger UI) are available at:
https://studio-api.saltfish.ai/studio/api/v1/docsLicense
MIT
