Skip to main content

Suno

SunoMusicClient generates music from a text prompt. It talks to sunoapi.org, a third-party gateway in front of Suno, not a first-party Suno API. You get the same MusicGenerator contract as every other capability: pass a MusicRequest, get back a GenerationResult with the audio bytes.

Music generation is a long job. The client submits the request, polls for the result, and downloads the finished track once it is ready.

Credentials​

Get a SUNO_API_KEY from your sunoapi.org dashboard (not from Suno directly). The client sends it as an Authorization: Bearer header. Requests go to https://api.sunoapi.org/api/v1.

credentialsFromEnv reads the key for you and returns null when it is absent:

import 'dart:io';

import 'package:ai_abstracted/ai_abstracted.dart';

final credentials = credentialsFromEnv(ProviderId.suno, Platform.environment);

Generate a track​

Give a prompt, wait for the track, write the bytes to a file:

import 'dart:io';

import 'package:ai_abstracted/ai_abstracted.dart';

Future<void> main() async {
final credentials = credentialsFromEnv(ProviderId.suno, Platform.environment);
if (credentials == null) {
stderr.writeln('Set SUNO_API_KEY first.');
return;
}

final client = SunoMusicClient(credentials: credentials);

// This makes a real, paid request to sunoapi.org and can take a minute.
final result = await client.generateMusic(
const MusicRequest(prompt: 'upbeat synthwave with a driving bassline'),
);

// ai_abstracted never touches the filesystem; you decide where bytes go.
await File('track.mp3').writeAsBytes(result.bytes);
}

The package stays free of dart:io. Writing the file is your code, so the dart:io import lives in your app, not in the library.

How it works​

generateMusic runs three steps against the gateway:

  1. Submit the request to /generate and read the task id from the response.
  2. Poll /generate/record-info until the track reports a finished audio url. The default interval is 5 seconds and the default deadline is 5 minutes.
  3. Download the first audio url and return its bytes.

result.metadata.providerJobId holds the Suno task id, which is useful for correlating a render with the gateway's own logs.

A single generation takes a while, so pass onProgress to drive UI. Suno is a polling provider, so the stages arrive as queued, then running on each poll, then downloading, then done. See the progress guide for the full stage sequence.

More control​

MusicRequest carries the options Suno accepts. Set instrumental: true to skip vocals. title and style steer the output, and model selects a version on the gateway (the value is passed straight through to sunoapi.org, so use whatever version strings their docs list):

import 'dart:io';

import 'package:ai_abstracted/ai_abstracted.dart';

Future<void> main() async {
final credentials = credentialsFromEnv(ProviderId.suno, Platform.environment)!;
final client = SunoMusicClient(credentials: credentials);

// Real, paid request.
final result = await client.generateMusic(
const MusicRequest(
prompt: 'a slow lo-fi beat for late-night studying',
instrumental: true,
title: 'Midnight Desk',
style: 'lo-fi hip hop',
model: 'V4', // whatever sunoapi.org accepts
),
onProgress: (progress) => stdout.writeln('stage: ${progress.stage.name}'),
);

stdout.writeln('suno task: ${result.metadata.providerJobId}');
await File('midnight_desk.mp3').writeAsBytes(result.bytes);
}

Music jobs can outrun the 5-minute default on busy days. Raise the deadline (and slow the polling if you like) on the constructor:

final client = SunoMusicClient(
credentials: credentials,
pollInterval: const Duration(seconds: 10),
pollTimeout: const Duration(minutes: 8),
);

MusicRequest.seconds and MusicRequest.format are not sent to this gateway; Suno controls track length, and the mime type comes from the downloaded file. seed is not sent either, though it is echoed back on result.seedUsed.

Errors​

  • AiResponseException when the submit response has no task id, or is otherwise malformed.
  • AiTimeoutException when no finished audio url appears before pollTimeout. A track that never completes surfaces here, not as a response error.
  • Auth, rate-limit, and transient HTTP failures come back as AiAuthException, AiRateLimitException, and AiTransientException from the shared transport; transient ones are retried per your RetryPolicy.

See also​