# Mobile Integration | AudioDN Docs

> Integrate AudioDN into your iOS, Android, or Flutter app using native API calls with Client-Side API Keys.

Source: https://audiodeliverynetwork.com/docs/integration/mobile/

---

# Mobile Integration

Use ADN's API with Client-Side API Keys from your iOS, Android, or Flutter app. Play and upload with native media elements.

#### Best For

iOS, Android, and Flutter apps. Setup takes about 30 minutes.

## Playing Tracks

Create play sessions and stream audio with native media players.

Call ADN's playback API directly from your mobile app using a Client-Side API Key. Create play sessions, fetch signed variant URLs, and stream audio with native media players.

### 1\. Create a Client-Side Player API Key

Under **Settings → API Keys**, create a **Client-Side Player** key. Scope it to the variants and content your mobile app should access.

### 2\. Create a Play Session

  

```
import Foundation

struct PlaySessionRequest: Encodable {
    let collectionId: String
    let variants: [String]
    let isDownloadable: Bool
    let expiresIn: Int
    enum CodingKeys: String, CodingKey {
        case collectionId = "collection_id"; case variants
        case isDownloadable = "is_downloadable"; case expiresIn = "expires_in"
    }
}
struct PlaySessionResponse: Decodable {
    let playSessionId: String
    let tracks: [Track]
    enum CodingKeys: String, CodingKey { case playSessionId = "play_session_id"; case tracks }
    struct Track: Decodable { let id: String }
}

func createPlaySession() async throws -> (playSessionId: String, tracks: [PlaySessionResponse.Track]) {
    var request = URLRequest(url: URL(string: "https://api.audiodelivery.net/v1/play_session/collection")!)
    request.httpMethod = "POST"
    request.setValue("Bearer CLIENT-SIDE-PLAYER-API-KEY", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    request.httpBody = try encoder.encode(PlaySessionRequest(
        collectionId: "COLLECTION-ID", variants: ["hq", "lq"], isDownloadable: false, expiresIn: 3600))
    let (data, _) = try await URLSession.shared.data(for: request)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let session = try decoder.decode(PlaySessionResponse.self, from: data)
    return (session.playSessionId, session.tracks)
}
```

```
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody

@Serializable
data class PlaySessionRequest(val collection_id: String, val variants: List<String>,
    val is_downloadable: Boolean, val expires_in: Int)
@Serializable
data class PlaySessionResponse(val play_session_id: String, val tracks: List<PlaySessionTrack>) {
    @Serializable data class PlaySessionTrack(val id: String)
}

val client = OkHttpClient()
val json = Json { ignoreUnknownKeys = true }

suspend fun createPlaySession(): PlaySessionResponse = withContext(Dispatchers.IO) {
    val body = json.encodeToString(PlaySessionRequest.serializer(), PlaySessionRequest(
        "COLLECTION-ID", listOf("hq", "lq"), false, 3600)).toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/play_session/collection")
        .post(body)
        .addHeader("Authorization", "Bearer CLIENT-SIDE-PLAYER-API-KEY")
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    json.decodeFromString<PlaySessionResponse>(responseBody)
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<({String playSessionId, List<dynamic> tracks})> createPlaySession() async {
  final response = await http.post(
    Uri.parse('https://api.audiodelivery.net/v1/play_session/collection'),
    headers: {
      'Authorization': 'Bearer CLIENT-SIDE-PLAYER-API-KEY',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'collection_id': 'COLLECTION-ID',
      'variants': ['hq', 'lq'],
      'is_downloadable': false,
      'expires_in': 3600,
    }),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Play session failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  return (
    playSessionId: json['play_session_id'] as String,
    tracks: json['tracks'] as List<dynamic>,
  );
}
```

### 3\. Fetch Track Variant URLs

  

```
struct TrackVariantsResponse: Decodable {
    let variants: [Variant]
    struct Variant: Decodable {
        let url: String
        let variant: VariantIndex
        struct VariantIndex: Decodable { let id: String }
    }
}

// No auth needed — the session ID acts as a bearer token
func getAudioUrl(playSessionId: String, trackId: String) async throws -> String {
    let url = URL(string: "https://api.audiodelivery.net/v1/play/\(playSessionId)/\(trackId)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let res = try decoder.decode(TrackVariantsResponse.self, from: data)
    let hq = res.variants.first { $0.variant.id == "hq" }!
    return hq.url
}
```

