Official TypeScript SDK for the Saturation API. Build powerful integrations with your workspace data including projects, budgets, actuals, contacts, purchase orders, and attachments.
- 🚀 Full TypeScript Support - Auto-generated types from OpenAPI specification
- 📦 Universal JavaScript - Works in Node.js, browsers, and React Native
- 🌐 Browser Compatible - CORS-friendly with native fetch API support
- 🔄 Real-time Collaboration - Changes appear instantly in the web app
- 🎯 Type-Safe - Complete type coverage for all API endpoints
- 🛠️ Developer Friendly - Intuitive API with comprehensive documentation
- ⚡ Lightweight - Minimal dependencies, optimized bundle size
npm install @saturation-api/js
# or
yarn add @saturation-api/js
# or
pnpm add @saturation-api/js
import { Saturation, type Project, type Budget } from '@saturation-api/js';
// Initialize the client
const client = new Saturation({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.saturation.io/api/v1', // Optional, this is the default
});
// List all projects
const { projects } = await client.listProjects({ status: 'active' });
console.log('Active projects:', projects);
// Get a specific project with its budget
const budget: Budget = await client.getProjectBudget('nike-swoosh-commercial', {
expands: ['phases', 'fringes', 'lines.contact', 'lines.phaseData'],
});
console.log('Project budget:', budget);
Get your API key from the Saturation web app:
- Go to Settings → API Keys
- Create a new API key
- Copy the key and use it in your application
const client = new Saturation({
apiKey: process.env.SATURATION_API_KEY!,
});
All types are auto-generated from the OpenAPI specification and available for import:
import {
Saturation,
type Project,
type Budget,
type BudgetLine,
type Actual,
type PurchaseOrder,
type Contact,
type CreateProjectInput,
type UpdateProjectInput,
type ListProjectsData,
// ... and many more
} from '@saturation-api/js';
// Use types for better type safety
function processProject(project: Project): void {
console.log(`Processing ${project.name}`);
}
// Use query parameter types
const params: ListProjectsData['query'] = {
status: 'active',
labels: ['nike'],
};
const { projects } = await client.listProjects(params);
Projects are the main organizational unit in Saturation. Each project contains budgets, actuals, purchase orders, and other related data.
// Create a new project
const project = await client.createProject({
name: 'Nike Holiday Commercial',
icon: '🎬',
spaceId: 'commercial-productions',
status: 'active',
labels: ['nike', 'q4-2024', 'commercial'],
});
// Update project details
const updated = await client.updateProject(project.id, {
status: 'archived',
labels: ['completed', 'q4-2024'],
});
// List projects with filters
const { projects } = await client.listProjects({
status: 'active',
labels: ['nike', 'commercial'],
spaceId: 'commercial-productions',
});
Work with multi-phase budgets, including line items, accounts, subtotals, and markups.
// Get complete budget with all details
const budget = await client.getProjectBudget('my-project', {
expands: ['phases', 'fringes', 'globals', 'lines.contact'],
idMode: 'user', // Use human-readable IDs
});
// Add budget lines
const updatedBudget = await client.createBudgetLines('my-project', {
accountId: 'root',
lines: [
{
type: 'line',
accountId: '1100',
description: 'Director',
phaseData: {
estimate: { amount: 50000 },
actual: { amount: 48000 },
},
},
{
type: 'account',
accountId: '2000',
description: 'Camera Department',
},
],
});
// Update a specific budget line
const line = await client.updateBudgetLine('my-project', '1100-DIRECTOR', {
description: 'Director - Extended Cut',
tags: ['above-the-line', 'key-personnel'],
});
Manage different budget phases like estimate, revised, and actual.
// Create a new phase
const phase = await client.createBudgetPhase('my-project', {
name: 'revised',
label: 'Revised Budget',
color: '#FFA500',
order: 2,
});
// List all phases
const { phases } = await client.listBudgetPhases('my-project');
// Update phase details
await client.updateBudgetPhase('my-project', phase.id, {
label: 'Revised Budget v2',
});
Track actual expenses and sync with your accounting system.
// Create an actual
const actual = await client.createActual('my-project', {
lineItemId: '2150-CAMERA',
amount: 35000,
date: '2024-03-20',
description: 'RED Camera Package Rental',
contactId: 'vendor-123',
tags: ['equipment', 'week-2'],
});
// List actuals with filters
const { actuals } = await client.listProjectActuals('my-project', {
dateFrom: '2024-03-01',
dateTo: '2024-03-31',
lineItemId: ['2150-CAMERA', '2160-LENSES'],
expands: ['contact', 'attachments'],
});
// Upload attachment to actual
const attachment = await client.uploadActualAttachment(
'my-project',
actual.id,
fileBuffer,
'invoice-12345.pdf'
);
Manage purchase orders with line-item level detail.
// Create a purchase order
const po = await client.createPurchaseOrder('my-project', {
number: 'PO-001',
contactId: 'vendor-456',
date: '2024-03-15',
items: [
{
lineItemId: '2150-CAMERA',
amount: 35000,
description: 'Camera equipment rental - 5 days',
},
{
lineItemId: '2160-LENSES',
amount: 8000,
description: 'Lens kit rental',
},
],
tags: ['equipment', 'approved'],
});
// List purchase orders
const { purchaseOrders } = await client.listPurchaseOrders('my-project', {
contactId: 'vendor-456',
hasAttachments: true,
expands: ['items.lineItem', 'contact'],
});
Manage vendors, crew members, and other contacts.
// Create a contact
const contact = await client.createContact({
name: 'John Smith',
email: 'john@example.com',
phone: '+1234567890',
type: 'individual',
companyName: 'Smith Productions',
address: '123 Main St, Los Angeles, CA 90001',
});
// Search contacts
const { contacts } = await client.listContacts({
name: 'Smith',
type: 'individual',
companyName: 'Productions',
});
// Upload tax documents
const taxDoc = await client.uploadContactTaxDocument(
contact.id,
w9Buffer,
'W9-2024.pdf'
);
Define custom rates for line items and contacts.
// Create a rate
const rate = await client.createWorkspaceRate({
lineItemId: '1100-DIRECTOR',
contactId: 'contact-789',
rate: 1500,
unit: 'day',
currency: 'USD',
tags: ['union', 'tier-1'],
});
// List rates with filters
const { rates } = await client.listWorkspaceRates({
lineItemId: '1100-DIRECTOR',
tags: ['union'],
});
Upload and manage file attachments.
// Upload a file
const upload = await client.uploadFile(fileBuffer, 'document.pdf');
// Download a file
const fileContent = await client.downloadFile(upload.id);
// Delete a file
await client.deleteFile(upload.id);
The SDK works seamlessly in React applications. Here are some common patterns:
import { useState, useEffect } from 'react';
import { Saturation, Project } from '@saturation-api/js';
const client = new Saturation({
apiKey: process.env.REACT_APP_SATURATION_API_KEY!,
});
function useProjects() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
client.listProjects({ status: 'active' })
.then(({ projects }) => setProjects(projects))
.catch(setError)
.finally(() => setLoading(false));
}, []);
return { projects, loading, error };
}
// Usage in component
function ProjectList() {
const { projects, loading, error } = useProjects();
if (loading) return <div>Loading projects...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{projects.map(project => (
<li key={project.id}>
{project.icon} {project.name}
</li>
))}
</ul>
);
}
import React, { useState, useEffect } from 'react';
import { Saturation, Budget } from '@saturation-api/js';
interface BudgetDashboardProps {
projectId: string;
apiKey: string;
}
export function BudgetDashboard({ projectId, apiKey }: BudgetDashboardProps) {
const [budget, setBudget] = useState<Budget | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const client = new Saturation({ apiKey });
client.getProjectBudget(projectId, {
expands: ['phases', 'fringes', 'lines.contact'],
})
.then(setBudget)
.catch(console.error)
.finally(() => setLoading(false));
}, [projectId, apiKey]);
if (loading) return <div>Loading budget...</div>;
if (!budget) return <div>No budget found</div>;
return (
<div className="budget-dashboard">
<h2>Budget Overview</h2>
<div className="budget-totals">
<div>Total: ${budget.account.totals.estimate || 0}</div>
<div>Actual: ${budget.account.totals.actual || 0}</div>
</div>
<h3>Budget Lines</h3>
<table>
<thead>
<tr>
<th>Account</th>
<th>Description</th>
<th>Estimate</th>
<th>Actual</th>
</tr>
</thead>
<tbody>
{budget.account.lines.map(line => (
<tr key={line.id}>
<td>{line.accountId}</td>
<td>{line.description}</td>
<td>${line.totals.estimate || 0}</td>
<td>${line.totals.actual || 0}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
import React, { useState } from 'react';
import { Saturation, CreateActualInput } from '@saturation-api/js';
interface ActualFormProps {
projectId: string;
client: Saturation;
onSuccess: () => void;
}
export function CreateActualForm({ projectId, client, onSuccess }: ActualFormProps) {
const [formData, setFormData] = useState<CreateActualInput>({
lineItemId: '',
amount: 0,
date: new Date().toISOString().split('T')[0],
description: '',
});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
try {
await client.createActual(projectId, formData);
onSuccess();
// Reset form
setFormData({
lineItemId: '',
amount: 0,
date: new Date().toISOString().split('T')[0],
description: '',
});
} catch (error) {
console.error('Failed to create actual:', error);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Line Item ID (e.g., 2150-CAMERA)"
value={formData.lineItemId}
onChange={(e) => setFormData({ ...formData, lineItemId: e.target.value })}
required
/>
<input
type="number"
placeholder="Amount"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: Number(e.target.value) })}
required
/>
<input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
required
/>
<textarea
placeholder="Description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Creating...' : 'Create Actual'}
</button>
</form>
);
}
import React, { createContext, useContext, ReactNode } from 'react';
import { Saturation } from '@saturation-api/js';
const SaturationContext = createContext<Saturation | null>(null);
interface SaturationProviderProps {
apiKey: string;
baseURL?: string;
children: ReactNode;
}
export function SaturationProvider({ apiKey, baseURL, children }: SaturationProviderProps) {
const client = React.useMemo(
() => new Saturation({ apiKey, baseURL }),
[apiKey, baseURL]
);
return (
<SaturationContext.Provider value={client}>
{children}
</SaturationContext.Provider>
);
}
export function useSaturation() {
const client = useContext(SaturationContext);
if (!client) {
throw new Error('useSaturation must be used within a SaturationProvider');
}
return client;
}
// Usage in your app
function App() {
return (
<SaturationProvider apiKey={process.env.REACT_APP_SATURATION_API_KEY!}>
<YourComponents />
</SaturationProvider>
);
}
import React, { useState } from 'react';
import { useSaturation } from './SaturationProvider';
interface FileUploadProps {
projectId: string;
actualId: string;
onUploadComplete: () => void;
}
export function FileUpload({ projectId, actualId, onUploadComplete }: FileUploadProps) {
const client = useSaturation();
const [uploading, setUploading] = useState(false);
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
// Convert File to Blob for the SDK
const blob = new Blob([await file.arrayBuffer()], { type: file.type });
await client.uploadActualAttachment(
projectId,
actualId,
blob,
file.name
);
onUploadComplete();
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={handleFileSelect}
disabled={uploading}
accept=".pdf,.jpg,.jpeg,.png"
/>
{uploading && <span>Uploading...</span>}
</div>
);
}
For server-side usage in Next.js:
// pages/api/projects.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Saturation } from '@saturation-api/js';
const client = new Saturation({
apiKey: process.env.SATURATION_API_KEY!,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method === 'GET') {
const { projects } = await client.listProjects({ status: 'active' });
res.status(200).json(projects);
} else if (req.method === 'POST') {
const project = await client.createProject(req.body);
res.status(201).json(project);
} else {
res.status(405).json({ error: 'Method not allowed' });
}
} catch (error) {
console.error('API error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
The SDK provides detailed error information with proper TypeScript types.
try {
const project = await client.getProject('non-existent');
} catch (error) {
console.error('API Error:', error);
// The generated client handles errors internally
// Check error.response for details if available
}
Many endpoints support the expands
parameter to include related data in a single request.
// Get budget with all related data
const budget = await client.getProjectBudget('my-project', {
expands: [
'phases', // Include all budget phases
'fringes', // Include fringe calculations
'globals', // Include global calculations
'lines.contact', // Include contact info for each line
'lines.phaseData', // Include phase-specific data
],
});
// Get actuals with full details
const { actuals } = await client.listProjectActuals('my-project', {
expands: [
'contact', // Include contact information
'attachments', // Include file attachments
'lineItem', // Include budget line details
],
});
The API supports using your existing account codes as identifiers.
// Use your account codes directly
const line = await client.getBudgetLine('my-project', '1100-LABOR');
const updated = await client.updateBudgetLine('my-project', '2150-CAMERA', {
description: 'Camera Equipment - Updated',
});
// Create actuals with your codes
await client.createActual('my-project', {
lineItemId: '2150-CAMERA',
amount: 5000,
date: '2024-03-20',
});
Handle large datasets with pagination parameters.
// Paginate through public rates
const { rates } = await client.listPublicRates({
search: 'camera',
page: 1,
limit: 50,
});
Create multiple items efficiently in a single request.
// Create multiple budget lines at once
await client.createBudgetLines('my-project', {
accountId: 'root',
lines: [
{ type: 'account', accountId: '1000', description: 'Above The Line' },
{ type: 'line', accountId: '1100', description: 'Producer' },
{ type: 'line', accountId: '1200', description: 'Director' },
{ type: 'subtotal', description: 'Total ATL' },
{ type: 'account', accountId: '2000', description: 'Below The Line' },
{ type: 'line', accountId: '2100', description: 'Camera' },
{ type: 'line', accountId: '2200', description: 'Lighting' },
],
});
The SDK is fully typed with TypeScript. All request and response types are auto-generated from the OpenAPI specification.
import type {
Project,
Budget,
Actual,
PurchaseOrder,
Contact,
CreateProjectInput,
UpdateProjectInput,
ListProjectsParams,
} from '@saturation-api/js';
// Type-safe function
async function getProjectBudgetTotal(projectId: string): Promise<number> {
const budget = await client.getProjectBudget(projectId);
return budget.account.totals.estimate || 0;
}
// Type-safe error handling
function isNotFoundError(error: unknown): boolean {
return error instanceof SaturationError && error.statusCode === 404;
}
For complete API documentation, see the API Reference.
listProjects(params?)
- List all projectsgetProject(projectId)
- Get a specific projectcreateProject(data)
- Create a new projectupdateProject(projectId, data)
- Update project detailsdeleteProject(projectId)
- Delete a project
getProjectBudget(projectId, params?)
- Get project budgetcreateBudgetLines(projectId, data)
- Add budget linesgetBudgetLine(projectId, lineId, params?)
- Get specific lineupdateBudgetLine(projectId, lineId, data)
- Update budget linedeleteBudgetLine(projectId, lineId)
- Delete budget line
listBudgetPhases(projectId)
- List budget phasescreateBudgetPhase(projectId, data)
- Create phasegetBudgetPhase(projectId, phaseId)
- Get phase detailsupdateBudgetPhase(projectId, phaseId, data)
- Update phasedeleteBudgetPhase(projectId, phaseId)
- Delete phase
listProjectActuals(projectId, params?)
- List actualsgetActual(projectId, actualId, params?)
- Get actual detailscreateActual(projectId, data)
- Create actualupdateActual(projectId, actualId, data)
- Update actualdeleteActual(projectId, actualId)
- Delete actualuploadActualAttachment(projectId, actualId, file, filename)
- Add attachment
listPurchaseOrders(projectId, params?)
- List purchase ordersgetPurchaseOrder(projectId, poId, params?)
- Get PO detailscreatePurchaseOrder(projectId, data)
- Create purchase orderupdatePurchaseOrder(projectId, poId, data)
- Update POdeletePurchaseOrder(projectId, poId)
- Delete POuploadPurchaseOrderAttachment(projectId, poId, file, filename)
- Add attachment
listContacts(params?)
- List contactscreateContact(data)
- Create contactgetContact(contactId)
- Get contact detailsupdateContact(contactId, data)
- Update contactuploadContactTaxDocument(contactId, file, filename)
- Upload tax docuploadContactAttachment(contactId, file, filename)
- Add attachment
# Clone the repository
git clone https://github.com/saturation-api/saturation-js.git
cd saturation-js
# Install dependencies
npm install
# Generate types from OpenAPI spec
npm run generate
# Run tests
npm test
# Build the package
npm run build
npm run build
- Build for productionnpm run dev
- Watch mode for developmentnpm test
- Run testsnpm run lint
- Lint codenpm run format
- Format code with Prettiernpm run generate
- Generate types from OpenAPI spec
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
MIT - see LICENSE file for details.