RoxyAPI

Menu

Tarot API Integration Guide: Add Card Readings to Your iOS/Android App

17 min read
By James Chen
TarotAPI IntegrationiOS DevelopmentAndroid Development

Complete technical guide for integrating tarot readings into mobile apps. Learn API authentication, endpoint structure, response handling, error management, and real-world code examples for iOS and Android.

Tarot API Integration Guide: Add Card Readings to Your iOS/Android App

Want to add mystical tarot reading features to your mobile app without dealing with card databases, interpretation logic, or complex algorithms? The RoxyAPI Tarot API provides everything you need: 78 professionally illustrated cards, detailed upright and reversed meanings, multiple spread types (Celtic Cross, three-card, love, career), and even reproducible seeded readings for consistent experiences.

This technical guide walks through complete API integration for both iOS and Android platforms. You'll learn authentication patterns, endpoint structures, response handling, error management, and production-ready code patterns that you can ship today.

What You'll Build

By the end of this guide, you'll integrate:

  • Card Database Access: Query all 78 cards with filtering by arcana, suit, and number
  • Daily Readings: Reproducible daily cards based on date and user ID
  • Custom Spreads: Three-card readings, Celtic Cross (10 cards), love spreads, career spreads
  • Yes/No Answers: Quick oracle-style guidance for binary questions
  • Seeded Draws: Deterministic card selection for sharing or testing

All endpoints return clean JSON with card metadata, keywords, interpretations, and high-quality image URLs.

Prerequisites

iOS Requirements:

  • Xcode 14+ with Swift 5.7+
  • iOS 15.0+ deployment target
  • URLSession or Alamofire for networking

Android Requirements:

  • Android Studio Hedgehog+ with Kotlin 1.9+
  • minSdk 24 (Android 7.0) or higher
  • Retrofit or OkHttp for API calls

API Access:

  1. Visit RoxyAPI Pricing
  2. Choose a plan
  3. Complete checkout - your API key displays immediately (save it!)
  4. Test your key: curl -H "X-API-Key: YOUR_KEY" https://roxyapi.com/api/v2/tarot/cards

API Fundamentals

Authentication Pattern

All requests require an X-API-Key header. The API uses HMAC-signed keys (not JWTs) for blazing-fast verification:

curl -H "X-API-Key: sub_abc123.signature456" \
     https://roxyapi.com/api/v2/tarot/cards

Never hardcode keys in your app code. Use:

  • iOS: Xcode configuration files or Keychain
  • Android: BuildConfig fields from gradle.properties or Android Keystore

Base URL

https://roxyapi.com/api

All tarot endpoints start with /tarot prefix.

Rate Limits

Rate limits are monthly only (no per-second throttling):

  • X-RateLimit-Limit: Total requests per month from your plan
  • X-RateLimit-Used: Requests consumed this month
  • X-RateLimit-Remaining: Requests left this month

Check response headers to display usage to users:

iOS Example:

if let limit = response.value(forHTTPHeaderField: "X-RateLimit-Limit"),
   let used = response.value(forHTTPHeaderField: "X-RateLimit-Used") {
    print("API Usage: \(used)/\(limit) requests this month")
}

Android Example:

val limit = response.headers()["X-RateLimit-Limit"]
val used = response.headers()["X-RateLimit-Used"]
println("API Usage: $used/$limit requests this month")

Response Format

All successful responses return data directly (no { success: true, data: {...} } wrapper):

{
  "id": "fool",
  "name": "The Fool",
  "arcana": "major",
  "number": 0,
  "keywords": ["Beginnings", "innocence", "spontaneity"],
  "imageUrl": "https://roxyapi.com/img/tarot/major/fool.jpg"
}

Errors return clean error objects with appropriate HTTP status codes:

{
  "error": "Invalid API key"
}

Core Endpoints

1. List All Cards

Get the complete 78-card tarot deck with optional filtering:

Endpoint:

GET /tarot/cards

Query Parameters:

  • arcana (optional): Filter by major or minor
  • suit (optional): Filter minor arcana by cups, wands, swords, pentacles
  • number (optional): Filter by card number (1-14 for minor, where Ace=1, Page=11, Knight=12, Queen=13, King=14)

iOS Implementation:

