Skip to content

Swift Integration ​

Add content moderation to your iOS app using Vettly's REST API with URLSession.

Using React Native or Expo? See the React Native or Expo guides instead.

Quick Start ​

swift
import Foundation

struct ModerationResult: Codable {
    let flagged: Bool
    let action: String
    let categories: [String: Double]
    let policy: String
    let latencyMs: Int

    enum CodingKeys: String, CodingKey {
        case flagged, action, categories, policy
        case latencyMs = "latency_ms"
    }
}

class VettlyClient {
    private let apiKey: String
    private let baseURL = "https://api.vettly.dev/v1"

    init(apiKey: String) {
        self.apiKey = apiKey
    }

    func check(content: String, contentType: String = "text", policyId: String? = nil) async throws -> ModerationResult {
        let url = URL(string: "\(baseURL)/check")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        var body: [String: Any] = [
            "content": content,
            "contentType": contentType
        ]
        if let policyId {
            body["policyId"] = policyId
        }

        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw VettlyError.invalidResponse
        }

        if httpResponse.statusCode == 429 {
            throw VettlyError.rateLimited
        }

        if httpResponse.statusCode != 200 {
            throw VettlyError.apiError(statusCode: httpResponse.statusCode)
        }

        return try JSONDecoder().decode(ModerationResult.self, from: data)
    }
}

enum VettlyError: Error {
    case invalidResponse
    case rateLimited
    case apiError(statusCode: Int)
}

Usage:

swift
let vettly = VettlyClient(apiKey: "vettly_your_api_key")

let result = try await vettly.check(content: "Hello, world!")

if result.action == "block" {
    // Content is not allowed
} else {
    // Content is safe to post
}

Text Moderation ​

Check User Comments Before Posting ​

swift
func submitComment(_ text: String) async throws {
    let result = try await vettly.check(content: text)

    switch result.action {
    case "block":
        throw CommentError.blocked("This comment is not allowed.")
    case "flag":
        // Queue for human review, but allow posting
        try await postComment(text, needsReview: true)
    default:
        try await postComment(text, needsReview: false)
    }
}

Chat Message Moderation ​

swift
class ChatViewModel: ObservableObject {
    @Published var messages: [ChatMessage] = []
    @Published var error: String?
    @Published var isSending = false

    private let vettly = VettlyClient(apiKey: apiKey)

    func send(_ text: String) async {
        guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }

        await MainActor.run { isSending = true }

        do {
            let result = try await vettly.check(content: text)

            if result.action == "block" {
                await MainActor.run {
                    error = "Message contains inappropriate content"
                    isSending = false
                }
                return
            }

            // Send the message
            let message = ChatMessage(text: text, sender: .me)
            await MainActor.run {
                messages.append(message)
                error = nil
                isSending = false
            }
        } catch {
            await MainActor.run {
                self.error = "Failed to send message"
                isSending = false
            }
        }
    }
}

Image Moderation ​

swift
import PhotosUI
import SwiftUI

func moderateImage(_ imageData: Data) async throws -> Bool {
    let base64 = imageData.base64EncodedString()

    let result = try await vettly.check(
        content: base64,
        contentType: "image"
    )

    return result.action != "block"
}

With PhotosPicker (SwiftUI) ​

swift
struct ImageUploadView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var isChecking = false
    @State private var error: String?

    var body: some View {
        VStack {
            PhotosPicker("Select Photo", selection: $selectedItem, matching: .images)

            if isChecking {
                ProgressView("Checking image...")
            }

            if let error {
                Text(error).foregroundColor(.red)
            }
        }
        .onChange(of: selectedItem) { _, newItem in
            Task {
                guard let data = try? await newItem?.loadTransferable(type: Data.self) else { return }

                isChecking = true
                error = nil

                do {
                    let allowed = try await moderateImage(data)

                    if !allowed {
                        error = "This image is not allowed"
                    } else {
                        // Proceed with upload
                    }
                } catch {
                    self.error = "Failed to check image"
                }

                isChecking = false
            }
        }
    }
}

