Multimedia API Documentation

Complete REST API reference for Swift developers transitioning from Cloudinary to Convex multimedia system

Production Base URL: https://api.wecompleteapp.com/api

1. General Multimedia Upload

✅ New Convex System

Direct upload to Convex with automatic file categorization and metadata

❌ Before (Cloudinary)

// Swift - Cloudinary Upload
uploadImageToCloudinary(
    image,
    fileName: fileName,
    cloudName: "dq1qk5sdk",
    uploadPreset: "ml_default"
) { url in
    // Handle URL
    self.handleUploadedURL(url)
}

✅ After (Convex)

// Swift - Direct Convex Upload
uploadToConvexMultimedia(
    images: [image],
    userId: userId,
    category: "general"
) { result in
    // Handle upload response
    self.handleUploadResponse(result)
}

📋 API Specification

POST/users/user/{userId}/multimedia/swift
Headers:
Content-Type: multipart/form-data; boundary={boundary}
Authorization: Bearer {token} // If required
Request Body (multipart/form-data):
file: [File] // One or more files
category: string // Optional: "general", "profile", "mood", etc.
title: string // Optional: Custom title for files
description: string // Optional: File description
tags: string // Optional: Comma-separated tags
groupId: string // Optional: Group related files

📤 Swift Implementation