struct TarotCard: Codable {
    let id: String
    let name: String
    let arcana: String
    let suit: String?
    let number: Int?
    let imageUrl: String
}

class TarotAPIService {
    private let baseURL = "https://roxyapi.com/api"
    private let apiKey: String
    
    init(apiKey: String) {
        self.apiKey = apiKey
    }
    
    func fetchCards(
        arcana: String? = nil,
        suit: String? = nil,
        number: Int? = nil
    ) async throws -> [TarotCard] {
        var components = URLComponents(string: "\(baseURL)/tarot/cards")!
        var queryItems: [URLQueryItem] = []
        
        if let arcana = arcana {
            queryItems.append(URLQueryItem(name: "arcana", value: arcana))
        }
        if let suit = suit {
            queryItems.append(URLQueryItem(name: "suit", value: suit))
        }
        if let number = number {
            queryItems.append(URLQueryItem(name: "number", value: "\(number)"))
        }
        
        if !queryItems.isEmpty {
            components.queryItems = queryItems
        }
        
        var request = URLRequest(url: components.url!)
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw TarotAPIError.invalidResponse
        }
        
        return try JSONDecoder().decode([TarotCard].self, from: data)
    }
}

// Usage
let service = TarotAPIService(apiKey: "YOUR_API_KEY")
let majorArcana = try await service.fetchCards(arcana: "major")
let cupsCards = try await service.fetchCards(suit: "cups")

Android Implementation:

data class TarotCard(
    val id: String,
    val name: String,
    val arcana: String,
    val suit: String?,
    val number: Int?,
    val imageUrl: String
)

interface TarotAPI {
    @GET("/api/tarot/cards")
    suspend fun getCards(
        @Query("arcana") arcana: String? = null,
        @Query("suit") suit: String? = null,
        @Query("number") number: Int? = null,
        @Header("X-API-Key") apiKey: String
    ): List<TarotCard>
}

class TarotRepository(private val apiKey: String) {
    private val api: TarotAPI = Retrofit.Builder()
        .baseUrl("https://roxyapi.com")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(TarotAPI::class.java)
    
    suspend fun fetchCards(
        arcana: String? = null,
        suit: String? = null,
        number: Int? = null
    ): Result<List<TarotCard>> = try {
        val cards = api.getCards(arcana, suit, number, apiKey)
        Result.success(cards)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

// Usage in ViewModel
class TarotViewModel : ViewModel() {
    private val repository = TarotRepository(BuildConfig.TAROT_API_KEY)
    
    fun loadMajorArcana() {
        viewModelScope.launch {
            repository.fetchCards(arcana = "major")
                .onSuccess { cards -> _cards.value = cards }
                .onFailure { error -> _error.value = error.message }
        }
    }
}

2. Get Single Card Details

Retrieve detailed information for a specific card including upright/reversed keywords and full interpretations:

Endpoint:

GET /tarot/cards/{id}

Path Parameters:

  • id: Card identifier in kebab-case (e.g., fool, ace-of-cups, queen-of-swords)

Response Example:

{
  "id": "fool",
  "name": "The Fool",
  "arcana": "major",
  "number": 0,
  "keywords": {
    "upright": ["Beginnings", "innocence", "spontaneity", "free spirit"],
    "reversed": ["Holding back", "recklessness", "risk-taking"]
  },
  "upright": "The Fool is a card of new beginnings, opportunity and potential...",
  "reversed": "When The Fool appears reversed, you may be holding back...",
  "imageUrl": "https://roxyapi.com/img/tarot/major/fool.jpg"
}

iOS Implementation:

struct CardDetail: Codable {
    let id: String
    let name: String
    let arcana: String
    let number: Int?
    let suit: String?
    let keywords: Keywords
    let upright: String
    let reversed: String
    let imageUrl: String
    
    struct Keywords: Codable {
        let upright: [String]
        let reversed: [String]
    }
}

extension TarotAPIService {
    func fetchCardDetail(id: String) async throws -> CardDetail {
        let url = URL(string: "\(baseURL)/tarot/cards/\(id)")!
        var request = URLRequest(url: url)
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw TarotAPIError.invalidResponse
        }
        
        return try JSONDecoder().decode(CardDetail.self, from: data)
    }
}

Android Implementation:

data class CardDetail(
    val id: String,
    val name: String,
    val arcana: String,
    val number: Int?,
    val suit: String?,
    val keywords: Keywords,
    val upright: String,
    val reversed: String,
    val imageUrl: String
) {
    data class Keywords(
        val upright: List<String>,
        val reversed: List<String>
    )
}

interface TarotAPI {
    // ... previous methods
    
