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
Quick Navigation
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/swiftHeaders:
Content-Type: multipart/form-data; boundary={boundary}
Authorization: Bearer {token} // If requiredRequest 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/swiftRequest 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/swiftRequest 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/swiftNote: 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/swiftRequest 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
- Enable feature flag for internal testing (1-2 weeks)
- Roll out to 10% of users and monitor (1 week)
- Increase to 50% if no issues (1 week)
- Full rollout to 100% of users
- 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