func uploadToConvexMultimedia(
    images: [UIImage],
    userId: String,
    category: String = "general",
    title: String? = nil,
    completion: @escaping (Result<UploadResponse, Error>) -> Void
) {
    let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/multimedia/swift")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    // Add images
    for (index, image) in images.enumerated() {
        guard let imageData = image.jpegData(compressionQuality: 0.8) else { continue }
        
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"image\(index).jpg\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add category
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"category\"\r\n\r\n".data(using: .utf8)!)
    body.append(category.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    
    // Add title if provided
    if let title = title {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"title\"\r\n\r\n".data(using: .utf8)!)
        body.append(title.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NSError(domain: "NoData", code: 1)))
            return
        }
        
        do {
            let uploadResponse = try JSONDecoder().decode(UploadResponse.self, from: data)
            completion(.success(uploadResponse))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

2. Mood Creation with Images

❌ Before (Two-Step Process)

// Step 1: Upload to Cloudinary
var uploadedURLs: [String] = []
for image in selectedImages {
    let url = try await uploadImageToCloudinary(image)
    uploadedURLs.append(url)
}

// Step 2: Create mood with URLs
let moodData = CreateMoodRequest(
    mood: "happy",
    note: "Great day!",
    imageURLs: uploadedURLs
)
let response = try await moodService.createMood(moodData)

✅ After (Single Request)

// Single request with mood data + images
let response = try await createMoodWithImages(
    mood: "happy",
    note: "Great day!",
    images: selectedImages,
    userId: userId
)
// Response includes mood data + uploaded image URLs

📋 API Specification

POST/users/user/{userId}/moods/swift
Request Body (multipart/form-data):
// Required fields
mood: string // e.g., "happy", "sad", "excited"

// Optional fields
reason: string // Reason for the mood
note: string // Additional notes
tags: string // Comma-separated tags
interests: string // Comma-separated interests
moodIntensity: number // 1-10 scale
createdAt: number // Unix timestamp
file: [File] // Images (supports multiple)
image: [File] // Alternative field name for images

📤 Swift Implementation

func createMoodWithImages(
    mood: String,
    note: String? = nil,
    reason: String? = nil,
    tags: [String] = [],
    interests: [String] = [],
    moodIntensity: Int? = nil,
    images: [UIImage],
    userId: String
) async throws -> MoodResponse {
    let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/moods/swift")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    // Add mood data
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"mood\"\r\n\r\n".data(using: .utf8)!)
    body.append(mood.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    
    if let note = note {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"note\"\r\n\r\n".data(using: .utf8)!)
        body.append(note.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if let reason = reason {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"reason\"\r\n\r\n".data(using: .utf8)!)
        body.append(reason.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if !tags.isEmpty {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"tags\"\r\n\r\n".data(using: .utf8)!)
        body.append(tags.joined(separator: ",").data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if !interests.isEmpty {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"interests\"\r\n\r\n".data(using: .utf8)!)
        body.append(interests.joined(separator: ",").data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if let intensity = moodIntensity {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"moodIntensity\"\r\n\r\n".data(using: .utf8)!)
        body.append("\(intensity)".data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add images
    for (index, image) in images.enumerated() {
        guard let imageData = image.jpegData(compressionQuality: 0.8) else { continue }
        
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"mood_image\(index).jpg\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw MoodError.invalidResponse
    }
    
    return try JSONDecoder().decode(MoodResponse.self, from: data)
}

3. Memory Creation with Images

📋 API Specification

POST/users/user/{userId}/memories/swift
Request Body (multipart/form-data):
// Required fields
title: string // Memory title

// Optional fields
description: string // Memory description
tags: string // Comma-separated tags
location: string // Location where memory was created
createdAt: number // Unix timestamp
file: [File] // Images (supports multiple)

📤 Swift Implementation

func createMemoryWithImages(
    title: String,
    description: String? = nil,
    location: String? = nil,
    tags: [String] = [],
    images: [UIImage],
    userId: String
) async throws -> MemoryResponse {
    let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/memories/swift")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    // Add memory data
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"title\"\r\n\r\n".data(using: .utf8)!)
    body.append(title.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    
    if let description = description {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"description\"\r\n\r\n".data(using: .utf8)!)
        body.append(description.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if let location = location {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"location\"\r\n\r\n".data(using: .utf8)!)
        body.append(location.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if !tags.isEmpty {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"tags\"\r\n\r\n".data(using: .utf8)!)
        body.append(tags.joined(separator: ",").data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add images
    for (index, image) in images.enumerated() {
        guard let imageData = image.jpegData(compressionQuality: 0.8) else { continue }
        
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"memory_image\(index).jpg\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw MemoryError.invalidResponse
    }
    
    return try JSONDecoder().decode(MemoryResponse.self, from: data)
}

4. Profile Picture Upload

📋 API Specification

POST/users/user/{userId}/profile-picture/swift

Note: Maximum file size: 10MB for profile pictures

Request Body (multipart/form-data):
file: File // Single image file (JPEG, PNG, WebP)

📤 Swift Implementation

func uploadProfilePicture(
    image: UIImage,
    userId: String
) async throws -> ProfilePictureResponse {
    let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/profile-picture/swift")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    guard let imageData = image.jpegData(compressionQuality: 0.8) else {
        throw ProfileError.imageConversionFailed
    }
    
    // Check file size (10MB limit)
    if imageData.count > 10 * 1024 * 1024 {
        throw ProfileError.fileTooLarge
    }
    
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"file\"; filename=\"profile_picture.jpg\"\r\n".data(using: .utf8)!)
    body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
    body.append(imageData)
    body.append("\r\n".data(using: .utf8)!)
    
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw ProfileError.uploadFailed
    }
    
    return try JSONDecoder().decode(ProfilePictureResponse.self, from: data)
}

5. Event Creation with Images

📋 API Specification

POST/users/user/{userId}/events/swift
Request Body (multipart/form-data):
// Required fields
title: string // Event title
date: string // Event date (ISO 8601 format)

// Optional fields
description: string // Event description
location: string // Event location
tags: string // Comma-separated tags
file: [File] // Images (supports multiple)

📤 Swift Implementation

func createEventWithImages(
    title: String,
    date: Date,
    description: String? = nil,
    location: String? = nil,
    tags: [String] = [],
    images: [UIImage],
    userId: String
) async throws -> EventResponse {
    let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/events/swift")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    // Add event data
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"title\"\r\n\r\n".data(using: .utf8)!)
    body.append(title.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    
    // Format date as ISO 8601
    let dateFormatter = ISO8601DateFormatter()
    let dateString = dateFormatter.string(from: date)
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"date\"\r\n\r\n".data(using: .utf8)!)
    body.append(dateString.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    
    if let description = description {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"description\"\r\n\r\n".data(using: .utf8)!)
        body.append(description.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if let location = location {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"location\"\r\n\r\n".data(using: .utf8)!)
        body.append(location.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    if !tags.isEmpty {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"tags\"\r\n\r\n".data(using: .utf8)!)
        body.append(tags.joined(separator: ",").data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add images
    for (index, image) in images.enumerated() {
        guard let imageData = image.jpegData(compressionQuality: 0.8) else { continue }
        
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"event_image\(index).jpg\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw EventError.invalidResponse
    }
    
    return try JSONDecoder().decode(EventResponse.self, from: data)
}

6. Voice Notes in Moods

🎤 New Voice Note Support

Complete voice recording and mood integration with automatic upload and playback

📋 Updated Mood API with Voice Notes

New Fields Added:
// Request body additions
voiceNote: File // Audio file (MP3, M4A, WAV, up to 25MB)
voiceNoteDuration: number // Duration in seconds

// Response additions  
{
  "mood": {
    "voiceNote": "https://api.wecompleteapp.com/.../voice_note_url",
    "voiceNoteDuration": 45.6,
    // ... other mood fields
  }
}

🎙️ Swift Voice Recording Implementation

import AVFoundation

class VoiceRecordingManager: NSObject, ObservableObject {
    private var audioRecorder: AVAudioRecorder?
    private var audioPlayer: AVAudioPlayer?
    
    @Published var isRecording = false
    @Published var isPlaying = false
    @Published var recordingDuration: TimeInterval = 0
    @Published var recordedFileURL: URL?
    
    private var recordingTimer: Timer?
    
    override init() {
        super.init()
        setupAudioSession()
    }
    
    private func setupAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        
        do {
            try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
            try audioSession.setActive(true)
        } catch {
            print("Failed to setup audio session: \(error)")
        }
    }
    
    func startRecording() {
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let audioFilename = documentsPath.appendingPathComponent("mood_voice_\(Date().timeIntervalSince1970).m4a")
        
        let settings = [
            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
            AVSampleRateKey: 44100,
            AVNumberOfChannelsKey: 2,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
        ]
        
        do {
            audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
            audioRecorder?.delegate = self
            audioRecorder?.record()
            
            isRecording = true
            recordedFileURL = audioFilename
            recordingDuration = 0
            
            recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
                self.recordingDuration = self.audioRecorder?.currentTime ?? 0
            }
        } catch {
            print("Recording failed: \(error)")
        }
    }
    
    func stopRecording() {
        audioRecorder?.stop()
        recordingTimer?.invalidate()
        recordingTimer = nil
        isRecording = false
    }
    
    func playRecording() {
        guard let url = recordedFileURL else { return }
        
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer?.delegate = self
            audioPlayer?.play()
            isPlaying = true
        } catch {
            print("Playback failed: \(error)")
        }
    }
    
    func stopPlaying() {
        audioPlayer?.stop()
        isPlaying = false
    }
}

extension VoiceRecordingManager: AVAudioRecorderDelegate, AVAudioPlayerDelegate {
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        isRecording = false
    }
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        isPlaying = false
    }
}

📱 SwiftUI Voice Note Interface

struct VoiceNoteRecorderView: View {
    @StateObject private var voiceRecorder = VoiceRecordingManager()
    
    let onVoiceNoteAdded: (URL, TimeInterval) -> Void
    
    var body: some View {
        VStack(spacing: 16) {
            if voiceRecorder.recordedFileURL != nil {
                // Recorded voice note preview
                VStack(spacing: 12) {
                    HStack {
                        Image(systemName: "waveform")
                            .foregroundColor(.blue)
                        
                        Text("Voice Note (\(Int(voiceRecorder.recordingDuration))s)")
                            .font(.subheadline)
                        
                        Spacer()
                        
                        Button(action: voiceRecorder.isPlaying ? voiceRecorder.stopPlaying : voiceRecorder.playRecording) {
                            Image(systemName: voiceRecorder.isPlaying ? "stop.fill" : "play.fill")
                                .foregroundColor(.blue)
                        }
                        
                        Button(action: { voiceRecorder.recordedFileURL = nil }) {
                            Image(systemName: "trash")
                                .foregroundColor(.red)
                        }
                    }
                    .padding()
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(8)
                    
                    Button("Use This Voice Note") {
                        if let url = voiceRecorder.recordedFileURL {
                            onVoiceNoteAdded(url, voiceRecorder.recordingDuration)
                        }
                    }
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(8)
                }
            } else {
                // Recording interface
                VStack(spacing: 12) {
                    Button(action: {
                        if voiceRecorder.isRecording {
                            voiceRecorder.stopRecording()
                        } else {
                            voiceRecorder.startRecording()
                        }
                    }) {
                        VStack {
                            Image(systemName: voiceRecorder.isRecording ? "stop.circle.fill" : "mic.circle.fill")
                                .font(.system(size: 64))
                                .foregroundColor(voiceRecorder.isRecording ? .red : .blue)
                            
                            Text(voiceRecorder.isRecording ? "Tap to Stop" : "Tap to Record")
                                .font(.headline)
                        }
                    }
                    
                    if voiceRecorder.isRecording {
                        Text("\(Int(voiceRecorder.recordingDuration))s")
                            .font(.title2)
                            .foregroundColor(.red)
                    }
                }
            }
        }
        .padding()
    }
}

🚀 Complete Mood Creation with Voice Notes

func createMoodWithVoiceAndImages(
    mood: String,
    note: String? = nil,
    voiceNoteURL: URL? = nil,
    voiceNoteDuration: TimeInterval? = nil,
    images: [UIImage] = [],
    userId: String
) async throws -> MoodResponse {
    let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/moods/swift")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    // Add mood data
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"mood\"\r\n\r\n".data(using: .utf8)!)
    body.append(mood.data(using: .utf8)!)
    body.append("\r\n".data(using: .utf8)!)
    
    if let note = note {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"note\"\r\n\r\n".data(using: .utf8)!)
        body.append(note.data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add voice note duration
    if let duration = voiceNoteDuration {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"voiceNoteDuration\"\r\n\r\n".data(using: .utf8)!)
        body.append("\(duration)".data(using: .utf8)!)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add voice note file
    if let voiceURL = voiceNoteURL {
        let voiceData = try Data(contentsOf: voiceURL)
        let filename = voiceURL.lastPathComponent
        
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"voiceNote\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: audio/m4a\r\n\r\n".data(using: .utf8)!)
        body.append(voiceData)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    // Add images
    for (index, image) in images.enumerated() {
        guard let imageData = image.jpegData(compressionQuality: 0.8) else { continue }
        
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"mood_image\(index).jpg\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
    }
    
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw MoodError.invalidResponse
    }
    
    return try JSONDecoder().decode(MoodResponse.self, from: data)
}

// Updated Response Models with Voice Notes
struct MoodData: Codable {
    let id: String
    let mood: String
    let note: String?
    let voiceNote: String?           // NEW: Voice note URL
    let voiceNoteDuration: Double?   // NEW: Duration in seconds
    let reason: String?
    let tags: [String]
    let interests: [String]
    let moodIntensity: Int?
    let createdAt: Int
}

7. Response Models

📦 Swift Model Definitions

// General Upload Response
struct UploadResponse: Codable {
    let success: Bool
    let files: [UploadedFile]
    let errors: [String]?
    let groupId: String?
    let count: Int
}

struct UploadedFile: Codable {
    let data: FileData
}

struct FileData: Codable {
    let url: String
    let key: String
    let appUrl: String
    let name: String
    let size: Int
    let type: String
    let fileId: String
}

// Profile Picture Response
struct ProfilePictureResponse: Codable {
    let success: Bool
    let message: String
    let imageUrl: String
}

// Mood Response (existing model - no changes needed)
struct MoodResponse: Codable {
    let success: Bool
    let mood: MoodData
    let imageUrls: [String]?
}

struct MoodData: Codable {
    let id: String
    let mood: String
    let note: String?
    let voiceNote: String?           // NEW: Voice note URL
    let voiceNoteDuration: Double?   // NEW: Duration in seconds
    let reason: String?
    let tags: [String]
    let interests: [String]
    let moodIntensity: Int?
    let createdAt: Int
}

// Memory Response (existing model - no changes needed)
struct MemoryResponse: Codable {
    let success: Bool
    let memory: MemoryData
    let imageUrls: [String]?
}

struct MemoryData: Codable {
    let id: String
    let title: String
    let description: String?
    let location: String?
    let tags: [String]
    let createdAt: Int
}

// Event Response
struct EventResponse: Codable {
    let success: Bool
    let event: EventData
    let imageUrls: [String]?
}

struct EventData: Codable {
    let id: String
    let title: String
    let description: String?
    let date: String
    let location: String?
    let tags: [String]
    let createdAt: Int
}

📋 Sample Response JSON

// Upload Response
{
  "success": true,
  "files": [
    {
      "data": {
        "url": "https://api.wecompleteapp.com/api/users/user/123/multimedia/files/abc123",
        "key": "abc123",
        "appUrl": "https://api.wecompleteapp.com/api/users/user/123/multimedia/files/abc123",
        "name": "mood_image0.jpg",
        "size": 1234567,
        "type": "image/jpeg",
        "fileId": "abc123"
      }
    }
  ],
  "errors": null,
  "groupId": "mood_group_xyz",
  "count": 1
}

// Mood Response
{
  "success": true,
  "mood": {
    "id": "mood_123",
    "mood": "happy",
    "note": "Great day!",
    "reason": "Good weather",
    "tags": ["sunny", "productive"],
    "interests": ["photography"],
    "moodIntensity": 8,
    "createdAt": 1640995200000
  },
  "imageUrls": [
    "https://api.wecompleteapp.com/api/users/user/123/multimedia/files/abc123"
  ]
}

// Profile Picture Response
{
  "success": true,
  "message": "Profile picture updated successfully",
  "imageUrl": "https://api.wecompleteapp.com/api/users/user/123/multimedia/files/def456"
}

8. Migration Guide

🚀 Phase 1: Feature Flag Implementation

// Add feature flag to your app
struct FeatureFlags {
    static let useConvexMultimedia = false // Start with false
}

// Wrapper function for gradual transition
func createMoodSafely(mood: AddMoodStruct, images: [UIImage]) async throws -> MoodResponse {
    if FeatureFlags.useConvexMultimedia {
        // Use new Convex system
        return try await createMoodWithImages(mood: mood, images: images, userId: currentUserId)
    } else {
        // Use existing Cloudinary system
        return try await createMoodWithCloudinaryImages(mood: mood, images: images)
    }
}

✅ Phase 2: Gradual Rollout

  1. Enable feature flag for internal testing (1-2 weeks)
  2. Roll out to 10% of users and monitor (1 week)
  3. Increase to 50% if no issues (1 week)
  4. Full rollout to 100% of users
  5. Remove old Cloudinary code after 2 weeks

⚠️ Rollback Plan

If issues arise, simply change the feature flag:

// Immediate rollback
struct FeatureFlags {
    static let useConvexMultimedia = false // Set back to false
}

9. Error Handling

🚨 Common Error Responses

// Error Response Format
{
  "error": "Error message",
  "details": "Detailed error information",
  "code": 400
}

// Common Error Codes:
// 400 - Bad Request (invalid data, missing fields)
// 401 - Unauthorized (invalid token)
// 413 - Payload Too Large (file size exceeded)
// 422 - Unprocessable Entity (invalid file type)
// 500 - Internal Server Error

🛠 Swift Error Handling Implementation

enum APIError: Error {
    case invalidResponse
    case fileTooLarge
    case invalidFileType
    case networkError(String)
    case serverError(String)
}

extension APIError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .invalidResponse:
            return "Invalid server response"
        case .fileTooLarge:
            return "File size exceeds limit"
        case .invalidFileType:
            return "Invalid file type"
        case .networkError(let message):
            return "Network error: \(message)"
        case .serverError(let message):
            return "Server error: \(message)"
        }
    }
}

// Error handling in upload functions
func handleUploadError(_ error: Error) {
    if let apiError = error as? APIError {
        switch apiError {
        case .fileTooLarge:
            showAlert("File too large. Please select a smaller image.")
        case .invalidFileType:
            showAlert("Invalid file type. Please select a JPEG or PNG image.")
        case .networkError:
            showAlert("Network error. Please check your connection.")
        case .serverError(let message):
            showAlert("Upload failed: \(message)")
        default:
            showAlert("Upload failed. Please try again.")
        }
    }
}

⚠️ File Size Limits

  • Profile Pictures: 10MB maximum
  • General Uploads: 50MB maximum per file
  • Bulk Uploads: 10 files maximum per request
  • Supported Formats: JPEG, PNG, WebP, GIF, MP4, MOV

📚 For more information, see the complete migration guide

Production API Base URL: https://api.wecompleteapp.com/api