    @GET("/api/tarot/cards/{id}")
    suspend fun getCardDetail(
        @Path("id") id: String,
        @Header("X-API-Key") apiKey: String
    ): CardDetail
}

3. Draw Random Cards

Draw one or more random cards with control over reversals and duplicates:

Endpoint:

POST /tarot/draw

Request Body:

{
  "count": 3,
  "allowReversals": true,
  "allowDuplicates": false,
  "seed": "optional-seed-for-reproducibility"
}

Parameters:

  • count (required): Number of cards (1-78)
  • allowReversals (optional): Allow reversed cards, default true
  • allowDuplicates (optional): Allow same card multiple times, default false
  • seed (optional): String seed for reproducible draws

Response:

{
  "cards": [
    {
      "id": "tower",
      "name": "The Tower",
      "arcana": "major",
      "number": 16,
      "position": 1,
      "reversed": false,
      "keywords": ["Sudden change", "upheaval", "chaos"],
      "meaning": "When The Tower appears...",
      "imageUrl": "https://roxyapi.com/img/tarot/major/tower.jpg"
    }
  ],
  "count": 1,
  "seed": "optional-seed-for-reproducibility"
}

iOS Implementation:

struct DrawRequest: Codable {
    let count: Int
    let allowReversals: Bool
    let allowDuplicates: Bool
    let seed: String?
}

struct DrawnCard: Codable {
    let id: String
    let name: String
    let arcana: String
    let suit: String?
    let number: Int?
    let position: Int
    let reversed: Bool
    let keywords: [String]
    let meaning: String
    let imageUrl: String
}

struct DrawResponse: Codable {
    let cards: [DrawnCard]
    let count: Int
    let seed: String?
}

extension TarotAPIService {
    func drawCards(
        count: Int,
        allowReversals: Bool = true,
        allowDuplicates: Bool = false,
        seed: String? = nil
    ) async throws -> DrawResponse {
        let url = URL(string: "\(baseURL)/tarot/draw")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = DrawRequest(
            count: count,
            allowReversals: allowReversals,
            allowDuplicates: allowDuplicates,
            seed: seed
        )
        request.httpBody = try JSONEncoder().encode(body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw TarotAPIError.invalidResponse
        }
        
        return try JSONDecoder().decode(DrawResponse.self, from: data)
    }
}

Android Implementation:

data class DrawRequest(
    val count: Int,
    val allowReversals: Boolean = true,
    val allowDuplicates: Boolean = false,
    val seed: String? = null
)

data class DrawnCard(
    val id: String,
    val name: String,
    val arcana: String,
    val suit: String?,
    val number: Int?,
    val position: Int,
    val reversed: Boolean,
    val keywords: List<String>,
    val meaning: String,
    val imageUrl: String
)

data class DrawResponse(
    val cards: List<DrawnCard>,
    val count: Int,
    val seed: String?
)

interface TarotAPI {
    // ... previous methods
    
    @POST("/api/tarot/draw")
    suspend fun drawCards(
        @Body request: DrawRequest,
        @Header("X-API-Key") apiKey: String
    ): DrawResponse
}

4. Celtic Cross Spread

The most comprehensive 10-card spread revealing past influences, present situation, future possibilities, and final outcome:

Endpoint:

POST /tarot/spreads/celtic-cross

Request Body:

{
  "question": "What opportunities await me in my career?",
  "seed": "optional-seed"
}

Response Structure:

{
  "spread": "Celtic Cross",
  "question": "What opportunities await me in my career?",
  "seed": "optional-seed",
  "positions": [
    {
      "position": 1,
      "name": "Present Situation",
      "interpretation": "Represents what is happening at present...",
      "card": {
        "id": "nine-of-cups",
        "name": "Nine of Cups",
        "position": 1,
        "reversed": false,
        "keywords": ["Contentment", "satisfaction", "gratitude"],
        "meaning": "You know those moments when...",
        "imageUrl": "..."
      }
    }
  ]
}

Position Meanings:

