npm package: @motosan-dev/sunoapi
Description: TypeScript SDK for [sunoapi.org](https://sunoapi.org) — a third-party API for Suno AI music generation.
Latest version: 0.2.1

# @motosan-dev/sunoapi

TypeScript SDK for [sunoapi.org](https://sunoapi.org) — a third-party API for Suno AI music generation.

Zero external dependencies, uses native `fetch`.

## Installation

```bash
npm install @motosan-dev/sunoapi
```

## Quick Start

```typescript
import { SunoApiClient } from "@motosan-dev/sunoapi";

const client = new SunoApiClient({
  apiKey: "your-api-key",
});

// Generate music and wait for completion
const result = await client.generateAndWait({
  prompt: "A chill lo-fi hip hop beat for studying",
  customMode: false,
  instrumental: true,
});

console.log(result.response?.sunoData);
```

## Initialization

```typescript
const client = new SunoApiClient({
  apiKey: "your-api-key",          // Required. Get from sunoapi.org
  baseUrl: "https://api.sunoapi.org", // Optional. Default value
  callbackUrl: "https://your-webhook.com/callback", // Optional. Global default callback URL
  pollingInterval: 5000,           // Optional. Polling interval (ms). Default: 5000
  pollingTimeout: 120000,          // Optional. Polling timeout (ms). Default: 120000
});
```

### callBackUrl Priority

1. **Method parameter** — each request can specify its own
2. **Config global setting** — `new SunoApiClient({ callbackUrl: "..." })`
3. **Built-in placeholder** — `https://example.com/callback` when none is provided

## API

### Music Generation

#### `generate(params)` → `GenerateTaskResult`

Submit a music generation task. Returns a `taskId`. Each request produces 2 tracks. **Non-blocking**.

```typescript
const { taskId } = await client.generate({
  prompt: "Japanese city pop with funky bass",
  customMode: false,
  instrumental: false,
  model: "V4",               // Optional. Default: "V4"
  style: "City Pop",         // Optional
  title: "Tokyo Nights",     // Optional
  negativeTags: "heavy metal", // Optional
  vocalGender: "f",          // Optional. "m" | "f"
  styleWeight: 0.8,          // Optional. 0.0-1.0
  callBackUrl: "https://...", // Optional
});
```

#### `generateAndWait(params)` → `MusicGenerationRecord`

Generate music and automatically poll until completion. **Blocking** — may take several minutes.

```typescript
const record = await client.generateAndWait({
  prompt: "Upbeat electronic dance music",
  customMode: false,
  instrumental: true,
});

for (const audio of record.response?.sunoData ?? []) {
  console.log(audio.audioUrl, audio.duration);
}
```

#### `extend(params)` → `GenerateTaskResult`

Extend an existing music track. **Non-blocking**.

```typescript
const { taskId } = await client.extend({
  audioId: "audio-id-from-generation",
  defaultParamFlag: true,
  model: "V4",
  prompt: "Continue with a guitar solo",
  continueAt: 30.0,          // Start extension at 30 seconds
});
```

#### `extendAndWait(params)` → `MusicGenerationRecord`

Extend music and automatically poll until completion. **Blocking**.

```typescript
const record = await client.extendAndWait({
  audioId: "audio-id",
  defaultParamFlag: false,    // Reuse original settings
  model: "V4",
});
```

#### `getMusicStatus(taskId)` → `MusicGenerationRecord`

Query music generation task status. **Non-blocking**.

```typescript
const record = await client.getMusicStatus("task-id");
console.log(record.status);
// "PENDING" | "TEXT_SUCCESS" | "FIRST_SUCCESS" | "SUCCESS"
// | "CREATE_TASK_FAILED" | "GENERATE_AUDIO_FAILED" | "SENSITIVE_WORD_ERROR"
```

### Lyrics Generation

#### `generateLyrics(params)` → `GenerateTaskResult`

Submit a lyrics generation task (text only, no audio). **Non-blocking**.

```typescript
const { taskId } = await client.generateLyrics({
  prompt: "A love song about missing someone in the rain",
});
```

#### `generateLyricsAndWait(params)` → `LyricsGenerationRecord`

Generate lyrics and automatically poll until completion. **Blocking**.

```typescript
const record = await client.generateLyricsAndWait({
  prompt: "A love song about missing someone in the rain",
});

console.log(record.response?.title);
console.log(record.response?.lyrics);
```

#### `getLyricsStatus(taskId)` → `LyricsGenerationRecord`

Query lyrics generation task status. **Non-blocking**.

```typescript
const record = await client.getLyricsStatus("task-id");
```

### WAV Conversion

#### `convertToWav(params)` → `GenerateTaskResult`

Submit a WAV conversion task for an existing track. **Non-blocking**.

```typescript
const { taskId } = await client.convertToWav({
  taskId: "original-task-id",
  audioId: "audio-id",
});
```

#### `convertToWavAndWait(params)` → `WavConversionRecord`

Convert to WAV and automatically poll until completion. **Blocking**.

```typescript
const record = await client.convertToWavAndWait({
  taskId: "original-task-id",
  audioId: "audio-id",
});

console.log(record.response?.audioWavUrl); // WAV download URL
```

#### `getWavStatus(taskId)` → `WavConversionRecord`

Query WAV conversion task status. **Non-blocking**.

```typescript
const record = await client.getWavStatus("wav-task-id");
// record.successFlag: "PENDING" | "SUCCESS" | "CREATE_TASK_FAILED" | "GENERATE_WAV_FAILED" | "CALLBACK_EXCEPTION"
```

### Credits

#### `getCredits()` → `number`

Get the remaining API credit balance. **Non-blocking**.

```typescript
const credits = await client.getCredits();
console.log(`Remaining credits: ${credits}`);
```

## Error Handling

```typescript
import {
  SunoApiError,
  SunoApiTimeoutError,
  SunoApiGenerationError,
  SunoApiValidationError,
} from "@motosan-dev/sunoapi";

try {
  await client.generateAndWait({ ... });
} catch (error) {
  if (error instanceof SunoApiValidationError) {
    // Invalid parameters — check error.message for details
  } else if (error instanceof SunoApiTimeoutError) {
    // Polling timed out — use error.taskId to check status later
  } else if (error instanceof SunoApiGenerationError) {
    // Generation failed (e.g. GENERATE_AUDIO_FAILED)
    console.log(error.status);
  } else if (error instanceof SunoApiError) {
    // API error (HTTP error or response code !== 200)
    console.log(error.code);
  }
}
```

| Error Class | Trigger |
|-------------|---------|
| `SunoApiValidationError` | Invalid request parameters |
| `SunoApiError` | API response `code !== 200`, or HTTP non-2xx |
| `SunoApiTimeoutError` | Polling exceeded `pollingTimeout`. Includes `taskId` for retry |
| `SunoApiGenerationError` | Terminal failure status (e.g. `GENERATE_AUDIO_FAILED`) |

## Types

All types are exported directly from the package:

```typescript
import type {
  SunoModel,                // "V4" | "V4_5" | "V4_5PLUS" | "V4_5ALL" | "V5"
  GenerateMusicParams,
  ExtendMusicParams,
  GenerateLyricsParams,
  ConvertToWavParams,
  GenerateTaskResult,
  MusicGenerationRecord,
  MusicGenerationStatus,
  LyricsGenerationRecord,
  WavConversionRecord,
  WavConversionStatus,
  CreditsInfo,
  SunoAudioData,
  SunoApiClientConfig,
} from "@motosan-dev/sunoapi";
```