```
@Serializable
data class TrackVariantsResponse(val variants: List<Variant>) {
    @Serializable data class Variant(val url: String, val variant: VariantIndex) {
        @Serializable data class VariantIndex(val id: String)
    }
}

// No auth needed — the session ID acts as a bearer token
suspend fun getAudioUrl(playSessionId: String, trackId: String): String = withContext(Dispatchers.IO) {
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/play/$playSessionId/$trackId")
        .get()
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    val res = json.decodeFromString<TrackVariantsResponse>(responseBody)
    res.variants.first { it.variant.id == "hq" }.url
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

// No auth needed — the session ID acts as a bearer token
Future<String> getAudioUrl(String playSessionId, String trackId) async {
  final response = await http.get(
    Uri.parse('https://api.audiodelivery.net/v1/play/$playSessionId/$trackId'),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Fetch track failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  final variants = json['variants'] as List<dynamic>;
  final hqVariant = variants.cast<Map<String, dynamic>>().firstWhere(
    (v) => (v['variant'] as Map<String, dynamic>)['id'] == 'hq',
    orElse: () => throw StateError('HQ variant not found'),
  );
  return hqVariant['url'] as String;
}
```

### 4\. Play with Native Player

  

```
import AVFoundation

let player = AVPlayer(url: URL(string: audioUrl)!)
player.play()
```

```
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

val player = ExoPlayer.Builder(context).build()
val mediaItem = MediaItem.fromUri(audioUrl)
player.setMediaItem(mediaItem)
player.prepare()
player.play()
```

```
import 'package:just_audio/just_audio.dart';

final player = AudioPlayer();
await player.setUrl(audioUrl);
await player.play();
// Dispose when done: player.dispose();
```

The signed URL works with any native media player. ADN handles CDN delivery and URL verification.

#### Waveform Data

ADN generates normalized RMS waveform data (320 samples) for every track automatically. The `levels` field in the track response contains the data — use it to render waveforms in your own player, or generate custom waveforms via the Audio Analysis variant type.

#### Cover Images & Theme Colors

ADN scans every uploaded track for embedded cover art and makes it available as `cover_image` in the track response (in icon/small/regular/large sizes). You can also upload a cover image separately via the API. When a cover image is present, ADN extracts a palette of colors and selects a primary `player_color` — use these to style your player UI. Alongside it, responses include `player_color_light` and `player_color_dark`, contrast-adjusted variants for drawing the color on light and dark backgrounds respectively. The full `theme` array contains all extracted colors with hex values, area, lightness, and saturation data.

## Uploading Tracks

Upload audio files from native apps using ADN's upload API.

Call ADN's upload API directly from your mobile app using a Client-Side API Key. Create upload sessions, get presigned URLs, and upload audio files — all from native code.

### 1\. Create a Client-Side Upload API Key

Login to ADN, go to **Settings → API Keys**, and create a **Client-Side Upload** key. Scope it to the collection your app should upload to.

### 2\. Create an Upload Session

  

```
import Foundation

struct UploadSessionRequest: Encodable {
    let collectionId: String
    let expiresIn: Int
    enum CodingKeys: String, CodingKey { case collectionId = "collection_id"; case expiresIn = "expires_in" }
}
struct UploadSessionResponse: Decodable {
    let uploadSession: UploadSession
    enum CodingKeys: String, CodingKey { case uploadSession = "upload_session" }
    struct UploadSession: Decodable { let id: String }
}

func createUploadSession() async throws -> String {
    var request = URLRequest(url: URL(string: "https://api.audiodelivery.net/v1/upload_session")!)
    request.httpMethod = "POST"
    request.setValue("Bearer CLIENT-SIDE-UPLOAD-API-KEY", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    request.httpBody = try encoder.encode(UploadSessionRequest(collectionId: "COLLECTION-ID", expiresIn: 3600))
    let (data, _) = try await URLSession.shared.data(for: request)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let session = try decoder.decode(UploadSessionResponse.self, from: data)
    return session.uploadSession.id
}
```

```
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody

@Serializable
data class UploadSessionRequest(val collection_id: String, val expires_in: Int)
@Serializable
data class UploadSessionResponse(val upload_session: UploadSession) {
    @Serializable data class UploadSession(val id: String)
}

val client = OkHttpClient()
val json = Json { ignoreUnknownKeys = true }

suspend fun createUploadSession(): String = withContext(Dispatchers.IO) {
    val body = json.encodeToString(UploadSessionRequest.serializer(),
        UploadSessionRequest("COLLECTION-ID", 3600)).toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/upload_session")
        .post(body)
        .addHeader("Authorization", "Bearer CLIENT-SIDE-UPLOAD-API-KEY")
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    val session = json.decodeFromString<UploadSessionResponse>(responseBody)
    session.upload_session.id
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> createUploadSession() async {
  final response = await http.post(
    Uri.parse('https://api.audiodelivery.net/v1/upload_session'),
    headers: {
      'Authorization': 'Bearer CLIENT-SIDE-UPLOAD-API-KEY',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'collection_id': 'COLLECTION-ID',
      'expires_in': 3600,
    }),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Upload session failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  return (json['upload_session'] as Map<String, dynamic>)['id'] as String;
}
```