  1. Present Situation - Current state and how you perceive it
  2. Challenge - Immediate obstacle requiring attention
  3. Distant Past - Events that led to present situation
  4. Near Future - What's likely within weeks/months
  5. Above - Your goal, aspiration, conscious mind
  6. Below - Subconscious realm, underlying feelings
  7. Advice - Guidance on how to proceed
  8. External Influences - Factors beyond your control
  9. Hopes and Fears - Intertwined desires and anxieties
  10. Final Outcome - Where situation is headed

iOS Implementation:

struct CelticCrossRequest: Codable {
    let question: String?
    let seed: String?
}

struct SpreadPosition: Codable {
    let position: Int
    let name: String
    let interpretation: String
    let card: DrawnCard
}

struct CelticCrossResponse: Codable {
    let spread: String
    let question: String?
    let seed: String?
    let positions: [SpreadPosition]
    let summary: String
}

extension TarotAPIService {
    func celticCross(question: String? = nil, seed: String? = nil) async throws -> CelticCrossResponse {
        let url = URL(string: "\(baseURL)/tarot/spreads/celtic-cross")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = CelticCrossRequest(question: question, seed: seed)
        request.httpBody = try JSONEncoder().encode(body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw TarotAPIError.invalidResponse
        }
        
        return try JSONDecoder().decode(CelticCrossResponse.self, from: data)
    }
}

5. Daily Tarot Reading

Get a deterministic daily card based on date and optional user ID:

Endpoint:

POST /tarot/daily

Request Body:

{
  "date": "2025-12-27",
  "userId": "user123"
}

Parameters:

  • date (optional): ISO date string (YYYY-MM-DD), defaults to today UTC
  • userId (optional): User identifier for personalized daily cards (same user + date = same card)

Response:

{
  "date": "2025-12-27",
  "userId": "user123",
  "card": {
    "id": "star",
    "name": "The Star",
    "reversed": false,
    "keywords": ["Hope", "faith", "purpose", "renewal"],
    "meaning": "The Star brings renewed hope...",
    "imageUrl": "..."
  },
  "message": "Your daily guidance for December 27, 2025"
}

iOS Implementation:

struct DailyRequest: Codable {
    let date: String?
    let userId: String?
}

struct DailyResponse: Codable {
    let date: String
    let seed: String
    let card: DrawnCard
    let dailyMessage: String
}

extension TarotAPIService {
    func dailyReading(date: String? = nil, userId: String? = nil) async throws -> DailyResponse {
        let url = URL(string: "\(baseURL)/tarot/daily")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = DailyRequest(date: date, userId: userId)
        request.httpBody = try JSONEncoder().encode(body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw TarotAPIError.invalidResponse
        }
        
        return try JSONDecoder().decode(DailyResponse.self, from: data)
    }
}

6. Yes/No Reading

Quick oracle-style guidance for binary questions:

Endpoint:

POST /tarot/yes-no

Request Body:

{
  "question": "Should I accept the job offer?",
  "seed": "optional-seed"
}

Response:

{
  "question": "Should I accept the job offer?",
  "answer": "yes",
  "confidence": "strong",
  "card": {
    "id": "sun",
    "name": "The Sun",
    "reversed": false,
    "keywords": ["Success", "radiance", "abundance"],
    "meaning": "...",
    "imageUrl": "..."
  },
  "interpretation": "The Sun shines brightly on this question..."
}

Answer Types:

  • yes - Strong positive indication
  • no - Clear negative indication
  • maybe - Unclear, requires more reflection

Confidence Levels:

  • strong - Major arcana upright or highly positive card
  • moderate - Minor arcana or neutral card
  • weak - Reversed card or ambiguous meaning

Error Handling

HTTP Status Codes

