npm package: @offload-run/sdk
Description: Official TypeScript SDK for Offload - A simple SaaS API for long-running tasks
Author: Ludovic Gueth
Latest version: 0.4.0

# @offload-run/sdk

Official TypeScript/JavaScript SDK for [Offload](https://offload.run) - A simple SaaS API for long-running tasks like video thumbnail extraction, image/video compression, and more.

## Installation

```bash
npm install @offload-run/sdk
# or
pnpm add @offload-run/sdk
# or
yarn add @offload-run/sdk
```

## Quick Start

```typescript
import { OffloadClient } from "@offload-run/sdk";

// Initialize client with your API key
const client = new OffloadClient({
  apiKey: process.env.OFFLOAD_API_KEY,
});

// Enqueue a job
const { jobId } = await client.enqueueJob({
  type: "compressImage",
  params: {
    imageUrl: "https://example.com/large-image.jpg",
    quality: 80,
    maxWidth: 1920,
  },
});

// Wait for job to complete
const job = await client.waitForJob(jobId);

console.log("Compressed image:", job.result.compressedUrl);
```

## Features

- ✅ **Type-safe** - Full TypeScript support with automatic type inference based on job type
- ✅ **Promise-based** - Modern async/await API
- ✅ **Webhook support** - Built-in webhook verification with HMAC signatures
- ✅ **Polling helper** - `waitForJob()` method for easy job completion waiting
- ✅ **Simple API** - Single `enqueueJob()` method for all job types with smart type inference
- ✅ **Error handling** - Clear error messages and types

## Usage

### Initialize Client

```typescript
import { OffloadClient } from "@offload-run/sdk";

const client = new OffloadClient({
  apiKey: "your-api-key",
  baseUrl: "https://app.offload.run/api/v1", // Optional, defaults to production
  timeout: 30000, // Optional, request timeout in ms
});
```

### Video Thumbnail Extraction

Extract a thumbnail from a video at a specific timestamp or use smart frame selection:

```typescript
// Smart frame selection (auto-detect best frame)
const { jobId } = await client.enqueueJob({
  type: "videoThumbnail",
  params: {
    videoUrl: "https://example.com/video.mp4",
    timestamp: "auto", // Automatically find the most representative frame
    quality: 90, // High quality (1-100, default: 85)
    format: "jpg",
  },
});

// Traditional timestamp-based extraction
const { jobId } = await client.enqueueJob({
  type: "videoThumbnail",
  params: {
    videoUrl: "https://example.com/video.mp4",
    timestamp: 5, // 5 seconds into the video
    width: 1280,
    height: 720,
    quality: 85, // Optional quality parameter (default: 85)
    format: "jpg",
  },
});

// Wait for completion
const job = await client.waitForJob(jobId);

console.log("Thumbnail:", job.result.thumbnailUrl);
console.log("Dimensions:", job.result.width, "x", job.result.height);
console.log("Actual timestamp used:", job.result.actualTimestamp); // Only set when using numeric timestamp
```

**Smart Frame Selection (`timestamp: "auto"`)**:
- Automatically analyzes the first 10 seconds of the video
- Selects the most representative frame
- Ideal for generating meaningful preview images

**Quality Parameter**:
- Range: 1-100 (default: 85)
- Higher values = better quality but larger file size
- Applies to JPG and WebP formats (PNG is lossless and ignores this)
- Recommended: 75-85 for web, 90-95 for high-quality previews

### Image Compression

Compress and optimize images:

```typescript
const { jobId } = await client.enqueueJob({
  type: "compressImage",
  params: {
    imageUrl: "https://example.com/image.png",
    quality: 80,
    maxWidth: 1920,
    maxHeight: 1080,
    format: "webp",
    stripMetadata: true,
  },
});

const job = await client.waitForJob(jobId);

console.log("Original size:", job.result.originalSize);
console.log("Compressed size:", job.result.compressedSize);
console.log("Compression ratio:", job.result.compressionRatio + "%");
console.log("Download:", job.result.compressedUrl);
```

### Video Compression

Compress videos with codec selection:

```typescript
const { jobId } = await client.enqueueJob({
  type: "compressVideo",
  params: {
    videoUrl: "https://example.com/video.mp4",
    quality: "medium", // 'low' | 'medium' | 'high'
    maxWidth: 1920,
    codec: "h264",
    stripAudio: false,
  },
});

const job = await client.waitForJob(jobId, {
  timeout: 300000, // 5 minutes
  pollInterval: 2000, // Poll every 2 seconds
  onProgress: (job) => {
    console.log("Status:", job.status);
  },
});

console.log("Compressed video:", job.result.compressedUrl);
```

### Extract Audio from Video

Extract audio track from a video file as MP3:

```typescript
const { jobId } = await client.enqueueJob({
  type: "extractAudio",
  params: {
    url: "https://example.com/video.mp4",
    quality: "high", // 'low' (128kbps) | 'medium' (192kbps) | 'high' (320kbps)
  },
});

const job = await client.waitForJob(jobId);

console.log("Audio URL:", job.url);
console.log("Duration:", job.duration, "seconds");
console.log("Bitrate:", job.bitrate);
console.log("Format:", job.format); // Always 'mp3'
console.log("Source audio codec:", job.source.audioCodec);
```

**Quality Presets**:
- `low`: 128 kbps - Smaller file size, suitable for voice recordings
- `medium`: 192 kbps - Good balance of quality and size (default)
- `high`: 320 kbps - Best quality, larger file size

### Video Extraction (Audio + Frames)

Extract audio and/or frames from a video in a single job. This is a combined operation that can extract the audio track and capture frames at specific points:

```typescript
// Extract audio + 10 evenly-spaced frames
const { jobId } = await client.enqueueJob({
  type: "videoExtraction",
  params: {
    url: "https://example.com/video.mp4",
    audio: {
      quality: "high", // 'low' (128kbps) | 'medium' (192kbps) | 'high' (320kbps)
    },
    frames: {
      mode: "count",
      count: 10, // Extract 10 evenly-spaced frames
      format: "jpg",
      quality: 85,
      skipBlackFrames: true, // Skip black/blank frames
    },
  },
});

const job = await client.waitForJob(jobId);

// Audio result (null if audio: false or video has no audio track)
if (job.result.audio) {
  console.log("Audio URL:", job.result.audio.url);
  console.log("Audio duration:", job.result.audio.duration);
  console.log("Audio format:", job.result.audio.format); // 'mp3'
}

// Frames array
for (const frame of job.result.frames) {
  console.log(`Frame at ${frame.timestamp}s:`, frame.url);
  console.log(`  Size: ${frame.width}x${frame.height}`);
}
```

**Frame Extraction Modes:**

```typescript
// Count mode - extract N evenly-spaced frames
frames: { mode: "count", count: 10 }

// Percentage mode - extract frames at specific percentage points
frames: { mode: "percentage", values: [0, 25, 50, 75, 100] }

// Interval mode - extract a frame every N seconds
frames: { mode: "interval", every: 5 } // One frame every 5 seconds

// Timestamps mode - extract frames at exact timestamps (in seconds)
frames: { mode: "timestamps", values: [0, 2.5, 10, 30] }
```

**Options:**

- `audio: false` — Skip audio extraction entirely
- `frames: false` — Skip frame extraction entirely
- `frames.skipBlackFrames` — Automatically skip black/blank frames
- `frames.width` / `frames.height` — Resize extracted frames
- `frames.format` — Output format: `'jpg'` | `'png'` | `'webp'`
- `frames.quality` — Quality 1-100 (for jpg/webp)

### Enqueue and Wait (One-liner)

```typescript
// Enqueue job and wait for completion in one call
const job = await client.enqueueAndWait({
  type: "compressImage",
  params: {
    imageUrl: "https://example.com/image.jpg",
    quality: 80,
  },
});

console.log("Done!", job.result.compressedUrl);
```

### Job Management

```typescript
// Get job status
const job = await client.getJob(jobId);
console.log("Status:", job.status); // 'pending' | 'processing' | 'completed' | 'failed'

// Cancel a job
await client.cancelJob(jobId);

// Wait with custom options
const job = await client.waitForJob(jobId, {
  timeout: 60000, // Max 60 seconds
  pollInterval: 1000, // Check every second
  onProgress: (job) => {
    console.log(`Job ${job.id} is ${job.status}`);
  },
});
```

## Webhooks

Instead of polling, configure webhooks to be notified when jobs complete:

### Setting up Webhooks

```typescript
// When creating a job, specify webhook URL
const { jobId } = await client.enqueueJob({
  type: "compressImage",
  params: {
    imageUrl: "https://example.com/image.jpg",
    quality: 80,
  },
  webhookUrl: "https://yourapp.com/api/webhooks/offload",
});
```

### Verifying Webhooks

```typescript
// Next.js example - app/api/webhooks/offload/route.ts
import { handleWebhook } from "@offload-run/sdk";

export async function POST(req: Request) {
  try {
    const payload = await handleWebhook(req, process.env.WEBHOOK_SECRET!);

    // Process the webhook
    console.log("Job completed:", payload.jobId);
    console.log("Result:", payload.result);

    return new Response("OK", { status: 200 });
  } catch (error) {
    console.error("Webhook error:", error);
    return new Response("Unauthorized", { status: 401 });
  }
}
```

For Express.js, use `parseWebhook()` or `webhookMiddleware()` - see SDK exports for details.

## API Reference

### OffloadClient

#### Constructor

```typescript
new OffloadClient({
  apiKey: string;
  baseUrl?: string;
  timeout?: number;
})
```

#### Methods

##### `enqueueJob(params)`

Enqueue a new job.

```typescript
await client.enqueueJob({
  type: 'videoThumbnail' | 'compressImage' | 'compressVideo' | 'compressPdf' | 'extractAudio' | 'videoExtraction',
  params: { /* job-specific params */ },
  priority?: number, // 1-10, lower = higher priority
  webhookUrl?: string,
  webhookSecret?: string,
});
```

##### `getJob(jobId)`

Get job status and result.

##### `cancelJob(jobId)`

Cancel a pending or processing job.

##### `waitForJob(jobId, options)`

Wait for job to complete by polling.

##### `enqueueAndWait(params, waitOptions)`

Enqueue a job and wait for it to complete. Returns the completed job with result.

## Error Handling

```typescript
try {
  const job = await client.waitForJob(jobId);

  if (job.status === "failed") {
    console.error("Job failed:", job.error);
  } else {
    console.log("Success:", job.result);
  }
} catch (error) {
  if (error.response?.status === 401) {
    console.error("Invalid API key");
  } else if (error.response?.status === 404) {
    console.error("Job not found");
  } else {
    console.error("Error:", error.message);
  }
}
```

## Rate Limits

Rate limits depend on your plan:

- **Free**: 2 concurrent jobs, 100 jobs/hour
- **Pro**: 10 concurrent jobs, 1000 jobs/hour
- **Enterprise**: 50 concurrent jobs, 10000 jobs/hour

## TypeScript Support

The SDK is written in TypeScript and provides full type safety:

```typescript
import type {
  Job,
  CompressImageResult,
  ExtractAudioResult,
  VideoExtractionResult,
} from "@offload-run/sdk";

const imageJob: Job<"compressImage"> = await client.getJob(imageJobId);
const audioJob: Job<"extractAudio"> = await client.getJob(audioJobId);
const extractionJob: Job<"videoExtraction"> = await client.getJob(extractionJobId);

// Results are properly typed
console.log(imageJob.compressionRatio); // Type-safe access
console.log(audioJob.bitrate); // Type-safe access
console.log(extractionJob.frames.length); // Type-safe access
```

## License

MIT

## Support

- **Documentation**: https://docs.offload.run
- **Issues**: https://github.com/ludovicgueth/offload/issues
- **Email**: support@offload.run