Report Button ​

Let users report offensive content via the appeals API.

swift
func reportContent(contentId: String, reason: String) async throws {
    let url = URL(string: "https://api.vettly.dev/v1/appeals")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body: [String: Any] = [
        "contentId": contentId,
        "reason": reason,
        "reporterId": currentUserId
    ]
    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    let (_, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 201 else {
        throw ReportError.failed
    }
}

SwiftUI Report Button ​

swift
struct ReportButton: View {
    let contentId: String
    @State private var showSheet = false
    @State private var reason = ""
    @State private var isSubmitting = false

    var body: some View {
        Button("Report") { showSheet = true }
            .foregroundColor(.red)
            .sheet(isPresented: $showSheet) {
                NavigationView {
                    Form {
                        Section("Why are you reporting this?") {
                            TextEditor(text: $reason)
                                .frame(minHeight: 100)
                        }

                        Button(isSubmitting ? "Submitting..." : "Submit Report") {
                            Task {
                                isSubmitting = true
                                try? await reportContent(contentId: contentId, reason: reason)
                                isSubmitting = false
                                showSheet = false
                            }
                        }
                        .disabled(reason.isEmpty || isSubmitting)
                    }
                    .navigationTitle("Report Content")
                    .toolbar {
                        ToolbarItem(placement: .cancellationAction) {
                            Button("Cancel") { showSheet = false }
                        }
                    }
                }
            }
    }
}

Block User ​

Add users to a blocklist via the API.

swift
func blockUser(_ blockedUserId: String) async throws {
    let url = URL(string: "https://api.vettly.dev/v1/blocklist")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body: [String: Any] = [
        "userId": currentUserId,
        "blockedUserId": blockedUserId
    ]
    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    let (_, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 201 else {
        throw BlockError.failed
    }
}

SwiftUI Block Button ​

swift
struct BlockUserButton: View {
    let userId: String
    @State private var showConfirmation = false

    var body: some View {
        Button("Block User") { showConfirmation = true }
            .foregroundColor(.red)
            .alert("Block User?", isPresented: $showConfirmation) {
                Button("Block", role: .destructive) {
                    Task {
                        try? await blockUser(userId)
                    }
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("You will no longer see content from this user.")
            }
    }
}

Error Handling ​

swift
func safeModerate(_ content: String) async -> ModerationResult? {
    do {
        return try await vettly.check(content: content)
    } catch VettlyError.rateLimited {
        // Back off and retry after delay
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return try? await vettly.check(content: content)
    } catch VettlyError.apiError(let statusCode) {
        print("Vettly API error: \(statusCode)")
        return nil // Fail open or closed based on your policy
    } catch {
        print("Network error: \(error)")
        return nil
    }
}

For production apps, moderate on your backend to keep your API key secure:

swift
// iOS app — call your own backend
func sendMessage(_ content: String) async throws {
    let url = URL(string: "https://your-api.com/messages")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body = ["content": content]
    request.httpBody = try JSONEncoder().encode(body)

    let (data, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse else { return }

    if httpResponse.statusCode == 403 {
        throw MessageError.blocked
    }
}
typescript
// Your backend (Node.js / Express)
app.post('/messages', async (req, res) => {
  const { content } = req.body

  const result = await vettly.check({ content, contentType: 'text' })

  if (result.action === 'block') {
    return res.status(403).json({ error: 'Content not allowed' })
  }

  await db.messages.create({ content, userId: req.user.id })
  res.json({ success: true })
})

Configuration ​

Store your API key securely. Never hardcode it in client-side code for production apps.

swift
// Use Xcode configuration or a secrets manager
enum Config {
    static let vettlyApiKey: String = {
        guard let key = Bundle.main.infoDictionary?["VETTLY_API_KEY"] as? String else {
            fatalError("VETTLY_API_KEY not set in Info.plist")
        }
        return key
    }()
}

let vettly = VettlyClient(apiKey: Config.vettlyApiKey)

Next Steps ​