  • 200 - Success
  • 400 - Bad request (invalid parameters, malformed JSON)
  • 401 - Unauthorized (missing or invalid API key)
  • 429 - Rate limit exceeded (monthly quota exhausted)
  • 500 - Server error (contact support if persistent)

Error Response Format

{
  "error": "Human-readable error message"
}

iOS Error Handling Pattern

enum TarotAPIError: Error {
    case invalidResponse
    case unauthorized
    case rateLimitExceeded
    case serverError
    case decodingError
    case networkError(Error)
    case unknown(String)
}

extension TarotAPIService {
    private func handleResponse(_ response: HTTPURLResponse, data: Data) throws {
        switch response.statusCode {
        case 200...299:
            return
        case 401:
            throw TarotAPIError.unauthorized
        case 429:
            throw TarotAPIError.rateLimitExceeded
        case 500...599:
            throw TarotAPIError.serverError
        default:
            if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
                throw TarotAPIError.unknown(errorResponse.error)
            }
            throw TarotAPIError.invalidResponse
        }
    }
}

struct ErrorResponse: Codable {
    let error: String
}

// Usage in ViewModel
func loadDailyCard() {
    Task {
        do {
            let daily = try await service.dailyReading()
            self.dailyCard = daily.card
        } catch TarotAPIError.unauthorized {
            self.error = "Invalid API key. Please check your subscription."
        } catch TarotAPIError.rateLimitExceeded {
            self.error = "Monthly limit reached. Upgrade your plan."
        } catch {
            self.error = "Failed to load daily card. Try again."
        }
    }
}

Android Error Handling Pattern

sealed class TarotResult<out T> {
    data class Success<T>(val data: T) : TarotResult<T>()
    sealed class Error : TarotResult<Nothing>() {
        data class Network(val exception: Exception) : Error()
        data object Unauthorized : Error()
        data object RateLimitExceeded : Error()
        data class Server(val message: String) : Error()
    }
}

class TarotRepository(private val apiKey: String) {
    suspend fun fetchDailyReading(
        date: String? = null,
        userId: String? = null
    ): TarotResult<DailyResponse> = try {
        val response = api.getDailyReading(
            DailyRequest(date, userId),
            apiKey
        )
        TarotResult.Success(response)
    } catch (e: HttpException) {
        when (e.code()) {
            401 -> TarotResult.Error.Unauthorized
            429 -> TarotResult.Error.RateLimitExceeded
            in 500..599 -> TarotResult.Error.Server(e.message ?: "Server error")
            else -> TarotResult.Error.Network(e)
        }
    } catch (e: Exception) {
        TarotResult.Error.Network(e)
    }
}

// Usage in ViewModel
fun loadDailyCard() {
    viewModelScope.launch {
        when (val result = repository.fetchDailyReading()) {
            is TarotResult.Success -> _dailyCard.value = result.data.card
            is TarotResult.Error.Unauthorized -> _error.value = "Invalid API key"
            is TarotResult.Error.RateLimitExceeded -> _error.value = "Monthly limit reached"
            is TarotResult.Error.Server -> _error.value = "Server error"
            is TarotResult.Error.Network -> _error.value = "Network error"
        }
    }
}

Production Best Practices

1. Image Caching