### 3\. Get a Track Upload URL

  

```
struct TrackUploadRequest: Encodable {
    let fileName: String
    enum CodingKeys: String, CodingKey { case fileName = "file_name" }
}
struct TrackUploadResponse: Decodable {
    let trackUpload: TrackUpload
    enum CodingKeys: String, CodingKey { case trackUpload = "track_upload" }
    struct TrackUpload: Decodable { let uploadUrl: String; let method: String
        enum CodingKeys: String, CodingKey { case uploadUrl = "upload_url"; case method }
    }
}

func getTrackUploadUrl(sessionId: String) async throws -> (url: String, method: String) {
    var request = URLRequest(url: URL(string: "https://api.audiodelivery.net/v1/upload/\(sessionId)/track")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    request.httpBody = try encoder.encode(TrackUploadRequest(fileName: "recording.m4a"))
    let (data, _) = try await URLSession.shared.data(for: request)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let res = try decoder.decode(TrackUploadResponse.self, from: data)
    return (res.trackUpload.uploadUrl, res.trackUpload.method)
}
```

```
@Serializable
data class TrackUploadRequest(val file_name: String)
@Serializable
data class TrackUploadResponse(val track_upload: TrackUpload) {
    @Serializable data class TrackUpload(val upload_url: String, val method: String)
}

suspend fun getTrackUploadUrl(uploadSessionId: String): Pair<String, String> = withContext(Dispatchers.IO) {
    val body = json.encodeToString(TrackUploadRequest.serializer(), TrackUploadRequest("recording.m4a"))
        .toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/upload/$uploadSessionId/track")
        .post(body)
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    val res = json.decodeFromString<TrackUploadResponse>(responseBody)
    res.track_upload.upload_url to res.track_upload.method
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<(String, String)> getTrackUploadUrl(String uploadSessionId) async {
  final response = await http.post(
    Uri.parse('https://api.audiodelivery.net/v1/upload/$uploadSessionId/track'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'file_name': 'recording.m4a'}),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Track upload URL failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  final trackUpload = json['track_upload'] as Map<String, dynamic>;
  return (trackUpload['upload_url'] as String, trackUpload['method'] as String);
}
```

### 4\. Upload the File

  

```
// Prefer streaming for large files instead of loading into memory
func uploadAudio(fileURL: URL, uploadURL: URL, method: String) async throws {
    var request = URLRequest(url: uploadURL)
    request.httpMethod = method
    request.setValue("audio/mp4", forHTTPHeaderField: "Content-Type")
    request.httpBodyStream = InputStream(url: fileURL)
    let (_, uploadResponse) = try await URLSession.shared.data(for: request)
    let httpResponse = uploadResponse as! HTTPURLResponse
    print("Upload status: \(httpResponse.statusCode)") // 200 on success
}
```

```
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File

suspend fun uploadFile(uploadUrl: String, filePath: String): Int = withContext(Dispatchers.IO) {
    val file = File(filePath)
    val fileBody = file.asRequestBody("audio/mp4".toMediaType())
    val request = Request.Builder()
        .url(uploadUrl)
        .put(fileBody)
        .addHeader("Content-Type", "audio/mp4")
        .build()
    val response = client.newCall(request).execute()
    response.code
}
```

```
import 'dart:io';
import 'package:http/http.dart' as http;

// For large files, use body: File(path).openRead() to stream instead of loading into memory
Future<void> uploadFile(String uploadUrl, String filePath) async {
  final file = File(filePath);
  final response = await http.put(
    Uri.parse(uploadUrl),
    headers: {'Content-Type': 'audio/mp4'},
    body: await file.readAsBytes(),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Upload failed: ${response.statusCode}');
  }
}
```

Repeat steps 3 and 4 for each file — every track needs its own upload request and signed URL.

### 5\. Wait Until the Track Is Ready

Processing begins automatically after the upload. Before you play the track, wait until it is ready — poll `GET /v1/track/{track_id}` until `track.track_status_id` is `ready`, or configure a webhook (next step) to be notified instead of polling.

### 6\. Optional: Webhooks

Add a webhook under **Settings → Webhook** to be notified when a track reaches a terminal outcome (`ready`, `incomplete`, `error`, or `init_error`) and when its full file set is complete. See the [Track Processing webhook](/docs/webhooks/track-processing) docs.

#### Cover Images

ADN automatically scans uploaded tracks for embedded cover art. You can also upload a cover image separately via the Covers API. When an image is present, ADN extracts colors (available as `theme`) that can be used to style your player UI.
