A comprehensive Kotlin Multiplatform library providing type-safe access to the Scryfall REST API, the premier Magic: The Gathering card database.
- 🎯 Fully Typed API - Complete Kotlin models for all Scryfall data structures
- 🔄 Multiplatform Support - Works on JVM, Android, iOS, JavaScript (Node.js & Browser)
- 🚀 Coroutine-First - All API calls are suspend functions
- 🔨 Type-Safe Query DSL - Build search queries with IDE autocomplete instead of raw strings
- 🃏 Card Extension Functions - Rich set of utilities for filtering, sorting, and checking card properties
- ✅ Input Validation - Client-side validation with clear error messages catches issues before API calls
- ⚡ Rate Limit Aware - Handles 429 responses with Retry-After headers
- 🛡️ Type-Safe Errors - Sealed exception hierarchy for robust error handling
- 📝 Configurable Logging - Built-in HTTP request/response logging for debugging
- 🧪 Well Tested - Comprehensive unit and integration tests
- 📚 Fully Documented - KDoc comments with links to official API docs
This library works on all major Kotlin Multiplatform targets:
- JVM - Desktop applications and server-side
- Android - Native Android apps (minSdk 24)
- iOS - Native iOS apps (x64, arm64, simulator arm64)
- JavaScript - Browser and Node.js environments
All features work consistently across all platforms with appropriate HTTP client engines configured for each target.
| API | Description | Endpoints |
|---|---|---|
| Cards | Search and retrieve card data | 13 methods including search, named lookup, random, and ID-based queries |
| Sets | Magic set information | Query by code, TCGPlayer ID, or Scryfall UUID |
| Rulings | Official card rulings | Retrieve rulings by various card identifiers |
| Catalogs | Reference data | 20 catalogs including card names, types, keywords, and abilities |
| Symbology | Mana symbols and parsing | List symbols and parse mana cost strings |
| Bulk Data | Bulk download metadata | Access bulk data exports for offline use |
Add the dependency to your build.gradle.kts:
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.devmugi:scryfall-api:1.0.1")
}
}
}npm install @devmugi/scryfall-apiThen import in your code:
import { Scryfall } from '@devmugi/scryfall-api';
const scryfall = new Scryfall();
const card = await scryfall.cards.namedExact({ name: 'Lightning Bolt' });
console.log(card.name);import devmugi.mtgcards.scryfall.Scryfall
// Create client
val scryfall = Scryfall()
// Search for cards
val results = scryfall.cards.search(q = "Lightning Bolt")
println("Found ${results.totalCards} cards")
results.data.forEach { card ->
println("${card.name} - ${card.setName}")
}
// Get specific card
val card = scryfall.cards.namedExact("Black Lotus")
println("${card.name} costs ${card.manaCost ?: "nothing!"}")
// Get random card
val random = scryfall.cards.random()
println("Random card: ${random.name}")See the samples directory for complete working examples on Android, iOS, Web, and Node.js.
The Cards API provides 13 different methods for searching and retrieving cards:
// Search with Scryfall syntax
val results = scryfall.cards.search(
q = "t:creature c:red pow>=4",
unique = UniqueModes.CARDS,
order = SortingCards.NAME,
dir = SortDirection.ASC
)
// Paginate through results
if (results.hasMore) {
val nextPage = results.loadNextPage(scryfall.cards.client)
}
// Or use Flow for automatic pagination
results.asFlow(scryfall.cards.client).collect { card ->
println(card.name)
}// Exact name match
val bolt = scryfall.cards.namedExact("Lightning Bolt")
// Fuzzy name match
val card = scryfall.cards.namedFuzzy("Jace, the Mind Sculptor")
// Specify set for specific printing
val alphaLotus = scryfall.cards.namedExact(
name = "Black Lotus",
set = "lea"
)// By Scryfall ID
val card1 = scryfall.cards.byScryfallId("f2eb06e-...")
// By Multiverse ID
val card2 = scryfall.cards.byMultiverseId(1234)
// By MTG Arena ID
val card3 = scryfall.cards.byArenaId(67890)
// By MTGO ID
val card4 = scryfall.cards.byMtgoId(54321)
// By TCGPlayer ID
val card5 = scryfall.cards.byTcgplayerId(123456)
// By set code and collector number
val card6 = scryfall.cards.bySetAndCollectorNumber(
code = "khm",
number = "1"
)val identifiers = listOf(
Identifier.ScryfallId("f2eb06e-..."),
Identifier.MultiverseId(1234),
Identifier.NameAndSet(name = "Lightning Bolt", set = "lea")
)
val collection = scryfall.cards.collection(identifiers)
println("Found ${collection.data.size} cards")
// Check for cards not found
collection.notFound?.forEach { notFound ->
println("Not found: ${notFound.identifier}")
}val suggestions = scryfall.cards.autocomplete("lig")
suggestions.data.forEach { suggestion ->
println(suggestion) // "Lightning Bolt", "Light Up the Stage", etc.
}// Completely random card
val random = scryfall.cards.random()
// Random card matching query
val randomCreature = scryfall.cards.random(q = "t:creature")// Get all sets
val allSets = scryfall.sets.all()
allSets.data.forEach { set ->
println("${set.name} (${set.code}) - ${set.cardCount} cards")
}
// Get specific set by code
val khm = scryfall.sets.byCode("khm")
println("${khm.name} released on ${khm.released_at}")
// Get set by TCGPlayer ID
val set = scryfall.sets.byTcgPlayerId(12345)
// Get set by Scryfall ID
val set2 = scryfall.sets.byId("uuid-here")// Get rulings by Scryfall card ID
val rulings = scryfall.rulings.byCardId("f2eb06e-...")
rulings.data.forEach { ruling ->
println("${ruling.publishedAt}: ${ruling.comment}")
}
// Get rulings by Multiverse ID
val rulings2 = scryfall.rulings.byMultiverseId(1234)
// Get rulings by set and collector number
val rulings3 = scryfall.rulings.byCollectorId(
code = "khm",
number = "1"
)Access curated reference data:
// Get all card names
val cardNames = scryfall.catalogs.cardNames()
println("Total cards: ${cardNames.data.size}")
// Get creature types
val creatureTypes = scryfall.catalogs.creatureTypes()
creatureTypes.data.forEach { type ->
println(type) // "Human", "Wizard", "Cat", etc.
}
// Get keyword abilities
val keywords = scryfall.catalogs.keywordAbilities()
println(keywords.data.joinToString()) // "Flying", "Trample", "Haste", etc.
// Other catalogs available:
// - artistNames(), wordBank()
// - supertypes(), cardTypes()
// - planeswalkerTypes(), landTypes(), artifactTypes(), battleTypes(), enchantmentTypes(), spellTypes()
// - powers(), toughnesses(), loyalties()
// - watermarks()
// - keywordActions(), abilityWords(), flavorWords()// Get all mana symbols
val symbols = scryfall.symbology.all()
symbols.data.forEach { symbol ->
println("${symbol.symbol}: ${symbol.englishDescription}")
}
// Parse mana cost
val parsed = scryfall.symbology.parseMana("{2}{U}{U}")
println("CMC: ${parsed.manaValue}")
println("Colors: ${parsed.colors.joinToString()}")// Get all bulk data exports
val bulkData = scryfall.bulk.all()
bulkData.data.forEach { item ->
println("${item.name}: ${item.downloadUri}")
}
// Get specific bulk data by type
val oracleCards = scryfall.bulk.byType("oracle_cards")
println("Download: ${oracleCards.downloadUri}")
println("Size: ${oracleCards.size} bytes")
println("Updated: ${oracleCards.updatedAt}")
// Get bulk data by ID
val bulkItem = scryfall.bulk.byId("uuid-here")Customize the HTTP client behavior:
val scryfall = Scryfall(
config = ScryfallConfig(
baseUrl = "https://api.scryfall.com",
userAgent = "MyApp/1.0 (+https://myapp.com; contact@myapp.com)",
connectTimeoutMillis = 10_000,
requestTimeoutMillis = 15_000,
socketTimeoutMillis = 30_000,
maxRetries = 2,
enableLogging = false,
logLevel = LogLevel.INFO
)
)Enable HTTP request/response logging for debugging:
import io.ktor.client.plugins.logging.*
// With default logger (prints to stdout)
val scryfall = Scryfall(
config = ScryfallConfig(
enableLogging = true,
logLevel = LogLevel.INFO // or LogLevel.ALL, LogLevel.HEADERS, LogLevel.BODY
)
)
// With custom logger
val scryfall = Scryfall(
config = ScryfallConfig(
enableLogging = true,
logLevel = LogLevel.BODY
),
logger = { message ->
// Custom logging implementation
println("[Scryfall] $message")
// or use your logging framework
// myLogger.debug(message)
}
)Available log levels:
LogLevel.NONE- No loggingLogLevel.INFO- Basic request/response infoLogLevel.HEADERS- Request/response with headersLogLevel.BODY- Request/response with headers and bodyLogLevel.ALL- Everything including internal details
// For testing or advanced use cases
val mockEngine = MockEngine { request ->
// Return mock responses
}
val scryfall = Scryfall(engine = mockEngine)The library uses a sealed exception hierarchy for type-safe error handling:
try {
val card = scryfall.cards.namedExact("Nonexistent Card")
} catch (e: ScryfallApiException) {
when (e) {
is ScryfallApiException.NotFound -> {
println("Card not found: ${e.error.details}")
}
is ScryfallApiException.RateLimited -> {
println("Rate limited, retry after ${e.retryAfterSeconds} seconds")
}
is ScryfallApiException.InvalidRequest -> {
println("Bad request: ${e.error.details}")
}
is ScryfallApiException.ServerError -> {
println("Scryfall server error: ${e.error.details}")
}
is ScryfallApiException.NetworkError -> {
println("Network error: ${e.cause?.message}")
}
is ScryfallApiException.ParseError -> {
println("JSON parse error: ${e.cause?.message}")
}
is ScryfallApiException.HttpError -> {
println("HTTP error ${e.status}: ${e.error?.details}")
}
}
}The library automatically retries failed requests with exponential backoff:
- Rate limits (429): Respects
Retry-Afterheader - Server errors (500-599): Retries up to
maxRetriestimes - Network errors: Retries transient failures
- Exponential backoff: Delay = baseDelay × 2^attemptNumber + random jitter
The library performs client-side input validation before making API calls to catch common errors early and provide clear error messages. This fail-fast approach helps developers catch bugs during development.
All API methods validate their input parameters:
// Search query validation
try {
scryfall.cards.search("") // Empty query
} catch (e: IllegalArgumentException) {
println(e.message) // "Query cannot be blank"
}
try {
scryfall.cards.search("a".repeat(1001)) // Query too long
} catch (e: IllegalArgumentException) {
println(e.message) // "Query too long (max 1000 characters)"
}
// Card name validation
try {
scryfall.cards.namedExact("") // Empty name
} catch (e: IllegalArgumentException) {
println(e.message) // "Card name cannot be blank"
}
// Page number validation
try {
scryfall.cards.search("t:creature", page = 0) // Invalid page
} catch (e: IllegalArgumentException) {
println(e.message) // "Page must be greater than 0"
}
try {
scryfall.cards.search("t:creature", page = 10001) // Page too large
} catch (e: IllegalArgumentException) {
println(e.message) // "Page number too large (max 10000)"
}
// Set code validation
try {
scryfall.cards.namedExact("Lightning Bolt", set = "AB") // Too short
} catch (e: IllegalArgumentException) {
println(e.message) // "Set code must be 3-5 characters"
}
// ID validation
try {
scryfall.cards.byArenaId(-1) // Negative ID
} catch (e: IllegalArgumentException) {
println(e.message) // "Arena ID must be positive"
}
// UUID validation
try {
scryfall.cards.byScryfallId("not-a-uuid") // Invalid UUID format
} catch (e: IllegalArgumentException) {
println(e.message) // "ID must be a valid UUID format"
}
// Collection size validation
try {
val ids = List(76) { Identifier.Name("Card $it") } // Too many identifiers
scryfall.cards.collection(ids)
} catch (e: IllegalArgumentException) {
println(e.message) // "Too many identifiers (max 75 per request)"
}The library enforces these validation rules:
| Parameter | Rules |
|---|---|
| Search Query | Not blank, max 1000 characters |
| Card Name | Not blank, max 500 characters |
| Page Number | Greater than 0, max 10000 |
| Set Code | 3-5 alphanumeric characters |
| Scryfall ID | Valid UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) |
| Collector Number | Not blank, max 10 characters |
| Language Code | 2 lowercase letters (ISO 639-1) |
| Identifiers List | Not empty, max 75 items |
| Numeric IDs | Positive integers (Arena, MTGO, Multiverse, TCGPlayer, Cardmarket) |
- Fail Fast: Catch invalid inputs before making HTTP requests
- Clear Error Messages: Get specific, actionable error messages
- Development Safety: Helps catch bugs during development
- Reduced API Load: Invalid requests don't reach Scryfall's servers
- Consistent Behavior: Same validation across all platforms
The Card class provides comprehensive card data:
card.id // Scryfall UUID
card.name // Card name
card.manaCost // e.g., "{2}{U}{U}"
card.manaValue // Converted mana cost
card.typeLine // e.g., "Creature — Human Wizard"
card.oracleText // Rules text
card.power // Creature power
card.toughness // Creature toughness
card.colors // Color identity
card.keywords // List of keywords
card.legalities // Format legality
card.setCode // Set code
card.setName // Set name
card.rarity // "common", "uncommon", "rare", "mythic"
card.imageUris // Image URLs
card.prices // Price data
card.releasedAt // Release date
card.cardFaces // For multiface cardsCards with multiple faces (transform, split, flip, etc.) have data in cardFaces:
if (card.cardFaces != null) {
card.cardFaces.forEach { face ->
println("${face.name}: ${face.oracleText}")
}
}card.imageUris?.let { uris ->
println("Small: ${uris.small}")
println("Normal: ${uris.normal}")
println("Large: ${uris.large}")
println("PNG: ${uris.png}")
println("Art crop: ${uris.artCrop}")
println("Border crop: ${uris.borderCrop}")
}The ScryfallList<T> wrapper provides pagination helpers:
val results: ScryfallList<Card> = scryfall.cards.search("t:creature")
// Check if more pages exist
if (results.hasMore) {
// Manual pagination
val nextPage = results.loadNextPage(scryfall.cards.client)
// Or use Flow
results.asFlow(scryfall.cards.client).collect { card ->
// Process each card across all pages
}
}
// Access metadata
println("Total cards: ${results.totalCards}")
println("Has more: ${results.hasMore}")
println("Warnings: ${results.warnings?.joinToString()}")The search() method supports full Scryfall search syntax:
// By card name
scryfall.cards.search("lightning")
// By type
scryfall.cards.search("t:creature")
// By color
scryfall.cards.search("c:red")
// By mana cost
scryfall.cards.search("cmc=2")
// Complex queries
scryfall.cards.search("t:creature c:red pow>=4 cmc<=5")
// By set
scryfall.cards.search("e:khm r:mythic")
// By text
scryfall.cards.search("o:flying o:haste")See the Scryfall Search Reference for complete syntax documentation.
For a more type-safe and IDE-friendly way to build search queries, use the SearchQueryBuilder DSL instead of manually constructing query strings:
// Instead of manually writing query strings:
val results = scryfall.cards.search("t:creature c:red pow>=4 cmc<=5 legal:standard")
// Use the DSL:
val results = scryfall.cards.searchWithBuilder {
type("creature")
color("red")
powerRange(min = 4)
cmcRange(max = 5)
isLegal("standard")
}searchWithBuilder {
// Exact name match
name("Lightning Bolt")
// Partial name match
nameContains("bolt")
}searchWithBuilder {
// Card type
type("creature")
type("instant")
// Color identity
color("R") // Red only
color("R", "G") // Red and green
color("W", "U", "B") // White, blue, and black
}searchWithBuilder {
// Exact CMC
cmc(3)
// CMC range
cmcRange(min = 2, max = 4)
cmcRange(min = 3) // 3 or more
cmcRange(max = 5) // 5 or less
}searchWithBuilder {
// Exact values
power(4)
toughness(4)
// Ranges
powerRange(min = 4, max = 10)
toughnessRange(min = 5)
}searchWithBuilder {
// Set code
set("khm")
set("neo")
// Rarity
rarity("mythic")
rarity("rare")
}searchWithBuilder {
// Oracle text
text("draw a card")
text("destroy target creature")
// Keyword abilities
keyword("flying")
keyword("trample")
}searchWithBuilder {
// Cards legal in format
isLegal("standard")
isLegal("modern")
isLegal("commander")
// Cards printed in format (regardless of legality)
format("standard")
}searchWithBuilder {
artist("Seb McKinnon")
}searchWithBuilder {
// For advanced queries not covered by DSL methods
raw("is:commander")
raw("frame:old")
}val results = scryfall.cards.searchWithBuilder {
type("creature")
color("R", "G")
powerRange(min = 4)
toughnessRange(min = 4)
cmcRange(max = 5)
isLegal("standard")
rarity("rare")
}val results = scryfall.cards.searchWithBuilder {
isLegal("commander")
rarity("uncommon")
text("draw")
}val results = scryfall.cards.searchWithBuilder(
order = SortingCards.EDH_REC,
dir = SortDirection.DESC
) {
isLegal("modern")
color("B")
text("destroy target")
cmcRange(max = 3)
}val results = scryfall.cards.searchWithBuilder {
nameContains("dragon")
artist("Seb McKinnon")
set("khm")
}val firstPage = scryfall.cards.searchWithBuilder(page = 1) {
type("creature")
isLegal("standard")
}
// Use pagination helpers
if (firstPage.hasMore) {
val nextPage = firstPage.loadNextPage(scryfall.cards.client)
}
// Or collect all results with Flow
firstPage.asFlow(scryfall.cards.client).collect { card ->
println(card.name)
}You can build query strings without executing them:
val query = searchQuery {
type("creature")
color("R")
powerRange(min = 4)
}
// query = "t:creature c:R pow>=4"
// Use the query string directly
val results = scryfall.cards.search(query)- Type Safety: Compile-time checks prevent syntax errors
- IDE Support: Autocomplete shows available filters
- Readable: Self-documenting code
- Maintainable: Easier to modify complex queries
- No Syntax Memorization: No need to remember Scryfall search operators
The library provides a rich set of extension functions for working with Card objects and lists of cards. These utilities make it easy to filter, sort, and check properties of cards without writing boilerplate code.
Check card types using convenient extension functions:
val card = scryfall.cards.namedExact("Lightning Bolt")
if (card.isInstant()) {
println("This is an instant!")
}
// Available type checks:
card.isCreature() // true if card is a creature
card.isInstant() // true if card is an instant
card.isSorcery() // true if card is a sorcery
card.isArtifact() // true if card is an artifact
card.isEnchantment() // true if card is an enchantment
card.isPlaneswalker() // true if card is a planeswalker
card.isLand() // true if card is a land
card.isBattle() // true if card is a battleCheck if a card is legal in specific formats:
val card = scryfall.cards.namedExact("Lightning Bolt")
if (card.isLegalIn("modern")) {
println("Legal in Modern!")
}
if (card.isLegalIn("standard")) {
println("Legal in Standard!")
}
// Works with all supported formats: standard, modern, legacy, vintage,
// commander, pioneer, historic, pauper, etc.Check card colors and other properties:
// Color checks
if (card.hasColor("R")) {
println("This card is red!")
}
if (card.isColorless()) {
println("This card has no colors")
}
if (card.isMulticolored()) {
println("This card has multiple colors")
}
// Keyword checks
if (card.hasKeyword("flying")) {
println("This creature flies!")
}Filter lists of cards using convenient extension functions:
val cards = scryfall.cards.search("set:mkm").data
// Filter by type
val creatures = cards.filterCreatures()
val instants = cards.filterInstants()
val artifacts = cards.filterArtifacts()
val dragons = cards.filterByType("Dragon")
// Filter by color
val redCards = cards.filterByColor("R") // Exactly red
val blueGreen = cards.filterByColor("U", "G") // Exactly blue-green
val hasRed = cards.filterByColorIncludes("R") // Contains red
val hasRedOrBlue = cards.filterByColorIncludes("R", "U") // Contains red or blue
// Filter by CMC
val threeDrop = cards.filterByCmc(3.0)
val lowCost = cards.filterByCmcRange(min = 0.0, max = 3.0)
val expensive = cards.filterByCmcRange(min = 7.0)
// Filter by rarity
val mythics = cards.filterByRarity("mythic")
val rares = cards.filterByRarity("rare")
// Filter by legality
val modernLegal = cards.filterLegalIn("modern")
val commanderLegal = cards.filterLegalIn("commander")
// Combine multiple filters
val affordableDragons = cards
.filterByType("Dragon")
.filterByCmcRange(max = 6.0)
.filterLegalIn("commander")Sort cards by various criteria:
val cards = scryfall.cards.search("type:creature").data
// Sort by name (alphabetically)
val alphabetical = cards.sortByName()
// Sort by CMC (mana value)
val byCost = cards.sortByCmc()
// Sort by price (USD)
val byPrice = cards.sortByPrice()
// Sort by rarity (common -> uncommon -> rare -> mythic)
val byRarity = cards.sortByRarity()
// Sort by release date (oldest first)
val byReleaseDate = cards.sortByReleaseDate()
// Sort by collector number
val byCollectorNumber = cards.sortByCollectorNumber()
// Chain sorting with filtering
val topDragons = cards
.filterByType("Dragon")
.filterLegalIn("commander")
.sortByPrice()
.take(10) // Get 10 cheapest commander-legal dragons// Find affordable commander-legal creatures
val budgetCreatures = scryfall.cards
.search("type:creature")
.data
.filterLegalIn("commander")
.filterByCmcRange(min = 2.0, max = 6.0)
.sortByPrice()
.take(50)
budgetCreatures.forEach { card ->
val price = card.prices?.usd ?: "N/A"
println("${card.name} - CMC: ${card.manaValue} - $$price")
}val set = scryfall.cards.search("set:mkm").data
val white = set.filterByColorIncludes("W").size
val blue = set.filterByColorIncludes("U").size
val black = set.filterByColorIncludes("B").size
val red = set.filterByColorIncludes("R").size
val green = set.filterByColorIncludes("G").size
val colorless = set.filterColorless().size
val multicolor = set.filterMulticolored().size
println("""
Set Color Distribution:
White: $white
Blue: $blue
Black: $black
Red: $red
Green: $green
Colorless: $colorless
Multicolor: $multicolor
""".trimIndent())// Find cards legal in both Modern and Pioneer
val dualFormatCards = scryfall.cards
.search("type:creature power>=3")
.data
.filter { it.isLegalIn("modern") && it.isLegalIn("pioneer") }
.sortByCmc()
dualFormatCards.forEach { card ->
println("${card.name} - ${card.manaValue} mana")
}val budgetRemoval = scryfall.cards
.search("(type:instant or type:sorcery) (o:destroy or o:exile)")
.data
.filterByCmcRange(max = 4.0)
.filterLegalIn("standard")
.sortByPrice()
.take(20)
budgetRemoval.forEach { card ->
val price = card.prices?.usd ?: "N/A"
println("${card.name} - ${card.manaValue} mana - $$price")
}The module includes comprehensive tests with 207 total tests across unit and integration test suites.
Tests are organized in src/jvmTest/kotlin/ using a naming convention:
- Unit tests:
*Test.kt(e.g.,CardsApiTest.kt) - use mocked HTTP responses - Integration tests:
*IntegrationTest.kt(e.g.,CardsApiSearchIntegrationTest.kt) - make real API calls
# Run all JVM tests (unit + integration)
./gradlew :scryfall-api:jvmTest
# Run only integration tests (real API calls)
./gradlew :scryfall-api:jvmTest --tests "*IntegrationTest"
# Run only unit tests (mocked, fast)
./gradlew :scryfall-api:jvmTest --tests "*Test" --tests "!*IntegrationTest"
# Run specific test class
./gradlew :scryfall-api:jvmTest --tests "CardsApiSearchIntegrationTest"
# Run all platform tests
./gradlew :scryfall-api:allTestsUnit tests use Ktor's MockEngine to simulate API responses without network calls:
@Test
fun search_validQuery_returnsCards() = runTest {
val mockEngine = MockEngine { request ->
// Verify request and return mock response
}
val api = CardsApi(engine = mockEngine)
val results = api.search("Lightning Bolt")
assertEquals(1, results.data.size)
}Integration tests make real API calls to verify end-to-end functionality:
@Test
fun search_realCall_returnsCards() = runTest {
val api = CardsApi()
val results = api.search("Lightning Bolt")
assertTrue(results.data.isNotEmpty())
}Tests follow the pattern: methodName_scenario_expectedResult
Examples:
search_validQuery_returnsCardsnamedExact_realCall_returnsExpectedCardcollection_multipleIdentifiers_returnsAllCards
The module uses static analysis tools to maintain code quality and consistency.
Detekt performs static code analysis to find code smells and potential issues.
Run Detekt analysis:
# Check all source sets
./gradlew :scryfall-api:detektAll --rerun-tasks
# Check specific source set
./gradlew :scryfall-api:detektCommonMain
./gradlew :scryfall-api:detektJvmMainView results:
- HTML report:
scryfall-api/build/reports/detekt/detekt.html - XML report:
scryfall-api/build/reports/detekt/detekt.xml
Configuration:
- Config file:
scryfall-api/detekt.yml
Ktlint enforces the official Kotlin code style.
Check code formatting:
# Check all source sets
./gradlew :scryfall-api:ktlintCheckAll --rerun-tasks
# Check specific source set
./gradlew :scryfall-api:ktlintCommonMainSourceSetCheckAuto-format code:
# Format all source sets
./gradlew :scryfall-api:ktlintFormatAll --rerun-tasks
# Format specific source set
./gradlew :scryfall-api:ktlintCommonMainSourceSetFormatConfiguration:
- EditorConfig:
scryfall-api/.editorconfig
Note: Linting tasks are separate from build/test tasks and won't cause build failures. They're designed to be run manually or in CI as informational checks.
The module uses Dokka to generate comprehensive API documentation from KDoc comments.
Generate HTML documentation:
./gradlew :scryfall-api:dokkaGenerateHtmlThe generated documentation will be available at:
scryfall-api/build/dokka/html/index.html
Open the generated documentation in your browser:
# macOS
open scryfall-api/build/dokka/html/index.html
# Linux
xdg-open scryfall-api/build/dokka/html/index.html
# Windows
start scryfall-api/build/dokka/html/index.htmlThe generated documentation includes:
- Scryfall - Main entry point class
- API Classes - CardsApi, SetsApi, RulingsApi, CatalogsApi, CardSymbolApi, BulkDataApi
- Core Classes - ScryfallConfig, ScryfallBaseApi, error handling types
- Data Models - Card, Set, Ruling, Symbol, and all supporting models
- Extension Functions - Card utilities, filters, and search query builders
- Pagination - ScryfallList with Flow support
Scryfall requests that you:
- Respect rate limits (approximately 10 requests per second)
- Use appropriate user agents (identify your app)
- Cache responses when possible
- Use bulk data for large-scale data needs
This library:
- ✅ Automatically handles 429 rate limit responses
- ✅ Includes configurable user agent
- ✅ Retries with exponential backoff
- ✅ Provides bulk data API access
// Good: Create once, reuse
class Repository {
private val scryfall = Scryfall()
suspend fun searchCards(query: String) = scryfall.cards.search(query)
}
// Bad: Creating new instances repeatedly
fun searchCards(query: String) = Scryfall().cards.search(query) // Don't do thissuspend fun safeSearch(query: String): List<Card> = try {
scryfall.cards.search(query).data
} catch (e: ScryfallApiException.NotFound) {
emptyList() // Return empty list for not found
} catch (e: ScryfallApiException) {
throw e // Propagate other errors
}// If you know the exact name, use namedExact (faster)
scryfall.cards.namedExact("Lightning Bolt")
// If you have fuzzy input, use namedFuzzy
scryfall.cards.namedFuzzy(userInput)
// For complex queries, use search
scryfall.cards.search("t:creature c:red")// Good: Use collection endpoint for multiple cards
val identifiers = cardNames.map { Identifier.Name(it) }
val cards = scryfall.cards.collection(identifiers).data
// Bad: Individual requests in a loop
val cards = cardNames.map { scryfall.cards.namedExact(it) } // Slower// For analyzing large datasets, use bulk data
val bulkData = scryfall.bulk.byType("oracle_cards")
// Download and process bulkData.downloadUriscryfall-api/
├── api/
│ ├── CardsApi.kt # Card search and retrieval
│ ├── SetsApi.kt # Set information
│ ├── RulingsApi.kt # Card rulings
│ ├── CatalogsApi.kt # Reference catalogs
│ ├── CardSymbolApi.kt # Mana symbols
│ ├── BulkDataApi.kt # Bulk data exports
│ ├── core/
│ │ ├── ScryfallBaseApi.kt # Base HTTP client setup
│ │ ├── ScryfallConfig.kt # Configuration
│ │ └── ScryfallApiException.kt # Error types
│ └── models/
│ ├── Card.kt # Card data model (520 lines)
│ ├── Set.kt, Ruling.kt, etc.
│ └── ScryfallList.kt # Pagination wrapper
└── Scryfall.kt # Main entry point
- HTTP Client: Ktor (with platform-specific engines)
- Serialization: kotlinx-serialization
- Coroutines: kotlinx-coroutines
- Platforms: JVM, Android, iOS, JavaScript
- Suspend functions: All API calls are suspend functions for coroutine support
- Immutable models: All data classes are immutable
- Sealed exceptions: Type-safe error handling
- Lazy initialization: API endpoints initialized on first use
- Platform abstraction: Uses Ktor's multiplatform HTTP client
| Scryfall Endpoint | Status | API Class |
|---|---|---|
| Cards - Search | ✅ | CardsApi.search() |
| Cards - Named | ✅ | CardsApi.namedExact(), namedFuzzy() |
| Cards - Autocomplete | ✅ | CardsApi.autocomplete() |
| Cards - Random | ✅ | CardsApi.random() |
| Cards - By ID | ✅ | CardsApi.byScryfallId(), byMultiverseId(), etc. |
| Cards - Collection | ✅ | CardsApi.collection() |
| Sets | ✅ | SetsApi |
| Rulings | ✅ | RulingsApi |
| Symbology | ✅ | CardSymbolApi |
| Catalogs | ✅ | CatalogsApi |
| Bulk Data | ✅ | BulkDataApi |
- Scryfall API Documentation
- Scryfall Search Syntax
- Scryfall Card Objects
- Ktor Documentation
- kotlinx-serialization
Contributions are welcome! Please ensure:
- All tests pass:
./gradlew :scryfall-api:allTests - Code follows Kotlin conventions
- New features include tests (unit + integration)
- Public APIs include KDoc comments
- Scryfall for providing an excellent Magic: The Gathering API
- The Kotlin Multiplatform team for making cross-platform Kotlin possible


