A Kotlin Multiplatform client library for the Pexels API, targeting Android, iOS, and JVM backends.
PexKit provides a type-safe, coroutine-based API to search and retrieve high-quality stock photos and videos from Pexels.
- Kotlin Multiplatform - Works on Android, iOS, and JVM from a single codebase
- Type-safe API - Full Kotlin data classes with serialization
- Coroutine-based - All API calls are suspend functions
- Java-friendly - Blocking and async APIs for JVM/Java backends
- Result types - No exceptions for expected errors, use
PexKitResultfor clean error handling - Rate limit aware - Every response includes rate limit information
- Minimal dependencies - Built on Ktor and kotlinx.serialization
Add the dependency to your build.gradle.kts:
// In your shared module or app module
dependencies {
implementation("io.pexkit:pexkit-client:0.1.0")
}Android - No additional setup required. PexKit uses OkHttp under the hood.
iOS - No additional setup required. PexKit uses the Darwin (URLSession) engine.
JVM - No additional setup required. PexKit uses the CIO (Coroutine I/O) engine. Requires Java 21+.
Sign up at Pexels API to get your free API key.
import io.pexkit.api.PexKit
// Simple initialization
val pexkit = PexKit("YOUR_API_KEY")
// Or with custom configuration
val pexkit = PexKit {
apiKey = "YOUR_API_KEY"
defaultPerPage = 20
timeout = 30.seconds
logLevel = LogLevel.HEADERS // NONE, HEADERS, or BODY
}import io.pexkit.api.response.PexKitResult
when (val result = pexkit.photos.search("nature")) {
is PexKitResult.Success -> {
val photos = result.data.data
photos.forEach { photo ->
println("${photo.photographer}: ${photo.src.medium}")
}
// Rate limit info is always available
println("Requests remaining: ${result.rateLimit.remaining}")
}
is PexKitResult.Failure -> {
println("Error: ${result.error.message}")
}
}
// Don't forget to close when done
pexkit.close()// Search photos
val result = pexkit.photos.search(
query = "mountains",
filters = PhotoFilters(
orientation = Orientation.LANDSCAPE,
size = Size.LARGE,
color = "blue", // or use Color.BLUE
),
pagination = PaginationParams(page = 1, perPage = 20),
)
// Get curated photos (trending, updated hourly)
val curated = pexkit.photos.curated()
// Get a specific photo by ID
val photo = pexkit.photos.get(2014422)Photo object properties:
id- Unique identifierwidth,height- Dimensions in pixelsurl- Pexels page URLphotographer,photographerUrl,photographerId- Photographer infoavgColor- Average color as hex stringsrc- Available sizes:original,large2x,large,medium,small,portrait,landscape,tinyalt- Alt text descriptionliked- Whether liked by API key owner
// Search videos
val result = pexkit.videos.search(
query = "ocean waves",
filters = VideoFilters(
orientation = Orientation.LANDSCAPE,
minWidth = 1920,
minHeight = 1080,
minDuration = 10,
maxDuration = 60,
),
pagination = PaginationParams(page = 1, perPage = 15),
)
// Get popular videos
val popular = pexkit.videos.popular()
// Get a specific video by ID
val video = pexkit.videos.get(857251)Video object properties:
id- Unique identifierwidth,height- Dimensions in pixelsurl- Pexels page URLimage- Thumbnail URLduration- Duration in secondsuser- Videographer info (id,name,url)videoFiles- List of available files withquality,fileType,width,height,fps,linkvideoPictures- Preview thumbnails
// Get featured collections
val featured = pexkit.collections.featured()
// Get your own collections
val myCollections = pexkit.collections.my()
// Get media from a collection
val media = pexkit.collections.media(
id = "abc123",
type = MediaType.PHOTOS, // or VIDEOS, or null for both
pagination = PaginationParams(page = 1, perPage = 20),
)
// Collection media can be photos or videos
when (val result = pexkit.collections.media("abc123")) {
is PexKitResult.Success -> {
result.data.data.forEach { item ->
when (item) {
is CollectionMedia.PhotoMedia -> println("Photo: ${item.photographer}")
is CollectionMedia.VideoMedia -> println("Video: ${item.user.name}")
}
}
}
is PexKitResult.Failure -> { /* handle error */ }
}Collection object properties:
id- Unique identifiertitle- Collection titledescription- Collection descriptionprivate- Whether the collection is privatemediaCount,photosCount,videosCount- Media counts
All list endpoints return paginated responses:
val result = pexkit.photos.search("cats")
if (result is PexKitResult.Success) {
val response = result.data
println("Page ${response.page} of results")
println("${response.perPage} items per page")
println("${response.totalResults} total results")
if (response.hasNextPage) {
// Fetch next page
val nextPage = pexkit.photos.search(
"cats",
pagination = PaginationParams(page = response.page + 1)
)
}
}PexKit uses a PexKitResult sealed class instead of throwing exceptions:
when (val result = pexkit.photos.search("nature")) {
is PexKitResult.Success -> {
// Use result.data
}
is PexKitResult.Failure -> {
when (val error = result.error) {
is PexKitError.Unauthorized -> {
// Invalid or missing API key (401)
}
is PexKitError.Forbidden -> {
// Access forbidden (403)
}
is PexKitError.NotFound -> {
// Resource not found (404)
println("Not found: ${error.resource}")
}
is PexKitError.RateLimited -> {
// Too many requests (429)
println("Retry after ${error.retryAfter} seconds")
}
is PexKitError.ServerError -> {
// Server error (5xx)
println("Server error: ${error.statusCode}")
}
is PexKitError.NetworkError -> {
// Connection failed, timeout, etc.
println("Network error: ${error.cause.message}")
}
is PexKitError.Unknown -> {
// Unexpected error
println("Unknown error: ${error.statusCode} - ${error.body}")
}
}
}
}// Get data or null
val photos = result.getOrNull()
// Get data or throw exception
val photos = result.getOrThrow()
// Get data or default value
val photos = result.getOrDefault(emptyList())
// Get data or compute fallback (lazy, has access to error)
val photos = result.getOrElse { error ->
logger.warn("Failed: ${error.message}")
loadFromCache()
}
// Transform success data
val photoCount = result.map { it.totalResults }
// Side effects
result
.onSuccess { data -> updateUI(data) }
.onFailure { error -> showError(error) }val pexkit = PexKit {
// Required: Your Pexels API key
apiKey = "YOUR_API_KEY"
// Default results per page (1-80, default: 15)
defaultPerPage = 20
// Request timeout (default: 30 seconds)
timeout = 60.seconds
// Logging level (default: NONE)
logLevel = LogLevel.BODY // NONE, HEADERS, BODY
// Custom HTTP engine (for testing)
httpClientEngine = mockEngine
}Pexels API has rate limits (default: 200 requests/hour). Every successful response includes rate limit information:
when (val result = pexkit.photos.search("nature")) {
is PexKitResult.Success -> {
val rateLimit = result.rateLimit
println("Limit: ${rateLimit.limit}")
println("Remaining: ${rateLimit.remaining}")
println("Resets at: ${rateLimit.reset}") // Unix timestamp
}
is PexKitResult.Failure -> {
if (result.error is PexKitError.RateLimited) {
val retryAfter = (result.error as PexKitError.RateLimited).retryAfter
println("Rate limited. Retry after $retryAfter seconds")
}
}
}| Filter | Values |
|---|---|
orientation |
LANDSCAPE, PORTRAIT, SQUARE |
size |
LARGE (24MP), MEDIUM (12MP), SMALL (4MP) |
color |
Hex code without # (e.g., "FF5733") or predefined: RED, ORANGE, YELLOW, GREEN, TURQUOISE, BLUE, VIOLET, PINK, BROWN, BLACK, GRAY, WHITE |
locale |
EN_US, DE_DE, FR_FR, ES_ES, IT_IT, JA_JP, and more |
| Filter | Description |
|---|---|
orientation |
LANDSCAPE, PORTRAIT, SQUARE |
size |
LARGE, MEDIUM, SMALL |
locale |
Same as photo filters |
minWidth |
Minimum width in pixels |
minHeight |
Minimum height in pixels |
minDuration |
Minimum duration in seconds |
maxDuration |
Maximum duration in seconds |
PexKit provides full support for JVM backends, including Spring Boot, Ktor Server, and standalone applications. Choose between coroutine-based, blocking, or async APIs based on your needs.
For Kotlin backends using coroutines (recommended):
import io.pexkit.api.PexKit
import io.pexkit.api.response.PexKitResult
class PhotoService {
private val pexkit = PexKit("YOUR_API_KEY")
suspend fun searchPhotos(query: String): List<Photo> {
return when (val result = pexkit.photos.search(query)) {
is PexKitResult.Success -> result.data.data
is PexKitResult.Failure -> throw result.error.toException()
}
}
fun close() = pexkit.close()
}For Kotlin code that doesn't use coroutines:
import io.pexkit.api.blocking.PexKitBlocking
class PhotoService {
private val pexkit = PexKitBlocking.create("YOUR_API_KEY")
fun searchPhotos(query: String): List<Photo> {
// Blocking call - throws PexKitException on error
return pexkit.photos.search(query).data
}
fun close() = pexkit.close()
}For Java applications using blocking calls:
import io.pexkit.api.blocking.PexKitBlocking;
import io.pexkit.api.model.Photo;
import io.pexkit.api.response.PaginatedResponse;
import io.pexkit.api.response.PexKitException;
public class PhotoService implements AutoCloseable {
private final PexKitBlocking pexkit;
public PhotoService(String apiKey) {
this.pexkit = PexKitBlocking.create(apiKey);
}
public List<Photo> searchPhotos(String query) throws PexKitException {
PaginatedResponse<Photo> response = pexkit.photos().search(query);
return response.getData();
}
@Override
public void close() {
pexkit.close();
}
}
// Usage with try-with-resources
try (PexKitBlocking pexkit = PexKitBlocking.create("YOUR_API_KEY")) {
PaginatedResponse<Photo> photos = pexkit.photos().search("nature");
photos.getData().forEach(photo ->
System.out.println(photo.getPhotographer())
);
}For Java applications using async/non-blocking patterns:
import io.pexkit.api.blocking.PexKitBlocking;
import java.util.concurrent.CompletableFuture;
public class PhotoService {
private final PexKitBlocking pexkit = PexKitBlocking.create("YOUR_API_KEY");
public CompletableFuture<List<Photo>> searchPhotosAsync(String query) {
return pexkit.photos().searchAsync(query)
.thenApply(response -> response.getData());
}
// Chain multiple async operations
public CompletableFuture<Void> processPhotos(String query) {
return pexkit.photos().searchAsync(query)
.thenAccept(response -> {
response.getData().forEach(photo ->
processPhoto(photo)
);
})
.exceptionally(ex -> {
logger.error("Failed to search photos", ex);
return null;
});
}
}Example service for Spring Boot applications:
import io.pexkit.api.blocking.PexKitBlocking
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import jakarta.annotation.PreDestroy
@Service
class PexelsService(
@Value("\${pexels.api-key}") apiKey: String
) {
private val pexkit = PexKitBlocking.create(apiKey)
fun searchPhotos(query: String, page: Int = 1, perPage: Int = 15): PaginatedResponse<Photo> {
return pexkit.photos.search(
query = query,
pagination = PaginationParams(page = page, perPage = perPage)
)
}
fun getPhoto(id: Long): Photo = pexkit.photos.get(id)
@PreDestroy
fun cleanup() = pexkit.close()
}Or in Java:
import io.pexkit.api.blocking.PexKitBlocking;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.annotation.PreDestroy;
@Service
public class PexelsService {
private final PexKitBlocking pexkit;
public PexelsService(@Value("${pexels.api-key}") String apiKey) {
this.pexkit = PexKitBlocking.create(apiKey);
}
public PaginatedResponse<Photo> searchPhotos(String query) {
return pexkit.photos().search(query);
}
@PreDestroy
public void cleanup() {
pexkit.close();
}
}PexKit and PexKitBlocking instances are thread-safe and can be shared across multiple threads. You should:
- Create a single instance and reuse it (e.g., as a singleton or Spring bean)
- Call
close()when your application shuts down to release resources - For Spring applications, use
@PreDestroyto ensure proper cleanup
The blocking API uses runBlocking internally to bridge suspend functions to blocking calls. Be aware of the following:
- Do not call from Android main thread: Calling blocking methods on the Android main (UI) thread will cause an ANR (Application Not Responding) error.
- Do not call from coroutine dispatchers: Calling from within a coroutine context (e.g.,
Dispatchers.DefaultorDispatchers.IO) may cause deadlocks in some configurations. - Recommended: Use the suspend-based
PexKitAPI when working with coroutines, orPexKitAsyncfor Java'sCompletableFuturepattern.
The async API returns CompletableFuture for Java interoperability. Note the following limitation:
- Cancellation does not stop HTTP requests: Cancelling a
CompletableFuturereturned by this API does not cancel the underlying HTTP request. The request will continue to completion even if the future is cancelled. This is a limitation of bridging coroutines toCompletableFuture. - For proper cancellation: Use the suspend-based
PexKitAPI with coroutines, which supports structured concurrency and cancellation.
| API Style | Class | Returns | Error Handling |
|---|---|---|---|
| Coroutines (Kotlin) | PexKit |
PexKitResult<T> |
Pattern matching |
| Blocking (Kotlin/Java) | PexKitBlocking |
T directly |
Throws PexKitException |
| Async (Java) | PexKitAsync |
CompletableFuture<T> |
.exceptionally() / .handle() |
For complete API documentation, rate limit details, and terms of use, visit the official Pexels API documentation: Pexels API Documentation