All card images are served from CDN (https://roxyapi.com/img/tarot/). Implement aggressive caching:

iOS (Kingfisher):

import Kingfisher

ImageView.kf.setImage(
    with: URL(string: card.imageUrl),
    options: [
        .transition(.fade(0.2)),
        .cacheMemoryOnly(false),
        .diskCacheExpiration(.days(30))
    ]
)

Android (Coil):

ImageView.load(card.imageUrl) {
    crossfade(true)
    memoryCachePolicy(CachePolicy.ENABLED)
    diskCachePolicy(CachePolicy.ENABLED)
}

2. Offline Support

Cache API responses locally for offline access:

iOS (SwiftData):

@Model
class CachedCard {
    @Attribute(.unique) var id: String
    var name: String
    var data: Data // JSON serialized
    var cachedAt: Date
    
    init(id: String, name: String, data: Data) {
        self.id = id
        self.name = name
        self.data = data
        self.cachedAt = Date()
    }
}

Android (Room):

@Entity(tableName = "cached_cards")
data class CachedCard(
    @PrimaryKey val id: String,
    val name: String,
    val jsonData: String,
    val cachedAt: Long
)

@Dao
interface CardDao {
    @Query("SELECT * FROM cached_cards WHERE id = :id")
    suspend fun getCard(id: String): CachedCard?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertCard(card: CachedCard)
}

3. Request Retries

Implement exponential backoff for transient failures:

iOS:

func fetchWithRetry<T>(
    maxAttempts: Int = 3,
    operation: @escaping () async throws -> T
) async throws -> T {
    var lastError: Error?
    
    for attempt in 1...maxAttempts {
        do {
            return try await operation()
        } catch {
            lastError = error
            if attempt < maxAttempts {
                let delay = UInt64(pow(2.0, Double(attempt))) * NSEC_PER_SEC
                try await Task.sleep(nanoseconds: delay)
            }
        }
    }
    
    throw lastError!
}

// Usage
let cards = try await fetchWithRetry {
    try await service.fetchCards()
}

4. Usage Monitoring

Display API usage to users before they hit limits:

iOS SwiftUI:

struct UsageView: View {
    @State private var limit: Int = 0
    @State private var used: Int = 0
    
    var body: some View {
        VStack {
            Text("API Usage")
                .font(.headline)
            
            ProgressView(value: Double(used), total: Double(limit))
            
            Text("\(used) / \(limit) requests")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .task {
            await loadUsage()
        }
    }
    
    func loadUsage() async {
        do {
            let response = try await service.fetchCards(arcana: "major")
            // Extract from response headers
        } catch {
            print("Failed to load usage")
        }
    }
}

Android Jetpack Compose:

@Composable
fun UsageCard(viewModel: TarotViewModel) {
    val usage by viewModel.apiUsage.collectAsState()
    
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("API Usage", style = MaterialTheme.typography.titleMedium)
            
            Spacer(modifier = Modifier.height(8.dp))
            
            LinearProgressIndicator(
                progress = usage.used.toFloat() / usage.limit.toFloat(),
                modifier = Modifier.fillMaxWidth()
            )
            
            Spacer(modifier = Modifier.height(4.dp))
            
            Text(
                "${usage.used} / ${usage.limit} requests",
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

Testing Your Integration

1. Seed-Based Testing

Use consistent seeds for reproducible test data:

// Test suite
func testThreeCardSpread() async throws {
    let response = try await service.drawCards(count: 3, seed: "test-seed-123")
    XCTAssertEqual(response.cards.count, 3)
    XCTAssertEqual(response.seed, "test-seed-123")
    
    // Same seed always returns same cards
    let response2 = try await service.drawCards(count: 3, seed: "test-seed-123")
    XCTAssertEqual(response.cards[0].id, response2.cards[0].id)
}

2. Mock API Responses

Create mock responses for UI testing without consuming API quota:

protocol TarotAPIProtocol {
    func fetchCards() async throws -> [TarotCard]
    func drawCards(count: Int) async throws -> DrawResponse
}

class MockTarotService: TarotAPIProtocol {
    func fetchCards() async throws -> [TarotCard] {
        return [
            TarotCard(id: "fool", name: "The Fool", arcana: "major", imageUrl: "...")
        ]
    }
    
    func drawCards(count: Int) async throws -> DrawResponse {
        return DrawResponse(
            cards: [DrawnCard(id: "fool", name: "The Fool", ...)],
            count: count,
            seed: nil
        )
    }
}

Next Steps

You now have a complete tarot reading integration! Here's what to build next:

  1. Daily Notifications: Push notifications with daily card at user's chosen time
  2. Reading History: Store user's past readings with notes and reflections
  3. Sharing: Generate beautiful card images for social media sharing
  4. Custom Spreads: Use /tarot/draw endpoint to create your own spread layouts
  5. Journaling: Combine readings with text input for personal growth tracking

Troubleshooting

Q: Getting 401 Unauthorized errors A: Verify your API key is correct and hasn't expired. Check your subscription status at roxyapi.com/products/tarot-api.

Q: Images not loading A: Ensure your app has internet permission and image URLs are being parsed correctly. All images are served over HTTPS from CDN.

Q: Rate limit exceeded too quickly A: Implement local caching for frequently accessed data (card list, individual card details). Only make API calls for dynamic content (daily cards, spreads).

Q: Inconsistent daily cards A: Ensure you're passing consistent userId and date parameters. The API uses UTC timezone - convert user's local date to UTC.

Resources


About RoxyAPI: Developer-focused API platform providing production-ready tarot, numerology, and astrology endpoints. Built for apps that need mystical features without the complexity of managing card databases, interpretation logic, or astronomical calculations. Pay per request with transparent monthly pricing.