# Stack0
> AI-native infrastructure platform for modern applications. Production-ready email, CDN, integrations, screenshots, AI extraction, crawl, map, and document parsing. Open Firecrawl alternative.
## What is Stack0?
Stack0 provides essential infrastructure services for developers building web applications:
- **Email Service**: Send transactional and marketing emails with templates, tracking, and analytics
- **CDN**: Asset storage and delivery with on-the-fly image transformations
- **Integrations**: Unified API to connect your users' third-party apps (CRM, storage, communication, productivity)
- **Webdata**: Screenshots, AI extraction, recursive **crawl**, URL **map**, PDF/DOCX **documents** parsing, **actions** (click/type/scroll), **stealth** mode
- **Workflows**: Multi-step AI pipelines with sequential/parallel execution and variable passing
- **Domain Management**: Custom domain verification and DNS management
All services are usage-based with no monthly minimums. Get started with $5 in free credits.
## Quick Start
### 1. Create an Account
Sign up at https://app.stack0.dev and create your first organization.
### 2. Get Your API Key
Navigate to Settings → API Keys in your dashboard and generate a new API key.
### 3. Install the SDK
```bash
npm install @stack0/sdk
# or
bun add @stack0/sdk
```
### 4. Install Elements (Optional)
Pre-built React components for file uploads, avatars, and image galleries:
```bash
# Install the package
npm install @stack0/elements
# Or with shadcn CLI (recommended)
npx shadcn@latest add https://www.stack0.dev/r/file-upload
npx shadcn@latest add https://www.stack0.dev/r/avatar-upload
npx shadcn@latest add https://www.stack0.dev/r/logo-upload
npx shadcn@latest add https://www.stack0.dev/r/image-gallery
npx shadcn@latest add https://www.stack0.dev/r/image
```
### 5. Send Your First Email
```typescript
import { Stack0 } from '@stack0/sdk';
const stack0 = new Stack0({
apiKey: process.env.STACK0_API_KEY,
});
await stack0.mail.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome to our app!',
html: '
Thanks for signing up!
',
});
```
### 6. Upload Your First Asset
```typescript
const asset = await stack0.cdn.upload({
projectSlug: 'my-project',
file: imageBuffer,
filename: 'hero.jpg',
mimeType: 'image/jpeg',
});
console.log(asset.cdnUrl); // https://cdn.stack0.dev/...
```
### 7. Capture Your First Screenshot
```typescript
const screenshot = await stack0.screenshots.captureAndWait({
url: 'https://example.com',
format: 'png',
fullPage: true,
blockAds: true,
});
console.log(screenshot.imageUrl); // https://cdn.stack0.dev/screenshots/...
```
### 8. Extract Data with AI
```typescript
const extraction = await stack0.extraction.extractAndWait({
url: 'https://example.com/product',
mode: 'schema',
schema: {
type: 'object',
properties: {
title: { type: 'string' },
price: { type: 'number' },
},
},
});
console.log(extraction.extractedData); // { title: '...', price: 29.99 }
```
### 9. Connect Your Users' Apps (Integrations)
```typescript
// List contacts from any connected CRM (HubSpot, Salesforce, Pipedrive)
const contacts = await stack0.integrations.crm.listContacts({
connectionId: 'conn_abc123', // User's connection ID
});
// Upload a file to user's Google Drive, Dropbox, or OneDrive
await stack0.integrations.storage.uploadFile({
connectionId: 'conn_def456',
name: 'report.pdf',
content: fileBuffer,
folderId: 'folder_123',
});
// Send a message to user's Slack channel
await stack0.integrations.communication.sendMessage({
connectionId: 'conn_ghi789',
channelId: 'C1234567890',
text: 'Hello from your app!',
});
```
### 10. Run an AI Workflow
```typescript
// Run a multi-step AI pipeline
const result = await stack0.workflows.runAndWait({
projectSlug: 'my-project',
workflowId: 'content-pipeline',
variables: {
topic: 'AI in Healthcare',
},
});
console.log(result.output); // Generated content from all steps
```
## Elements - React Components
`@stack0/elements` provides pre-built React components for common file handling patterns when working with Stack0 CDN.
### Installation
```bash
# Using npm
npm install @stack0/elements
# Using pnpm
pnpm add @stack0/elements
# Using bun
bun add @stack0/elements
# Using shadcn CLI (recommended - copies components to your project)
npx shadcn@latest add https://www.stack0.dev/r/file-upload
npx shadcn@latest add https://www.stack0.dev/r/avatar-upload
npx shadcn@latest add https://www.stack0.dev/r/logo-upload
npx shadcn@latest add https://www.stack0.dev/r/image-gallery
npx shadcn@latest add https://www.stack0.dev/r/image
```
Required peer dependencies:
```bash
npm install class-variance-authority clsx tailwind-merge lucide-react
```
### Available Components
- **FileUpload**: Drag-and-drop file upload with progress tracking and multi-file support
- **AvatarUpload**: Circular avatar upload with preview, fallback initials, and remove functionality
- **LogoUpload**: Rectangular logo upload with multiple aspect ratios and SVG support
- **ImageGallery**: Responsive image grid with lightbox, selection, and action support
- **Image**: next/image wrapper optimized for Stack0 CDN with built-in transforms
### Upload Handler Pattern
Elements use a handler pattern that works with any backend. Your server handles authentication and returns presigned URLs:
```typescript
// Client-side usage
import { FileUpload, createStack0Handler } from '@stack0/elements'
const handler = createStack0Handler({
endpoint: '/api/upload', // Your API endpoint
})
```
```typescript
// Server-side API route (Next.js App Router)
// app/api/upload/route.ts
import { stack0 } from '@stack0/sdk'
export async function POST(request: Request) {
const { filename, mimeType, size, folder } = await request.json()
const result = await stack0.cdn.createUpload({
filename,
mimeType,
size,
folder,
public: true,
})
return Response.json({
uploadUrl: result.uploadUrl,
assetId: result.assetId,
})
}
```
### Custom Upload Handler
Create your own handler for any backend:
```typescript
import { FileUpload, type UploadHandler } from '@stack0/elements'
const customHandler: UploadHandler = {
getUploadUrl: async (file) => {
const response = await fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({
filename: file.name,
mimeType: file.type,
size: file.size,
}),
})
const data = await response.json()
return {
uploadUrl: data.uploadUrl,
assetId: data.assetId,
}
},
onUploadComplete: async (assetId, file) => {
await fetch(`/api/upload/${assetId}/confirm`, { method: 'POST' })
return { url: `https://cdn.example.com/${assetId}` }
},
onError: (error, file) => {
console.error(`Upload failed for ${file.name}:`, error)
},
}
```
### Component Examples
#### FileUpload
```tsx
import { FileUpload, createStack0Handler } from '@stack0/elements'
const handler = createStack0Handler({ endpoint: '/api/upload' })
console.log(files)}
/>
```
#### AvatarUpload
```tsx
import { AvatarUpload, createStack0Handler } from '@stack0/elements'
const handler = createStack0Handler({ endpoint: '/api/upload', folder: '/avatars' })
updateUser({ avatarUrl: url })}
fallback={user.initials}
size="lg"
/>
```
#### Image (CDN-optimized)
```tsx
import { Image } from '@stack0/elements'
```
## CDN API Reference
The CDN API enables you to upload, manage, and transform assets. All CDN operations are project-scoped using `projectSlug`.
### Upload File
Upload files directly with the high-level `upload` method:
```typescript
const asset = await stack0.cdn.upload({
projectSlug: 'my-project',
file: fileBuffer, // Blob, Buffer, or ArrayBuffer
filename: 'photo.jpg',
mimeType: 'image/jpeg',
folder: '/images/avatars', // Optional folder path
metadata: { userId: 'user_123' }, // Optional custom metadata
});
// Response
{
id: 'asset_abc123',
filename: 'photo.jpg',
originalFilename: 'photo.jpg',
mimeType: 'image/jpeg',
size: 102400,
type: 'image',
cdnUrl: 'https://cdn.stack0.dev/...',
width: 1920,
height: 1080,
status: 'ready',
folder: '/images/avatars',
createdAt: Date
}
```
### Upload with Watermark
Automatically apply a watermark to images during upload:
```typescript
const asset = await stack0.cdn.upload({
projectSlug: 'my-project',
file: photoBuffer,
filename: 'branded-photo.jpg',
mimeType: 'image/jpeg',
watermark: {
assetId: 'logo-asset-id', // Reference another CDN asset
// Or use url: 'https://example.com/logo.png' for external images
position: 'bottom-right', // 9 position options
opacity: 50, // 0-100
sizingMode: 'relative', // 'relative' or 'absolute'
width: 15, // 15% of image width (relative mode)
offsetX: 20, // 20px from edge
offsetY: 20,
},
});
```
#### Watermark Options
- `assetId`: Reference to another CDN asset (your logo, etc.)
- `url`: Direct URL to watermark image (alternative to assetId)
- `position`: Placement on image
- `top-left`, `top-center`, `top-right`
- `center-left`, `center`, `center-right`
- `bottom-left`, `bottom-center`, `bottom-right`
- `offsetX`, `offsetY`: Pixel offset from position (default: 0)
- `sizingMode`: How to interpret width/height
- `absolute`: Width/height in exact pixels
- `relative`: Width/height as percentage of main image (1-100)
- `width`, `height`: Size of watermark (based on sizingMode)
- `opacity`: Transparency 0-100 (default: 100 = fully opaque)
- `rotation`: Rotation angle -360 to 360 degrees (default: 0)
- `tile`: Repeat watermark across image (default: false)
- `tileSpacing`: Spacing between tiles in pixels (default: 100)
- `borderRadius`: Corner radius in pixels (default: 0)
#### Manual Upload Flow
For more control, use the presigned URL flow:
```typescript
// 1. Get presigned upload URL
const { uploadUrl, assetId, expiresAt } = await stack0.cdn.getUploadUrl({
projectSlug: 'my-project',
filename: 'document.pdf',
mimeType: 'application/pdf',
size: 1048576, // File size in bytes
folder: '/documents',
});
// 2. Upload directly to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': 'application/pdf' },
});
// 3. Confirm upload completed
const asset = await stack0.cdn.confirmUpload(assetId);
```
### Upload from URL (Server-Side)
Upload a file by providing a source URL. The server fetches and stores the file directly, so the client never downloads the bytes. Ideal for serverless environments with tight memory limits.
```typescript
const asset = await stack0.cdn.uploadFromUrl({
projectSlug: 'my-project',
sourceUrl: 'https://replicate.delivery/output/video.mp4',
filename: 'video.mp4',
mimeType: 'video/mp4',
folder: '/ai-generated',
metadata: { source: 'replicate' },
});
console.log(asset.cdnUrl); // https://cdn.stack0.dev/...
```
### List Assets
Query assets with filters and pagination:
```typescript
const { assets, total, hasMore } = await stack0.cdn.list({
projectSlug: 'my-project',
type: 'image', // 'image' | 'video' | 'audio' | 'document' | 'other'
status: 'ready', // 'pending' | 'processing' | 'ready' | 'failed'
folder: '/images',
search: 'avatar', // Search in filename
tags: ['profile', 'user'],
sortBy: 'createdAt', // 'createdAt' | 'filename' | 'size' | 'type'
sortOrder: 'desc', // 'asc' | 'desc'
limit: 20,
offset: 0,
});
```
### Get Asset
Retrieve a single asset by ID:
```typescript
const asset = await stack0.cdn.get('asset_abc123');
console.log(asset.filename);
console.log(asset.cdnUrl);
console.log(asset.width, asset.height); // For images/videos
console.log(asset.duration); // For videos/audio (seconds)
```
### Update Asset
Update asset metadata:
```typescript
const asset = await stack0.cdn.update({
id: 'asset_abc123',
filename: 'renamed-photo.jpg',
folder: '/images/archived',
tags: ['nature', 'sunset'],
alt: 'A beautiful sunset over the ocean',
metadata: { category: 'landscape' },
});
```
### Delete Assets
```typescript
// Delete single asset
await stack0.cdn.delete('asset_abc123');
// Delete multiple assets
const { deletedCount } = await stack0.cdn.deleteMany([
'asset_abc123',
'asset_def456',
'asset_ghi789',
]);
```
### Move Assets
Move assets to a different folder:
```typescript
await stack0.cdn.move({
assetIds: ['asset_abc123', 'asset_def456'],
folder: '/images/archive', // Use null for root folder
});
```
### Image Transformations
Generate optimized and transformed image URLs client-side (no API call required):
```typescript
// Using asset's cdnUrl directly (recommended)
const url = stack0.cdn.getTransformUrl(asset.cdnUrl, {
width: 800,
height: 600,
fit: 'cover', // 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
format: 'webp', // 'webp' | 'jpeg' | 'png' | 'avif' | 'auto'
quality: 80, // 1-100
});
// Or using s3Key when cdnUrl is configured in Stack0 options
const stack0 = new Stack0({
apiKey: process.env.STACK0_API_KEY,
cdnUrl: 'https://cdn.yourproject.stack0.dev',
});
const url = stack0.cdn.getTransformUrl(asset.s3Key, { width: 400 });
// Use transformed URL
//
```
#### Transformation Options
- `width`: Target width in pixels (snapped to optimal sizes for caching)
- `height`: Target height in pixels
- `fit`: How to fit the image
- `cover`: Crop to fill dimensions (default)
- `contain`: Fit within dimensions, preserving aspect ratio
- `fill`: Stretch to fill dimensions
- `inside`: Fit inside dimensions
- `outside`: Cover dimensions while preserving aspect ratio
- `format`: Output format (webp recommended for web, 'auto' for browser detection)
- `quality`: Compression quality (1-100)
- `crop`: Smart crop position ('attention', 'entropy', 'center', etc.)
- `blur`: Blur sigma (0.3-100)
- `sharpen`: Sharpen sigma
- `brightness`: Brightness adjustment (-100 to 100)
- `saturation`: Saturation adjustment (-100 to 100)
- `grayscale`: Convert to grayscale (boolean)
- `rotate`: Rotation angle (0, 90, 180, 270)
- `flip`: Flip vertically (boolean)
- `flop`: Flip horizontally (boolean)
### Folders
Organize assets with virtual folders:
```typescript
// Get folder tree
const tree = await stack0.cdn.getFolderTree({
projectSlug: 'my-project',
maxDepth: 3,
});
// Tree structure
[
{
id: 'folder_123',
name: 'images',
path: '/images',
assetCount: 42,
children: [
{
id: 'folder_456',
name: 'avatars',
path: '/images/avatars',
assetCount: 15,
children: []
}
]
}
]
// Create folder
const folder = await stack0.cdn.createFolder({
projectSlug: 'my-project',
name: 'avatars',
parentId: 'folder_123', // Optional, null for root
});
// Delete folder
await stack0.cdn.deleteFolder('folder_123');
await stack0.cdn.deleteFolder('folder_123', true); // Delete with contents
```
### Asset Types
Assets are automatically categorized by MIME type:
- `image`: jpeg, png, gif, webp, svg, etc.
- `video`: mp4, webm, mov, avi, etc.
- `audio`: mp3, wav, ogg, etc.
- `document`: pdf, doc, docx, txt, etc.
- `other`: All other file types
## Video Streaming API Reference
The Video Streaming API enables you to transcode videos into HLS adaptive streaming and MP4 formats for optimal playback across devices.
### Transcode Video
Start a transcoding job to convert video to HLS or MP4:
```typescript
const job = await stack0.cdn.transcode({
projectSlug: 'my-project',
assetId: 'video-asset-id',
outputFormat: 'hls', // 'hls' for adaptive streaming, 'mp4' for progressive download
variants: [
{ quality: '720p', codec: 'h264' },
{ quality: '1080p', codec: 'h264' },
],
webhookUrl: 'https://your-app.com/webhook', // Optional: get notified when complete
});
// Response
{
id: 'job_abc123',
assetId: 'asset_xyz',
status: 'pending',
outputFormat: 'hls',
variants: [{ quality: '720p', codec: 'h264' }, { quality: '1080p', codec: 'h264' }],
progress: 0,
createdAt: Date
}
```
#### Transcode Options
- `assetId` (required): ID of the video asset to transcode
- `outputFormat` (required): `hls` for adaptive streaming or `mp4` for progressive download
- `variants` (required): Array of quality variants (1-10)
- `quality`: `360p`, `480p`, `720p`, `1080p`, `1440p`, `2160p`
- `codec`: `h264` (default) or `h265`
- `bitrate`: Custom bitrate in kbps (optional)
- `maxFramerate`: Maximum frame rate (optional)
- `watermark`: Add watermark to video (optional)
- `type`: `image` or `text`
- `imageAssetId`: Asset ID for image watermark
- `text`: Text for text watermark
- `position`: `top-left`, `top-right`, `bottom-left`, `bottom-right`, `center`
- `opacity`: 0-100 (default 50)
- `trim`: Trim video (optional)
- `start`: Start time in seconds
- `end`: End time in seconds
- `webhookUrl`: URL to receive completion notification
### Get Transcoding Job Status
```typescript
const job = await stack0.cdn.getJob('job_abc123');
console.log(`Status: ${job.status}`);
console.log(`Progress: ${job.progress}%`);
// Status values: 'pending' | 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'
```
### List Transcoding Jobs
```typescript
const { jobs, total, hasMore } = await stack0.cdn.listJobs({
projectSlug: 'my-project',
assetId: 'video-asset-id', // Optional: filter by asset
status: 'processing', // Optional: filter by status
limit: 20,
offset: 0,
});
```
### Cancel Transcoding Job
```typescript
await stack0.cdn.cancelJob('job_abc123');
```
### Get Streaming URLs
After transcoding completes, get URLs for playback:
```typescript
const urls = await stack0.cdn.getStreamingUrls('asset-id');
// HLS adaptive streaming (recommended for web/mobile)
console.log(`HLS Master Playlist: ${urls.hlsUrl}`);
// MP4 direct download URLs by quality
for (const mp4 of urls.mp4Urls) {
console.log(`${mp4.quality}: ${mp4.url}`);
}
// Generated thumbnails
for (const thumb of urls.thumbnails) {
console.log(`Thumbnail at ${thumb.timestamp}s: ${thumb.url}`);
}
```
### Generate Thumbnails
Extract thumbnail images from video at specific timestamps:
```typescript
const thumbnail = await stack0.cdn.getThumbnail({
assetId: 'video-asset-id',
timestamp: 10.5, // 10.5 seconds into the video
width: 320, // Optional: resize width
format: 'webp', // 'jpg', 'png', 'webp'
});
console.log(`Thumbnail URL: ${thumbnail.url}`);
```
### Extract Audio
Extract audio track from video as MP3, AAC, or WAV:
```typescript
const { jobId, status } = await stack0.cdn.extractAudio({
projectSlug: 'my-project',
assetId: 'video-asset-id',
format: 'mp3', // 'mp3', 'aac', 'wav'
bitrate: 192, // Optional: kbps
});
```
### Generate GIF
Create an animated GIF from a video segment:
```typescript
const gif = await stack0.cdn.generateGif({
projectSlug: 'my-project',
assetId: 'video-asset-id',
startTime: 5, // Start at 5 seconds into the video
duration: 3, // 3 second GIF (max 30 seconds)
width: 480, // Output width in pixels (100-800)
fps: 10, // Frames per second (5-30)
optimizePalette: true, // Two-pass palette optimization for smaller file size
});
// Response
{
id: 'gif_abc123',
assetId: 'asset_xyz',
startTime: 5,
duration: 3,
fps: 10,
url: null, // null until completed
status: 'pending',
createdAt: Date
}
```
#### GIF Generation Options
- `assetId` (required): ID of the video asset
- `startTime`: Start position in seconds (default: 0)
- `duration`: Duration in seconds, 0.5-30 (default: 5)
- `width`: Output width 100-800px (default: 480, height auto-calculated)
- `fps`: Frames per second 5-30 (default: 10, lower = smaller file)
- `optimizePalette`: Use two-pass palette generation for better quality and smaller file size (default: true)
#### Get GIF Status
```typescript
const gif = await stack0.cdn.getGif('gif_abc123');
if (gif?.status === 'completed') {
console.log(`GIF URL: ${gif.url}`);
console.log(`Size: ${gif.sizeBytes} bytes`);
console.log(`Frames: ${gif.frameCount}`);
}
// Status values: 'pending' | 'processing' | 'completed' | 'failed'
```
#### List GIFs for a Video
```typescript
const gifs = await stack0.cdn.listGifs({ assetId: 'video-asset-id' });
for (const gif of gifs) {
console.log(`${gif.startTime}s-${gif.startTime + gif.duration}s: ${gif.url}`);
}
```
#### GIF Best Practices
- **Duration**: Keep GIFs under 10 seconds for reasonable file sizes
- **Width**: 480px is a good balance of quality and file size
- **FPS**: 10 fps is sufficient for most use cases; use 15-20 for smoother motion
- **File Size**: A 5-second 480px GIF at 10fps is typically 1-5MB depending on content
### Video Player Integration
Use HLS.js for cross-browser HLS playback:
```typescript
import Hls from 'hls.js';
const urls = await stack0.cdn.getStreamingUrls('asset-id');
const video = document.getElementById('video') as HTMLVideoElement;
if (Hls.isSupported() && urls.hlsUrl) {
const hls = new Hls();
hls.loadSource(urls.hlsUrl);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS support
video.src = urls.hlsUrl!;
}
```
### Video Transcoding Status Flow
1. **pending**: Job created, waiting to be queued
2. **queued**: Job queued for processing
3. **processing**: Video being transcoded
4. **completed**: Transcoding finished, streaming URLs available
5. **failed**: Transcoding failed (check `errorMessage`)
6. **cancelled**: Job was cancelled
## Video Merge API Reference
The Video Merge API enables you to combine multiple videos and images into a single video with optional audio overlay. Perfect for creating slideshows, video compilations, or adding background music.
### Create Merge Job
Combine multiple assets (videos, images) into a single video:
```typescript
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'intro-video-id' }, // Full video
{ assetId: 'image-id', duration: 5 }, // Image for 5 seconds
{ assetId: 'main-video-id', startTime: 10, endTime: 60 }, // Trimmed video
],
audioTrack: {
assetId: 'background-music-id',
loop: true, // Loop if audio is shorter than video
fadeIn: 2, // 2 second fade in
fadeOut: 3, // 3 second fade out
},
output: {
format: 'mp4', // 'mp4' or 'webm'
quality: '1080p', // '360p', '480p', '720p', '1080p', '1440p', '2160p'
filename: 'final-video.mp4',
},
webhookUrl: 'https://your-app.com/webhook',
});
// Response
{
id: 'merge_abc123',
status: 'pending',
progress: 0,
outputFormat: 'mp4',
outputQuality: '1080p',
createdAt: Date
}
```
#### Merge Input Options
Each input item in the `inputs` array supports:
- `assetId` (required): UUID of the video or image asset
- `duration`: Display duration in seconds (required for images, max 3600)
- `startTime`: Trim start point in seconds (videos only)
- `endTime`: Trim end point in seconds (videos only)
- `textOverlay`: Text caption configuration (see below)
#### Text Overlay Options
Add text captions/overlays to images and videos, perfect for TikTok/Instagram style content:
- `text` (required): The text to display (1-500 characters)
- `position`: Vertical position - `top`, `center`, `bottom` (default: `bottom`)
- `fontSize`: Font size in pixels (12-200, default: 48)
- `fontFamily`: Font family (default: `Liberation Sans`)
- `fontWeight`: `normal` or `bold` (default: `bold`)
- `color`: Text color in hex format (default: `#FFFFFF`)
- `backgroundColor`: Background color (e.g., `rgba(0,0,0,0.5)`)
- `padding`: Padding from edges in pixels (0-100, default: 20)
- `maxWidth`: Maximum width as percentage of video width (10-100, default: 90)
- `shadow`: Shadow configuration
- `color`: Shadow color in hex (default: `#000000`)
- `offsetX`: Horizontal offset (0-20, default: 2)
- `offsetY`: Vertical offset (0-20, default: 2)
- `stroke`: Stroke/outline configuration
- `color`: Stroke color in hex (default: `#000000`)
- `width`: Stroke width in pixels (1-10, default: 2)
#### Audio Track Options
- `assetId` (required): UUID of the audio file
- `loop`: Loop audio if shorter than video (default: false)
- `fadeIn`: Fade in duration (0-10 seconds)
- `fadeOut`: Fade out duration (0-10 seconds)
#### Output Options
- `format`: Output format - `mp4` (default) or `webm`
- `quality`: Quality preset - `360p`, `480p`, `720p` (default), `1080p`, `1440p`, `2160p`
- `filename`: Custom filename for the output (max 255 chars)
### Get Merge Job Status
```typescript
const job = await stack0.cdn.getMergeJob('merge_abc123');
console.log(`Status: ${job.status}`);
console.log(`Progress: ${job.progress}%`);
if (job.status === 'completed' && job.outputAsset) {
console.log(`Output video: ${job.outputAsset.cdnUrl}`);
console.log(`Duration: ${job.outputAsset.duration}s`);
console.log(`Size: ${job.outputAsset.size} bytes`);
}
// Status values: 'pending' | 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'
```
### List Merge Jobs
```typescript
const { jobs, total, hasMore } = await stack0.cdn.listMergeJobs({
projectSlug: 'my-project',
status: 'completed', // Optional: filter by status
limit: 20,
offset: 0,
});
```
### Cancel Merge Job
```typescript
await stack0.cdn.cancelMergeJob('merge_abc123');
```
### Merge Job Status Flow
1. **pending**: Job created, waiting to be queued
2. **queued**: Job queued for processing
3. **processing**: Video being merged
4. **completed**: Merge finished, output asset available
5. **failed**: Merge failed (check `errorMessage`)
6. **cancelled**: Job was cancelled
### Use Cases
#### Photo Slideshow with Music
```typescript
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'photo-1', duration: 3 },
{ assetId: 'photo-2', duration: 3 },
{ assetId: 'photo-3', duration: 3 },
],
audioTrack: {
assetId: 'background-music',
fadeIn: 1,
fadeOut: 2,
},
output: {
format: 'mp4',
quality: '1080p',
},
});
```
#### TikTok/Instagram Style Video with Text Captions
```typescript
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{
assetId: 'intro-image',
duration: 3,
textOverlay: {
text: 'Welcome to my story!',
position: 'bottom',
fontSize: 64,
fontWeight: 'bold',
color: '#FFFFFF',
stroke: { color: '#000000', width: 2 },
shadow: { color: '#000000', offsetX: 2, offsetY: 2 },
},
},
{
assetId: 'main-content-video',
startTime: 0,
endTime: 15,
textOverlay: {
text: 'This is the main part',
position: 'bottom',
fontSize: 48,
color: '#FFFFFF',
backgroundColor: 'rgba(0,0,0,0.5)',
padding: 20,
},
},
{
assetId: 'outro-image',
duration: 3,
textOverlay: {
text: 'Follow for more!',
position: 'center',
fontSize: 72,
fontWeight: 'bold',
color: '#FFD700',
stroke: { color: '#000000', width: 3 },
},
},
],
audioTrack: {
assetId: 'trending-sound',
loop: true,
fadeOut: 2,
},
output: {
format: 'mp4',
quality: '1080p',
},
});
```
#### Video Compilation with Trimming
```typescript
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'intro-video' },
{ assetId: 'main-footage', startTime: 30, endTime: 90 }, // 60 second clip
{ assetId: 'title-card-image', duration: 5 },
{ assetId: 'outro-video', endTime: 10 }, // First 10 seconds only
],
output: {
format: 'mp4',
quality: '1080p',
filename: 'highlight-reel.mp4',
},
});
```
### Merge Limits
- Maximum inputs per job: 100
- Maximum image duration: 3600 seconds (1 hour)
- Maximum fade duration: 10 seconds
### Asset Status Flow
1. **pending**: Upload URL generated, waiting for upload
2. **processing**: File uploaded, being processed (thumbnails, metadata extraction)
3. **ready**: Asset ready for use
4. **failed**: Processing failed
### CDN URL Structure
Assets are served from the CDN with the following URL patterns:
- Original: `https://cdn.stack0.dev/{s3Key}`
- Transformed: `https://cdn.stack0.dev/{s3Key}?w=800&h=600&fit=cover&f=webp&q=80`
### Private Files
Private files are stored in a separate secure bucket with no public CDN access. They can only be accessed through presigned download URLs with configurable expiration times (1 hour to 7 days).
#### Upload Private File
```typescript
// High-level upload (handles presigned URL flow)
const file = await stack0.cdn.uploadPrivate({
projectSlug: 'my-project',
file: fileBuffer, // Blob, Buffer, or ArrayBuffer
filename: 'confidential.pdf',
mimeType: 'application/pdf',
folder: '/contracts',
description: 'Q4 Sales Contract',
metadata: { clientId: 'client_123' },
});
// Response
{
id: 'file_abc123',
filename: 'confidential.pdf',
mimeType: 'application/pdf',
size: 102400,
status: 'ready',
folder: '/contracts',
createdAt: Date
}
```
#### Manual Upload Flow
```typescript
// 1. Get presigned upload URL
const { uploadUrl, fileId, expiresAt } = await stack0.cdn.getPrivateUploadUrl({
projectSlug: 'my-project',
filename: 'report.xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
size: 204800,
});
// 2. Upload directly to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
});
// 3. Confirm upload completed
const privateFile = await stack0.cdn.confirmPrivateUpload(fileId);
```
#### Generate Download URL
Create a temporary presigned download URL:
```typescript
const { downloadUrl, expiresAt } = await stack0.cdn.getPrivateDownloadUrl({
fileId: 'file_abc123',
expiresIn: 86400, // 24 hours (1hr to 7 days supported)
});
// Use the URL for secure downloads
// URL is valid only until expiresAt
```
#### List Private Files
```typescript
const { files, total, hasMore } = await stack0.cdn.listPrivateFiles({
projectSlug: 'my-project',
folder: '/contracts',
status: 'ready',
search: 'contract',
sortBy: 'createdAt',
sortOrder: 'desc',
limit: 20,
offset: 0,
});
```
#### Update Private File
```typescript
const file = await stack0.cdn.updatePrivateFile({
fileId: 'file_abc123',
description: 'Updated description',
folder: '/contracts/archived',
tags: ['contract', '2024'],
});
```
#### Delete Private Files
```typescript
// Single file
await stack0.cdn.deletePrivateFile('file_abc123');
// Multiple files
const { deletedCount } = await stack0.cdn.deletePrivateFiles([
'file_abc123',
'file_def456',
]);
```
### Download Bundles
Download bundles allow you to create zip archives containing multiple public assets and/or private files. Bundles are created asynchronously via background jobs.
#### Create Bundle
```typescript
const { bundle } = await stack0.cdn.createBundle({
projectSlug: 'my-project',
name: 'Project Assets - Dec 2024',
description: 'All project media files',
assetIds: ['asset_1', 'asset_2'], // Public CDN assets
privateFileIds: ['file_1', 'file_2'], // Private files
expiresIn: 86400, // Bundle expires in 24 hours
});
// Response
{
id: 'bundle_abc123',
name: 'Project Assets - Dec 2024',
status: 'pending', // 'pending' | 'processing' | 'ready' | 'failed' | 'expired'
createdAt: Date
}
```
#### Check Bundle Status
```typescript
const bundle = await stack0.cdn.getBundle('bundle_abc123');
if (bundle.status === 'ready') {
console.log(`Bundle ready: ${bundle.fileCount} files, ${bundle.size} bytes`);
}
```
#### Download Bundle
```typescript
const { downloadUrl, expiresAt } = await stack0.cdn.getBundleDownloadUrl({
bundleId: 'bundle_abc123',
expiresIn: 3600, // 1 hour
});
// Download the zip file
window.location.href = downloadUrl;
// or
const response = await fetch(downloadUrl);
```
#### List Bundles
```typescript
const { bundles, total, hasMore } = await stack0.cdn.listBundles({
projectSlug: 'my-project',
status: 'ready',
search: 'Dec 2024',
limit: 20,
});
```
#### Delete Bundle
```typescript
await stack0.cdn.deleteBundle('bundle_abc123');
```
### Bundle Status Flow
1. **pending**: Bundle created, job queued
2. **processing**: Background worker creating zip archive
3. **ready**: Bundle ready for download
4. **failed**: Bundle creation failed (check error)
5. **expired**: Bundle download link expired
## Mail API Reference
The Mail SDK provides a complete email platform including transactional emails, templates, audiences, contacts, campaigns, automated sequences, and event tracking.
### Core Email Operations
#### Send Email
```typescript
const result = await stack0.mail.send({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Hello World',
html: 'Email content here
',
text: 'Email content here', // Optional plaintext
});
// With display name
const result = await stack0.mail.send({
from: { email: 'noreply@yourdomain.com', name: 'My App' },
to: { email: 'user@example.com', name: 'User' },
subject: 'Hello World',
html: 'Email content here
',
});
// With threading (for replies)
const result = await stack0.mail.send({
from: 'support@yourdomain.com',
to: 'user@example.com',
subject: 'Re: Your question',
html: 'Here is my reply...
',
inReplyTo: '',
references: '',
});
```
#### Send Batch (up to 100 emails)
```typescript
await stack0.mail.sendBatch({
emails: [
{ from: 'noreply@example.com', to: 'user1@example.com', subject: 'Hello', html: 'Hi User 1
' },
{ from: 'noreply@example.com', to: 'user2@example.com', subject: 'Hello', html: 'Hi User 2
' },
]
});
```
#### Send Broadcast (same content, up to 1000 recipients)
```typescript
await stack0.mail.sendBroadcast({
from: 'newsletter@yourdomain.com',
to: ['user1@example.com', 'user2@example.com'],
subject: 'Newsletter',
html: 'Content
',
});
```
#### Get, List, Resend, Cancel
```typescript
const email = await stack0.mail.get('email_id');
const { emails } = await stack0.mail.list({ status: 'delivered', limit: 50 });
await stack0.mail.resend('email_id');
await stack0.mail.cancel('email_id'); // For scheduled emails
```
#### Analytics
```typescript
const analytics = await stack0.mail.getAnalytics();
const timeSeries = await stack0.mail.getTimeSeriesAnalytics({ days: 30 });
const hourly = await stack0.mail.getHourlyAnalytics();
const { senders } = await stack0.mail.listSenders();
```
### Domains (stack0.mail.domains)
Manage sending domains with DNS verification.
```typescript
// List domains
const domains = await stack0.mail.domains.list({ projectSlug: 'my-project' });
// Add and verify domain
const { dnsRecords } = await stack0.mail.domains.add({ domain: 'yourdomain.com' });
const records = await stack0.mail.domains.getDnsRecords('domain_id');
const { verified } = await stack0.mail.domains.verify('domain_id');
// Set default and delete
await stack0.mail.domains.setDefault('domain_id');
await stack0.mail.domains.delete('domain_id');
```
### Templates (stack0.mail.templates)
Create and manage reusable email templates with variable substitution.
```typescript
// CRUD operations
const template = await stack0.mail.templates.create({
name: 'Welcome Email',
slug: 'welcome',
subject: 'Welcome {{name}}!',
html: 'Hi {{name}}
Thanks for joining!
',
});
const { templates } = await stack0.mail.templates.list({ search: 'welcome' });
const template = await stack0.mail.templates.get('template_id');
const template = await stack0.mail.templates.getBySlug('welcome');
await stack0.mail.templates.update({ id: 'template_id', subject: 'New subject' });
await stack0.mail.templates.delete('template_id');
// Preview with variables
const preview = await stack0.mail.templates.preview({
id: 'template_id',
variables: { name: 'John' },
});
```
### Audiences (stack0.mail.audiences)
Organize contacts into lists for targeted email campaigns.
```typescript
// CRUD
const audience = await stack0.mail.audiences.create({ name: 'Newsletter Subscribers' });
const { audiences } = await stack0.mail.audiences.list({ search: 'newsletter' });
const audience = await stack0.mail.audiences.get('audience_id');
await stack0.mail.audiences.update({ id: 'audience_id', name: 'New Name' });
await stack0.mail.audiences.delete('audience_id');
// Manage contacts in audience
const { contacts } = await stack0.mail.audiences.listContacts({ id: 'audience_id' });
await stack0.mail.audiences.addContacts({ id: 'audience_id', contactIds: ['c1', 'c2'] });
await stack0.mail.audiences.removeContacts({ id: 'audience_id', contactIds: ['c1'] });
```
### Contacts (stack0.mail.contacts)
Manage individual contacts with metadata.
```typescript
// CRUD
const contact = await stack0.mail.contacts.create({
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
metadata: { company: 'Acme' },
});
const { contacts } = await stack0.mail.contacts.list({ search: 'john', status: 'subscribed' });
const contact = await stack0.mail.contacts.get('contact_id');
await stack0.mail.contacts.update({ id: 'contact_id', firstName: 'Jane' });
await stack0.mail.contacts.delete('contact_id');
// Bulk import
const result = await stack0.mail.contacts.import({
audienceId: 'audience_id',
contacts: [
{ email: 'a@example.com', firstName: 'Alice' },
{ email: 'b@example.com', firstName: 'Bob' },
],
});
```
### Campaigns (stack0.mail.campaigns)
Create and send email campaigns to audiences.
```typescript
// CRUD
const campaign = await stack0.mail.campaigns.create({
name: 'Product Launch',
subject: 'Introducing Our New Product',
fromEmail: 'news@yourdomain.com',
audienceId: 'audience_id',
templateId: 'template_id',
});
const { campaigns } = await stack0.mail.campaigns.list({ status: 'draft' });
const campaign = await stack0.mail.campaigns.get('campaign_id');
await stack0.mail.campaigns.update({ id: 'campaign_id', subject: 'New Subject' });
await stack0.mail.campaigns.delete('campaign_id');
// Lifecycle
await stack0.mail.campaigns.send({ id: 'campaign_id', sendNow: true });
await stack0.mail.campaigns.send({ id: 'campaign_id', scheduledAt: new Date('2025-01-15') });
await stack0.mail.campaigns.pause('campaign_id');
await stack0.mail.campaigns.cancel('campaign_id');
const duplicate = await stack0.mail.campaigns.duplicate('campaign_id');
// Statistics
const stats = await stack0.mail.campaigns.getStats('campaign_id');
```
### Sequences (stack0.mail.sequences)
Create automated email sequences. Supported triggers: 'contact_created', 'contact_updated' (set triggerConfig.propertyName, optional propertyValue), 'contact_added_to_list' (set triggerConfig.audienceId), and 'event_received' (set triggerConfig.eventName). Contacts can also be enrolled manually with addContact().
```typescript
// CRUD
const sequence = await stack0.mail.sequences.create({
name: 'Onboarding Flow',
triggerType: 'event_received',
triggerConfig: { eventName: 'user_signup' },
});
const { sequences } = await stack0.mail.sequences.list({ status: 'active' });
const sequence = await stack0.mail.sequences.get('sequence_id'); // Includes nodes, connections, and avgCompletionMs (avg time to complete, ms)
// Lifecycle
await stack0.mail.sequences.publish('sequence_id');
await stack0.mail.sequences.pause('sequence_id');
await stack0.mail.sequences.resume('sequence_id');
await stack0.mail.sequences.archive('sequence_id');
const duplicate = await stack0.mail.sequences.duplicate('sequence_id');
// Node management
const node = await stack0.mail.sequences.createNode({
id: 'sequence_id',
nodeType: 'email',
name: 'Welcome Email',
positionX: 100,
positionY: 200,
});
await stack0.mail.sequences.setNodeEmail('sequence_id', {
nodeId: 'node_id',
subject: 'Welcome!',
templateId: 'template_id',
});
await stack0.mail.sequences.setNodeTimer('sequence_id', {
nodeId: 'node_id',
delayAmount: 3,
delayUnit: 'days',
});
// Connections between nodes
await stack0.mail.sequences.createConnection({
id: 'sequence_id',
sourceNodeId: 'node_1',
targetNodeId: 'node_2',
});
// Contact entries (paginated + searchable by email/name)
const { entries } = await stack0.mail.sequences.listEntries({ id: 'sequence_id', search: 'jane@', status: 'active', limit: 25, offset: 0 });
await stack0.mail.sequences.addContact({ id: 'sequence_id', contactId: 'contact_id' });
await stack0.mail.sequences.removeContact({ id: 'sequence_id', entryId: 'entry_id' });
// Analytics
const analytics = await stack0.mail.sequences.getAnalytics('sequence_id');
```
### Events (stack0.mail.events)
Define and track custom events to trigger sequences and segment contacts.
```typescript
// Define events
const event = await stack0.mail.events.create({
name: 'purchase_completed',
description: 'Triggered when a user completes a purchase',
propertiesSchema: {
properties: [
{ name: 'amount', type: 'number', required: true },
{ name: 'product_id', type: 'string' },
],
},
});
const { events } = await stack0.mail.events.list();
const event = await stack0.mail.events.get('event_id');
// Track events (triggers sequences)
await stack0.mail.events.track({
eventName: 'purchase_completed',
contactEmail: 'user@example.com',
properties: { amount: 99.99, product_id: 'prod_123' },
});
// Batch tracking
await stack0.mail.events.trackBatch({
events: [
{ eventName: 'page_viewed', contactEmail: 'a@example.com', properties: { page: '/pricing' } },
{ eventName: 'page_viewed', contactEmail: 'b@example.com', properties: { page: '/features' } },
],
});
// View occurrences and analytics
const { occurrences } = await stack0.mail.events.listOccurrences({ eventId: 'event_id' });
const analytics = await stack0.mail.events.getAnalytics('event_id');
```
### Agent Mailboxes (stack0.mail.mailboxes)
Create email accounts for AI agents that can send AND receive. Inbound emails are delivered via webhook.
#### Create a Mailbox
```typescript
const mailbox = await stack0.mail.mailboxes.create({
address: 'remi', // local part (auto-generated if omitted)
displayName: 'Remi',
domain: 'agents.mastagents.com', // must be a verified domain
webhookUrl: 'https://mastagents.com/api/webhooks/inbound-email',
metadata: { agentId: 'abc123' },
maxInboundPerDay: 1000, // rate limit (default: 1000)
});
// mailbox.address = "remi@agents.mastagents.com"
```
#### Send as the Agent
Use the standard `mail.send()` with the mailbox address:
```typescript
await stack0.mail.send({
from: { email: 'remi@agents.mastagents.com', name: 'Remi' },
to: 'craig@company.com',
subject: 'Daily briefing',
html: 'Here is your daily briefing...
',
});
```
#### Manage Mailboxes
```typescript
const { mailboxes } = await stack0.mail.mailboxes.list({ domain: 'agents.mastagents.com' });
const mailbox = await stack0.mail.mailboxes.get('mailbox_id');
await stack0.mail.mailboxes.update({ id: 'mailbox_id', status: 'paused' });
await stack0.mail.mailboxes.delete('mailbox_id');
```
#### Inbound Messages
When someone replies to your agent's email, Stack0 delivers the message to your webhook URL:
```json
{
"event": "email.inbound",
"mailbox": "remi@agents.mastagents.com",
"mailboxId": "...",
"from": { "email": "craig@company.com", "name": "Craig" },
"to": "remi@agents.mastagents.com",
"subject": "Re: Daily briefing",
"text": "Thanks, looks good!",
"html": "Thanks, looks good!
",
"messageId": "",
"inReplyTo": "",
"references": [""],
"attachments": [],
"metadata": { "agentId": "abc123" },
"receivedAt": "2026-04-09T10:30:00Z"
}
```
Webhooks are signed with `X-Stack0-Signature` (HMAC-SHA256). Failed deliveries retry with exponential backoff (1m, 5m, 30m, 2h, 12h, max 5 attempts).
#### View Inbound Messages
```typescript
const { messages } = await stack0.mail.mailboxes.listMessages({ mailboxId: 'mailbox_id' });
const message = await stack0.mail.mailboxes.getMessage('message_id');
```
#### Setup Requirements
To receive inbound email on a verified domain, add an MX record:
- **Type**: MX
- **Value**: `10 inbound-smtp.us-east-1.amazonaws.com`
### Email Status Flow
1. **pending**: Email created, queued for sending
2. **sent**: Email handed off to mail server
3. **delivered**: Email successfully delivered to recipient
4. **bounced**: Email rejected by recipient server
5. **failed**: Permanent delivery failure
## Screenshots API Reference
The Screenshots API enables you to capture high-quality screenshots of any webpage with full browser rendering.
### Basic Screenshot
```typescript
const { id, status } = await stack0.screenshots.capture({
url: 'https://example.com',
format: 'png', // 'png' | 'jpeg' | 'webp' | 'pdf'
fullPage: false, // Capture full scrollable page
deviceType: 'desktop', // 'desktop' | 'tablet' | 'mobile'
});
// Poll for completion
const screenshot = await stack0.screenshots.get({ id });
```
### Capture and Wait
For synchronous operations, use `captureAndWait`:
```typescript
const screenshot = await stack0.screenshots.captureAndWait({
url: 'https://example.com',
format: 'png',
fullPage: true,
blockAds: true,
blockCookieBanners: true,
});
// Response
{
id: 'scr_abc123',
status: 'completed',
imageUrl: 'https://cdn.stack0.dev/screenshots/...',
imageWidth: 1280,
imageHeight: 720,
processingTimeMs: 2340,
}
```
#### Screenshot Options
- `url` (required): URL to capture
- `format`: Output format - `png`, `jpeg`, `webp`, `pdf`
- `quality`: Image quality 1-100 (for jpeg/webp)
- `fullPage`: Capture full scrollable page
- `deviceType`: `desktop` (1280x720), `tablet` (768x1024), `mobile` (375x667)
- `viewportWidth`: Custom viewport width (320-3840)
- `viewportHeight`: Custom viewport height (240-2160)
- `deviceScaleFactor`: Device pixel ratio (1-3)
- `waitForSelector`: CSS selector to wait for before capture
- `waitForTimeout`: Additional wait time in ms (0-30000)
- `blockAds`: Block advertisements
- `blockCookieBanners`: Block cookie consent popups
- `blockChatWidgets`: Block chat widgets
- `blockTrackers`: Block tracking scripts
- `darkMode`: Enable dark mode
- `customCss`: Inject custom CSS (max 10KB)
- `customJs`: Inject custom JavaScript (max 10KB)
- `selector`: Capture specific element only
- `hideSelectors`: Array of CSS selectors to hide (max 50)
- `clickSelector`: Click element before capture
- `clip`: Capture specific region `{ x, y, width, height }`
## Integrations API Reference
The Integrations API provides a unified interface to access your users' third-party applications. Connect once, access CRM, storage, communication, and productivity tools through one consistent API.
### Supported Categories
- **CRM**: HubSpot, Salesforce, Pipedrive, Attio - contacts, companies, deals
- **Storage**: Google Drive, Dropbox, OneDrive - files, folders, uploads
- **Communication**: Slack, Discord - channels, messages, users
- **Productivity**: Notion, Google Sheets, Airtable - documents, tables, rows
- **Calendar**: Google Calendar - calendars, events
- **Email**: Gmail - messages, threads, labels
### Setting Up Connections
Users connect their apps through the IntegrationsConnect component or via OAuth flow:
```typescript
// Server-side: Create a connection session for a user
const session = await stack0.integrations.createConnectionSession({
endUserId: 'user_123', // Your user's ID
connectorSlug: 'hubspot', // Which app to connect
redirectUrl: 'https://yourapp.com/integrations/callback',
});
// Redirect user to session.authUrl to complete OAuth
```
### List Available Connectors
```typescript
const connectors = await stack0.integrations.listConnectors();
// Returns available integrations
[
{ slug: 'hubspot', name: 'HubSpot', category: 'crm', status: 'active' },
{ slug: 'google-drive', name: 'Google Drive', category: 'storage', status: 'active' },
{ slug: 'slack', name: 'Slack', category: 'communication', status: 'active' },
// ...
]
```
### List User Connections
```typescript
const connections = await stack0.integrations.listConnections({
endUserId: 'user_123',
});
// Returns user's connected apps
[
{
id: 'conn_abc123',
connectorSlug: 'hubspot',
status: 'active',
createdAt: Date,
},
]
```
### CRM Operations
```typescript
// List contacts
const { contacts, nextCursor } = await stack0.integrations.crm.listContacts({
connectionId: 'conn_abc123',
limit: 50,
cursor: undefined, // For pagination
});
// Create a contact
const contact = await stack0.integrations.crm.createContact({
connectionId: 'conn_abc123',
data: {
email: 'john@example.com',
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890',
},
});
// List companies
const { companies } = await stack0.integrations.crm.listCompanies({
connectionId: 'conn_abc123',
});
// List deals/opportunities
const { deals } = await stack0.integrations.crm.listDeals({
connectionId: 'conn_abc123',
});
// Create a note
await stack0.integrations.crm.createNote({
connectionId: 'conn_abc123',
data: {
content: 'Follow up on proposal',
contactId: 'contact_123',
},
});
```
### Storage Operations
```typescript
// List files in a folder
const { files } = await stack0.integrations.storage.listFiles({
connectionId: 'conn_def456',
folderId: 'root', // or specific folder ID
});
// Upload a file
const file = await stack0.integrations.storage.uploadFile({
connectionId: 'conn_def456',
name: 'report.pdf',
content: fileBuffer, // Buffer or base64
mimeType: 'application/pdf',
folderId: 'folder_123', // Optional
});
// Download a file
const { content, mimeType } = await stack0.integrations.storage.downloadFile({
connectionId: 'conn_def456',
fileId: 'file_abc',
});
// Create a folder
const folder = await stack0.integrations.storage.createFolder({
connectionId: 'conn_def456',
name: 'Reports',
parentId: 'root',
});
```
### Communication Operations
```typescript
// List channels
const { channels } = await stack0.integrations.communication.listChannels({
connectionId: 'conn_ghi789',
});
// Send a message
await stack0.integrations.communication.sendMessage({
connectionId: 'conn_ghi789',
channelId: 'C1234567890',
text: 'Hello from your app!',
});
// List users
const { users } = await stack0.integrations.communication.listUsers({
connectionId: 'conn_ghi789',
});
```
### Productivity Operations
```typescript
// List documents (Notion pages, etc.)
const { documents } = await stack0.integrations.productivity.listDocuments({
connectionId: 'conn_jkl012',
});
// List tables (Google Sheets, Airtable bases)
const { tables } = await stack0.integrations.productivity.listTables({
connectionId: 'conn_jkl012',
});
// Get table rows
const { rows } = await stack0.integrations.productivity.getTableRows({
connectionId: 'conn_jkl012',
tableId: 'table_abc',
});
// Create a table row
await stack0.integrations.productivity.createTableRow({
connectionId: 'conn_jkl012',
tableId: 'table_abc',
data: {
Name: 'New Item',
Status: 'Active',
Date: '2025-01-15',
},
});
```
### Calendar Operations
```typescript
// List calendars
const { data: calendars } = await stack0.integrations.calendar.listCalendars('conn_abc123');
// List events with time range
const { data: events } = await stack0.integrations.calendar.listEvents('conn_abc123', 'primary', {
timeMin: new Date('2026-04-01'),
timeMax: new Date('2026-04-30'),
});
// Create event
const event = await stack0.integrations.calendar.createEvent('conn_abc123', 'primary', {
title: 'Team Standup',
startTime: new Date('2026-04-10T09:00:00Z'),
endTime: new Date('2026-04-10T09:30:00Z'),
attendees: [{ email: 'teammate@company.com' }],
location: 'Zoom',
});
// Update / delete event
await stack0.integrations.calendar.updateEvent('conn_abc123', 'primary', 'event_id', { title: 'Updated Title' });
await stack0.integrations.calendar.deleteEvent('conn_abc123', 'primary', 'event_id');
```
### Email Operations (Gmail)
```typescript
// List messages
const { data: messages } = await stack0.integrations.mail.listMessages('conn_abc123', { query: 'is:unread' });
// Get message
const message = await stack0.integrations.mail.getMessage('conn_abc123', 'msg_id');
// Send email
const sent = await stack0.integrations.mail.sendMessage('conn_abc123', {
to: ['user@example.com'],
subject: 'Hello',
body: 'Email body text',
});
// Threads
const { data: threads } = await stack0.integrations.mail.listThreads('conn_abc123');
const thread = await stack0.integrations.mail.getThread('conn_abc123', 'thread_id');
// Labels
const labels = await stack0.integrations.mail.listLabels('conn_abc123');
```
### Passthrough API
For provider-specific features not covered by the unified API:
```typescript
// Make a raw API call to the connected provider
const response = await stack0.integrations.passthrough({
connectionId: 'conn_abc123',
method: 'GET',
path: '/crm/v3/objects/custom_object',
query: { limit: '10' },
});
```
### Delete Connection
```typescript
await stack0.integrations.deleteConnection({
connectionId: 'conn_abc123',
});
```
### IntegrationsConnect Element
Pre-built React component for connecting user apps:
```tsx
// Note: Requires a backend proxy for security (API keys must stay server-side)
// 1. Create API proxy route (app/api/integrations/[...path]/route.ts)
export async function POST(request, { params }) {
const session = await auth();
if (!session?.user) return unauthorized();
const path = params.path.join('/');
const body = await request.json();
const response = await fetch(`https://api.stack0.dev/v1/integrations/${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STACK0_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...body, endUserId: session.user.id }),
});
return Response.json(await response.json());
}
// 2. Use the component
import { IntegrationsConnect } from '@stack0/elements';
{
console.log('Connected:', connection.connectorSlug);
}}
onError={(error) => {
console.error('Connection failed:', error);
}}
/>
```
## AI Extraction API Reference
Extract structured data from any webpage using AI.
### Basic Extraction
```typescript
const { id, status } = await stack0.extraction.extract({
url: 'https://example.com/article',
mode: 'markdown', // 'auto' | 'markdown' | 'schema' | 'raw'
});
// Poll for completion
const extraction = await stack0.extraction.get({ id });
```
### Extract and Wait
```typescript
const extraction = await stack0.extraction.extractAndWait({
url: 'https://example.com/product',
mode: 'schema',
schema: {
type: 'object',
properties: {
title: { type: 'string' },
price: { type: 'number' },
description: { type: 'string' },
inStock: { type: 'boolean' },
},
},
});
// Response
{
id: 'ext_abc123',
status: 'completed',
extractedData: {
title: 'Product Name',
price: 29.99,
description: 'Product description...',
inStock: true,
},
processingTimeMs: 3200,
}
```
#### Extraction Modes
- `auto`: AI automatically extracts the most relevant content
- `markdown`: Convert page content to clean, formatted markdown
- `schema`: Extract data matching your custom JSON schema
- `html`: Get the raw HTML content
#### Extraction Options
- `url` (required): URL to extract from
- `mode`: Extraction mode
- `schema`: JSON Schema for structured extraction (when mode is `schema`)
- `prompt`: Custom prompt to guide AI extraction
- `includeLinks`: Include links in extracted content
- `includeImages`: Include image URLs in extracted content
- `includeMetadata`: Include page metadata (title, description, etc.)
- `waitForSelector`: CSS selector to wait for before extraction
- `waitForTimeout`: Additional wait time in ms
### Get Screenshot/Extraction Status
Check the status of an async operation:
```typescript
// Get screenshot status
const screenshot = await stack0.screenshots.get({ id: 'scr_abc123' });
// Get extraction status
const extraction = await stack0.extraction.get({ id: 'ext_abc123' });
```
### Status Flow
1. **pending**: Request created, queued for processing
2. **processing**: Being processed by workers
3. **completed**: Successfully completed
4. **failed**: Processing failed (check `error` field)
### Batch Processing
Process multiple URLs in parallel:
```typescript
// Batch screenshots
const batch = await stack0.screenshots.batchAndWait({
urls: [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
],
config: {
format: 'png',
fullPage: true,
},
});
// Batch extractions
const extractionBatch = await stack0.extraction.batchAndWait({
urls: ['https://example.com/article1', 'https://example.com/article2'],
config: {
mode: 'markdown',
includeMetadata: true,
},
});
```
### Webhooks
Configure webhooks to receive notifications when jobs complete:
```typescript
const { id } = await stack0.screenshots.capture({
url: 'https://example.com',
webhookUrl: 'https://yourapp.com/webhook',
webhookSecret: 'your-secret-key',
});
```
Webhook payload:
```json
{
"event": "screenshot.completed",
"data": {
"id": "scr_abc123",
"status": "completed",
"imageUrl": "https://cdn.stack0.dev/screenshots/...",
"processingTimeMs": 2340
}
}
```
### Caching
Enable caching to avoid re-processing identical requests:
```typescript
const screenshot = await stack0.screenshots.captureAndWait({
url: 'https://example.com',
cacheKey: 'homepage-v1',
cacheTtl: 3600, // Cache for 1 hour
});
```
## Crawl API
Recursively crawl a website and scrape every page. Honors robots.txt and sitemap.xml. Open Firecrawl alternative.
```typescript
const crawl = await stack0.crawl.startAndWait({
url: "https://docs.example.com",
maxDepth: 3,
maxPages: 200,
formats: ["markdown", "links"],
respectRobotsTxt: true,
stealth: true,
});
for (const page of crawl.pages ?? []) {
console.log(page.url, page.markdown?.slice(0, 120));
}
```
Endpoints (prefix all with `/v1`): `POST /webdata/crawl`, `GET /webdata/crawl/{id}?includePages=true`, `GET /webdata/crawl/{crawlId}/pages`, `POST /webdata/crawl/{id}/cancel`, `DELETE /webdata/crawl/{id}`, `GET /webdata/crawl` (list).
## Map API
Discover every URL on a site in seconds. Uses `sitemap.xml` with a link-crawl fallback.
```typescript
const { urls } = await stack0.map.createAndWait({
url: "https://docs.example.com",
search: "api",
limit: 5000,
});
```
Endpoints (prefix all with `/v1`): `POST /webdata/map`, `GET /webdata/map/{id}`, `GET /webdata/map`, `DELETE /webdata/map/{id}`.
## Documents API
Parse PDF and DOCX files into clean markdown.
```typescript
const doc = await stack0.documents.parseAndWait({
url: "https://example.com/whitepaper.pdf",
});
console.log(doc.pageCount, doc.markdown);
```
Endpoints (prefix all with `/v1`): `POST /webdata/documents`, `GET /webdata/documents/{id}`, `GET /webdata/documents`, `DELETE /webdata/documents/{id}`.
## Actions (multi-step scrapes)
Pass an `actions` array on any scrape, extraction, crawl, or search request to drive the page before content is captured. Supported types: `click`, `type`, `press`, `scroll`, `wait`, `screenshot`, `execute_js`, `extract`.
```typescript
await stack0.extraction.extractAndWait({
url: "https://example.com/search",
mode: "markdown",
actions: [
{ type: "type", selector: "input[name=q]", text: "playwright" },
{ type: "press", key: "Enter" },
{ type: "wait", selector: ".results", timeout: 5000 },
{ type: "scroll", direction: "down", pixels: 1200 },
{ type: "screenshot" },
],
});
```
## Stealth & Proxy
Every webdata request accepts `stealth: true` (default) which applies playwright-extra's evasion patches. Set `proxy: "residential" | "datacenter"` and optional `proxyLocation: "us"` to route through your configured proxy provider (configure via `WEBDATA_PROXY_SERVER` env var).
## MCP Server
The Stack0 MCP server exposes crawl, map, search, and document tools alongside screenshots and extraction:
- `crawl_start`, `crawl_get`, `crawl_list`, `crawl_cancel`
- `map_create`, `map_get`, `map_list`
- `document_parse`, `document_get`, `document_list`
Point Claude, Cursor, or any MCP client at `npx @stack0/mcp` with `STACK0_API_KEY` set.
## Workflows API Reference
The Workflows API enables you to build multi-step AI pipelines with sequential and parallel execution, variable passing between steps, and webhook notifications.
### Supported Providers
- **Anthropic**: Claude models (claude-3-5-sonnet, claude-3-opus, claude-3-haiku)
- **OpenAI**: GPT models (gpt-4o, gpt-4-turbo, gpt-3.5-turbo)
- **Gemini**: Google AI models (gemini-1.5-pro, gemini-1.5-flash)
- **Replicate**: Image/video models (SDXL, Stable Diffusion, etc.)
- **Stack0**: Built-in AI capabilities
### Step Types
- `llm`: Text generation with AI models
- `image`: Image generation (Replicate, DALL-E)
- `video`: Video generation (Kling, Replicate)
- `audio`: Audio generation (ElevenLabs, etc.)
- `code`: Execute custom code
- `http`: Make HTTP requests
- `transform`: Transform data between steps
- `condition`: Conditional branching
- `loop`: Iterate over arrays
### Create Workflow
```typescript
const workflow = await stack0.workflows.create({
projectSlug: 'my-project',
slug: 'content-pipeline',
name: 'Content Generation Pipeline',
description: 'Generate blog posts with AI',
steps: [
{
id: 'generate-outline',
type: 'llm',
name: 'Generate Outline',
provider: 'anthropic',
model: 'claude-3-5-sonnet-20241022',
prompt: 'Create an outline for a blog post about {{topic}}',
outputVariable: 'outline',
},
{
id: 'write-content',
type: 'llm',
name: 'Write Content',
provider: 'openai',
model: 'gpt-4o',
prompt: 'Write a blog post based on this outline:\n\n{{steps.generate-outline.output}}',
dependsOn: ['generate-outline'],
outputVariable: 'content',
},
],
variables: {
topic: { type: 'string', required: true },
},
});
```
### List Workflows
```typescript
const { workflows, total, hasMore } = await stack0.workflows.list({
projectSlug: 'my-project',
limit: 20,
offset: 0,
});
```
### Get Workflow
```typescript
const workflow = await stack0.workflows.get({
projectSlug: 'my-project',
id: 'workflow_abc123',
});
```
### Update Workflow
```typescript
const workflow = await stack0.workflows.update({
projectSlug: 'my-project',
id: 'workflow_abc123',
name: 'Updated Pipeline Name',
steps: [...updatedSteps],
isActive: true,
});
```
### Delete Workflow
```typescript
await stack0.workflows.delete({
projectSlug: 'my-project',
id: 'workflow_abc123',
});
```
### Run Workflow
Start a workflow execution:
```typescript
const run = await stack0.workflows.run({
projectSlug: 'my-project',
workflowId: 'workflow_abc123',
variables: {
topic: 'AI in Healthcare',
},
webhook: {
url: 'https://yourapp.com/webhook',
secret: 'your-webhook-secret',
},
metadata: {
userId: 'user_123',
source: 'api',
},
});
// Response
{
id: 'run_xyz789',
workflowId: 'workflow_abc123',
status: 'pending',
variables: { topic: 'AI in Healthcare' },
createdAt: Date,
}
```
### Run and Wait
For synchronous execution, use `runAndWait`:
```typescript
const result = await stack0.workflows.runAndWait({
projectSlug: 'my-project',
workflowId: 'workflow_abc123',
variables: {
topic: 'AI in Healthcare',
},
});
// Response includes completed output
{
id: 'run_xyz789',
status: 'completed',
output: {
outline: '1. Introduction\n2. Current Applications...',
content: 'Artificial intelligence is transforming...',
},
totalDurationMs: 12500,
creditsUsed: 15,
}
```
### Get Run Status
```typescript
const run = await stack0.workflows.getRun({
projectSlug: 'my-project',
id: 'run_xyz789',
});
console.log(`Status: ${run.status}`);
console.log(`Progress: ${run.completedSteps}/${run.totalSteps}`);
// Check step states
for (const [stepId, state] of Object.entries(run.stepStates)) {
console.log(`${stepId}: ${state.status}`);
}
```
### List Runs
```typescript
const { items, total, hasMore } = await stack0.workflows.listRuns({
projectSlug: 'my-project',
workflowId: 'workflow_abc123', // Optional: filter by workflow
status: 'completed', // Optional: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
limit: 50,
offset: 0,
});
```
### Cancel Run
```typescript
await stack0.workflows.cancelRun({
projectSlug: 'my-project',
id: 'run_xyz789',
});
```
### Variable Interpolation
Use `{{variable}}` syntax to reference variables and step outputs:
```typescript
// Reference input variables
prompt: 'Write about {{topic}}'
// Reference step outputs
prompt: 'Expand on: {{steps.generate-outline.output}}'
// Reference nested data
prompt: 'Use this title: {{steps.generate-metadata.output.title}}'
```
### Step Configuration
Each step type has specific configuration options:
#### LLM Step
```typescript
{
id: 'step-id',
type: 'llm',
name: 'Generate Text',
provider: 'anthropic', // 'anthropic' | 'openai' | 'gemini'
model: 'claude-3-5-sonnet-20241022',
prompt: 'Your prompt here with {{variables}}',
systemPrompt: 'Optional system instructions',
temperature: 0.7, // 0-2
maxTokens: 4096,
responseFormat: 'text', // 'text' | 'json'
dependsOn: ['previous-step-id'], // Step dependencies
outputVariable: 'variableName',
}
```
#### Image Step
```typescript
{
id: 'generate-image',
type: 'image',
name: 'Generate Image',
provider: 'replicate',
model: 'stability-ai/sdxl',
prompt: 'A futuristic city at sunset',
width: 1024,
height: 1024,
outputVariable: 'generatedImage',
}
```
#### HTTP Step
```typescript
{
id: 'fetch-data',
type: 'http',
name: 'Fetch External Data',
method: 'POST',
url: 'https://api.example.com/data',
headers: {
'Authorization': 'Bearer {{apiKey}}',
},
body: {
query: '{{searchQuery}}',
},
outputVariable: 'externalData',
}
```
### Webhook Events
Configure webhooks to receive notifications:
- `run.started`: Workflow run started
- `run.completed`: Workflow run completed successfully
- `run.failed`: Workflow run failed
- `step.completed`: Individual step completed
Webhook payload:
```json
{
"event": "run.completed",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"id": "run_xyz789",
"workflowId": "workflow_abc123",
"status": "completed",
"output": { ... },
"totalDurationMs": 12500,
"creditsUsed": 15
}
}
```
### Run Status Flow
1. **pending**: Run created, waiting to start
2. **running**: Workflow executing steps
3. **completed**: All steps completed successfully
4. **failed**: One or more steps failed
5. **cancelled**: Run was cancelled
### Pricing
- **LLM Steps**: Based on token usage (varies by provider/model)
- **Image Generation**: $0.02-0.10 per image (varies by model)
- **Video Generation**: $0.50-2.00 per video (varies by model/duration)
- **HTTP Steps**: $0.001 per request
## Domain Verification
To send emails from your custom domain, you need to verify domain ownership.
### 1. Add Domain
In the dashboard, go to Mail → Domains → Add Domain and enter your domain (e.g., `yourdomain.com`).
### 2. Configure DNS Records
Add the provided DNS records to your domain:
- **SPF Record**: Authorizes Stack0 to send emails
- **DKIM Records**: Cryptographic authentication
- **DMARC Record**: Email policy and reporting
- **Verification Record**: Proves domain ownership
### 3. Verify Domain
Click "Verify Domain" in the dashboard. Verification typically completes within minutes but can take up to 72 hours.
### 4. Set as Default
Mark your verified domain as default to use it automatically for all emails.
## Email Tracking
Stack0 automatically tracks email engagement:
- **Sends**: When email leaves the server
- **Deliveries**: When recipient server accepts email
- **Opens**: When recipient opens email (requires HTML)
- **Clicks**: When recipient clicks links in email
- **Bounces**: When email is rejected
- **Complaints**: When recipient marks as spam
View analytics in the dashboard under Mail → Analytics.
## Common Use Cases
### Transactional Emails
Send emails triggered by user actions:
```typescript
// Welcome email
await stack0.mail.send({
from: 'welcome@yourapp.com',
to: newUser.email,
subject: 'Welcome to YourApp!',
templateId: 'welcome-email',
templateVariables: { name: newUser.name },
});
// Password reset
await stack0.mail.send({
from: 'noreply@yourapp.com',
to: user.email,
subject: 'Reset your password',
templateId: 'password-reset',
templateVariables: {
resetUrl: generateResetUrl(user.id),
expiresIn: '1 hour',
},
});
// Order confirmation
await stack0.mail.send({
from: 'orders@yourapp.com',
to: customer.email,
subject: `Order #${order.id} confirmed`,
templateId: 'order-confirmation',
templateVariables: { order, customer },
});
```
### Marketing Emails
Send newsletters and promotional content:
```typescript
// Newsletter to subscribers
const subscribers = await getActiveSubscribers();
await stack0.mail.sendBroadcast({
from: 'newsletter@yourapp.com',
to: subscribers.map(s => s.email),
subject: 'Monthly Update - October 2025',
templateId: 'monthly-newsletter',
tags: ['newsletter', 'october-2025'],
});
// Product announcement
await stack0.mail.sendBroadcast({
from: 'updates@yourapp.com',
to: userSegment.emails,
subject: 'Introducing Our New Feature!',
html: announcementHtml,
tags: ['product-update', 'feature-launch'],
});
```
### User-Generated Content with CDN
Handle user uploads in your application:
```typescript
// Upload user avatar
const avatar = await stack0.cdn.upload({
projectSlug: 'my-project',
file: uploadedFile,
filename: `avatar-${userId}.jpg`,
mimeType: 'image/jpeg',
folder: '/avatars',
metadata: { userId },
});
// Get optimized thumbnail (client-side, no API call)
const thumbnailUrl = stack0.cdn.getTransformUrl(avatar.cdnUrl, {
width: 150, height: 150, fit: 'cover', format: 'webp'
});
// Save to user profile
await updateUser(userId, { avatarUrl: thumbnailUrl });
```
### Product Images
Manage e-commerce product images:
```typescript
// Upload product image
const image = await stack0.cdn.upload({
projectSlug: 'my-project',
file: productImage,
filename: `product-${productId}-main.jpg`,
mimeType: 'image/jpeg',
folder: `/products/${productId}`,
tags: ['product', productId],
metadata: { productId, isPrimary: true },
});
// Generate multiple sizes for responsive images
const sizes = [
{ width: 1200, name: 'large' },
{ width: 800, name: 'medium' },
{ width: 400, name: 'small' },
{ width: 150, name: 'thumbnail' },
];
const urls = await Promise.all(
sizes.map(async ({ width, name }) => {
const { url } = await stack0.cdn.getTransformUrl({
assetId: image.id,
options: { width, format: 'webp', quality: 85 },
});
return { name, url };
})
);
```
## Error Handling
The SDK throws typed errors for different scenarios:
```typescript
import { Stack0, Stack0Error } from '@stack0/sdk';
try {
await stack0.mail.send({
from: 'invalid@unverified-domain.com',
to: 'user@example.com',
subject: 'Test',
html: 'Test
',
});
} catch (error) {
if (error instanceof Stack0Error) {
console.error('Stack0 Error:', error.message);
console.error('Status Code:', error.statusCode);
console.error('Error Code:', error.code);
}
}
```
Common error codes:
- `INVALID_FROM`: From address not verified
- `INVALID_EMAIL`: Malformed email address
- `RATE_LIMIT`: Too many requests
- `QUOTA_EXCEEDED`: Usage limit reached
- `UNAUTHORIZED`: Invalid API key
- `NOT_FOUND`: Resource not found
- `PROJECT_NOT_FOUND`: Invalid project slug
## Rate Limits
Default rate limits per organization:
- **Emails per second**: 10
- **Emails per minute**: 100
- **Emails per hour**: 1,000
- **Emails per day**: 10,000
- **CDN uploads per minute**: 60
- **CDN requests per second**: 100
- **Screenshots per hour**: 100 (Free), 1,000 (Starter), 10,000 (Pro)
- **Extractions per hour**: 50 (Free), 500 (Starter), 5,000 (Pro)
Contact support for higher limits. Rate limit information is included in response headers:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1637000000
```
## Webhooks
Configure webhooks to receive real-time notifications about email events.
### Available Events
- `email.sent`: Email sent to mail server
- `email.delivered`: Email delivered to recipient
- `email.bounced`: Email bounced
- `email.opened`: Recipient opened email
- `email.clicked`: Recipient clicked link
- `email.complained`: Recipient marked as spam
### Setup
1. Go to Mail → Webhooks → Create Webhook
2. Enter your endpoint URL
3. Select events to receive
4. Save and copy the signing secret
### Payload Example
```json
{
"event": "email.delivered",
"timestamp": "2025-11-24T10:30:00Z",
"data": {
"id": "email_123abc",
"from": "noreply@yourdomain.com",
"to": "user@example.com",
"subject": "Welcome!",
"status": "delivered",
"deliveredAt": "2025-11-24T10:30:00Z"
}
}
```
### Verifying Webhooks
Webhooks include a signature header for verification:
```typescript
import { createHmac } from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string) {
const hmac = createHmac('sha256', secret);
const digest = hmac.update(payload).digest('hex');
return digest === signature;
}
```
## Pricing
Usage-based pricing with no monthly fees:
- **Emails**: $0.50 per 1,000 emails
- **CDN Storage & Bandwidth**: $0.25 per GB
- **Image Transformations**: $0.50 per 1,000 transforms
- **Video Transcoding**: $0.005 per minute
- **Video Streaming**: $0.05 per GB
- **Audio Transcoding**: $0.002 per minute
- **Integrations**: $1.00 per 1,000 API calls
- **Screenshots**: $1.00 per 1,000 screenshots
- **AI Extractions**: $2.00 per 1,000 extractions
- **Memory Store**: $1.00 per 10,000 memories
- **Memory Recall**: $2.00 per 10,000 queries
- **Memory Search**: $0.50 per 10,000 queries
- **Memory Storage**: $0.25 per GB
### Billing
Usage is calculated daily and billed monthly. View current usage in Settings → Billing.
## API Keys
Create and manage API keys in Settings → API Keys.
### Types
- **Standard**: Full access to all resources in organization
- **Send-only**: Can only send emails, cannot read
- **Read-only**: Can view data, cannot send or modify
### Security
- Store API keys securely (environment variables)
- Never commit keys to version control
- Rotate keys regularly
- Use send-only keys for client-side code
- Revoke compromised keys immediately
## Environment Variables
Recommended environment variable naming:
```bash
# Production
STACK0_API_KEY=sk_live_...
# Development
STACK0_API_KEY=sk_test_...
# Optional: Custom API endpoint
STACK0_API_URL=https://api.stack0.dev/v1
```
## Migration from Other Services
### From Resend
Stack0's API is compatible with Resend - just change the import:
```typescript
// Before
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
// After
import { Mail as Resend } from '@stack0/sdk/mail';
const resend = new Resend({
apiKey: process.env.STACK0_API_KEY
});
```
### From SendGrid
```typescript
// Before (SendGrid)
sgMail.send({
to: 'user@example.com',
from: 'noreply@yourdomain.com',
subject: 'Hello',
html: 'Content
',
});
// After (Stack0)
stack0.mail.send({
to: 'user@example.com',
from: 'noreply@yourdomain.com',
subject: 'Hello',
html: 'Content
',
});
```
### From Cloudinary/Imgix
```typescript
// Before (Cloudinary)
cloudinary.url('sample.jpg', { width: 300, crop: 'fill' });
// After (Stack0)
const { url } = await stack0.cdn.getTransformUrl({
assetId: 'asset_abc123',
options: { width: 300, fit: 'cover' },
});
```
## Dashboard Features
Access your dashboard at https://app.stack0.dev
### Mail Dashboard
- **Overview**: Key metrics and recent emails
- **Logs**: Search and filter all sent emails
- **Templates**: Create and manage email templates
- **Domains**: Verify and manage sending domains
- **Analytics**: Delivery rates, opens, clicks, bounces
- **Webhooks**: Configure event notifications
- **API Keys**: Generate and manage access keys
### CDN Dashboard
- **Assets**: Browse, search, and manage all public CDN assets
- **Private Files**: Manage private files with secure download links
- **Bundles**: Create and manage zip download bundles
- **Folders**: Organize assets with virtual folders
- **Usage**: Monitor storage and bandwidth consumption
- **Transformations**: View transformation statistics
### Organization Management
- **Members**: Invite team members with role-based access
- **Billing**: View usage and manage payment methods
- **Settings**: Configure organization preferences
- **Activity Log**: Audit trail of all actions
## Support
- **Documentation**: https://docs.stack0.dev
- **Dashboard**: https://app.stack0.dev
- **Status**: https://status.stack0.dev
- **Email**: support@stack0.dev
## Legal
- **Terms of Service**: https://www.stack0.dev/terms
- **Privacy Policy**: https://www.stack0.dev/privacy
- **Acceptable Use**: https://www.stack0.dev/acceptable-use
## MCP Server
Stack0 provides a hosted MCP (Model Context Protocol) server that exposes all Stack0 products as tools for AI agents. Connect Claude Desktop, Cursor, Claude Code, or any MCP-compatible client to access screenshots, extraction, CDN, mail, integrations, workflows, and video tools.
### Connection Configuration
```json
{
"mcpServers": {
"stack0": {
"url": "https://api.stack0.dev/mcp/sse",
"headers": {
"Authorization": "Bearer sk_live_your_api_key"
}
}
}
}
```
For Streamable HTTP transport (recommended for Claude Code):
```json
{
"mcpServers": {
"stack0": {
"url": "https://api.stack0.dev/mcp",
"headers": {
"Authorization": "Bearer sk_live_your_api_key"
}
}
}
}
```
### Available Tools
#### Screenshots (4 tools)
- `screenshots_capture` - Capture webpage screenshot with options for format, viewport, full page
- `screenshots_capture_batch` - Capture multiple URLs in parallel
- `screenshots_get` - Get screenshot by ID
- `screenshots_list` - List screenshots with filters
#### Extraction (4 tools)
- `extraction_extract` - Extract structured data from URL using AI
- `extraction_extract_batch` - Extract from multiple URLs
- `extraction_get` - Get extraction by ID
- `extraction_list` - List extractions with filters
#### CDN (8 tools)
- `cdn_upload` - Upload file to CDN
- `cdn_get` - Get asset by ID
- `cdn_list` - List assets with filters
- `cdn_delete` - Delete asset
- `cdn_get_transform_url` - Get transformed image URL
- `cdn_transcode` - Start video transcode job
- `cdn_get_streaming_urls` - Get HLS/MP4 streaming URLs
- `cdn_create_folder` - Create folder for organization
#### Mail (6 tools)
- `mail_send` - Send single email
- `mail_send_batch` - Send batch emails (up to 100)
- `mail_list` - List sent emails
- `mail_get` - Get email by ID
- `mail_create_contact` - Create contact
- `mail_list_contacts` - List contacts
#### Integrations (6 tools)
- `integrations_list_connections` - List OAuth connections
- `integrations_crm_list_contacts` - List CRM contacts
- `integrations_crm_create_contact` - Create CRM contact
- `integrations_storage_list_files` - List storage files
- `integrations_storage_upload` - Upload to storage
- `integrations_communication_send` - Send message
#### Workflows (4 tools)
- `workflows_create` - Create AI workflow
- `workflows_run` - Run workflow
- `workflows_get_run` - Get run status
- `workflows_list` - List workflows
#### Video (4 tools)
- `video_transcode` - Start transcode job
- `video_get_job` - Get job status
- `video_merge` - Create merge job
- `video_extract_audio` - Extract audio from video
### MCP Endpoints
- `POST /mcp` - Streamable HTTP transport
- `GET /mcp/sse` - SSE transport for long-lived connections
- `POST /mcp/messages` - SSE message endpoint
- `DELETE /mcp` - Close session
- `GET /mcp/health` - Health check
- `GET /mcp/tools` - List available tools
### Authentication
All MCP endpoints require API key authentication via:
- `Authorization: Bearer sk_live_...` header
- `X-API-Key: sk_live_...` header
### Example: Claude Desktop
Add to `~/.config/claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"stack0": {
"url": "https://api.stack0.dev/mcp/sse",
"headers": {
"Authorization": "Bearer sk_live_your_api_key"
}
}
}
}
```
### Example: Using Tools
Once connected, ask Claude to:
- "Take a screenshot of example.com"
- "Extract product data from this URL"
- "Send an email to user@example.com"
- "Upload this image to CDN"
- "Run my content-pipeline workflow"
## Version
Current SDK version: v0.2.9
API version: v1
---
*Stack0 is an AI-native infrastructure platform. This document helps AI assistants understand Stack0's products and guide users in using our services effectively.*