Connect CE.SDK to Pexels so Android users can browse, search, and add royalty-free stock photos without leaving the editor.
Pexels accepts an API key in the Authorization request header. In production, the Android app should call your backend proxy,
and the proxy should hold the key, forward curated or search requests to Pexels, and return the Pexels JSON response.
This guide covers the Android client side: registering a custom AssetSource, querying and translating Pexels responses,
applying a result programmatically, exposing Pexels in the editor asset library, and preserving provider and photographer
attribution.
Prerequisites#
Before integrating Pexels, make sure you have:
- A Pexels API key from the Pexels API documentation.
- For production Android apps, a backend proxy or secrets layer appropriate for your deployment.
- An integration that follows Pexels’ API guidelines, provider-link and photographer-credit guidance, and rate limits.
- A plan for displaying Pexels provider and photographer attribution in your app-owned UI.
The sample accepts an API key parameter and calls Pexels directly so you can inspect the complete request flow.
Protect the API Key in Production#
The recommended request flow for an Android production app is:
Android app (CE.SDK) -> Your backend proxy -> Pexels API
The production implementation of PexelsClient sends the search text, page, and page size to your proxy. The proxy validates
the app request, authenticates to Pexels with the API key, calls either the curated or search endpoint, and returns the Pexels
response. Because CE.SDK uses zero-based pages on Android and Pexels uses one-based pages, the source adds one to
FindAssetsQuery.page before making the provider request.
The sample uses these Pexels request values:
| Value | Purpose |
|---|---|
query |
Search text from FindAssetsQuery.query; omitted for curated browsing. |
page |
One-based Pexels page number. |
per_page |
Number of results requested for the page. |
Authorization |
Pexels API key. Add this header on the proxy in production. |
Keep the Pexels response shape intact when possible. The Android client maps fields such as photos, total_results,
next_page, image dimensions and URLs, the public photo page, and photographer information into CE.SDK assets.
Create the Asset Source#
On Android, a custom remote provider is an AssetSource subclass. Give the Pexels source a stable ID, declare its supported
image formats, and provide source-level credits and licensing metadata.
class PexelsAssetSource( private val client: PexelsClient,) : AssetSource(sourceId = SOURCE_ID) { override suspend fun getGroups(): List<String>? = null
override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) override val credits = AssetCredits( name = "Pexels", uri = Uri.parse("https://www.pexels.com/"), )
override val license = AssetLicense( name = "Pexels license (free)", uri = Uri.parse("https://www.pexels.com/license/"), )
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { findPexelsAssets( client = client, sourceId = sourceId, query = query, ) }
companion object { const val SOURCE_ID = "ly.img.asset.source.pexels" }}supportedMimeTypes,credits, andlicensedescribe the source and apply to all results.getGroups()returnsnullbecause this source does not expose another grouping layer.findAssets(query)moves the complete request, decoding, mapping, and pagination path toDispatchers.IO.
Per-asset credits identify the photographer and link to the public Pexels photo page.
Query Pexels#
The sample client separates endpoint selection from request execution. Keep request and response processing off the engine
thread; PexelsAssetSource.findAssets() runs the complete path on Dispatchers.IO, while the included HttpURLConnection
transport uses finite timeouts and always disconnects.
Build the Pexels Request#
Use /v1/curated when the asset query has no search text and /v1/search for a non-empty query. Both requests carry the
one-based page and page size.
class HttpPexelsClient( private val apiKey: String, private val transport: PexelsTransport = UrlConnectionPexelsTransport,) : PexelsClient { override suspend fun searchPhotos( query: String, page: Int, perPage: Int, ): PexelsResponse = requestPexels( apiKey = apiKey, transport = transport, endpoint = "search", queryParameters = mapOf( "query" to query, "page" to page.toString(), "per_page" to perPage.toString(), ), )
override suspend fun curatedPhotos( page: Int, perPage: Int, ): PexelsResponse = requestPexels( apiKey = apiKey, transport = transport, endpoint = "curated", queryParameters = mapOf( "page" to page.toString(), "per_page" to perPage.toString(), ), )}In a production integration, inject a PexelsClient implementation that calls your proxy instead of HttpPexelsClient. The
proxy-backed client sends only your app’s request parameters; the proxy chooses or validates the upstream endpoint and adds the
Pexels credential.
Load the Pexels Response#
Build the URI, add the Authorization header, reject unsuccessful responses, and decode the response body. The sample accepts
a transport dependency so tests can exercise the same client without live credentials.
suspend fun requestPexels( apiKey: String, transport: PexelsTransport, endpoint: String, queryParameters: Map<String, String>,): PexelsResponse { val uriBuilder = Uri.parse("https://api.pexels.com/v1") .buildUpon() .appendPath(endpoint) queryParameters.forEach { (key, value) -> uriBuilder.appendQueryParameter(key, value) }
val response = transport.execute( PexelsHttpRequest( uri = uriBuilder.build(), headers = mapOf("Authorization" to apiKey), ), ) require(response.statusCode == HttpURLConnection.HTTP_OK) { response.body.ifBlank { "Pexels request failed with HTTP ${response.statusCode}." } } return PexelsResponse.fromJson(JSONObject(response.body))}The runnable sample sends the key from Android for transparency. In production, omit that provider credential from the app request and let your backend add it when calling Pexels.
Parse Pexels Responses#
Decode the Response#
Decode the photo array together with Pexels’ documented total_results and optional next_page fields.
fun fromJson(json: JSONObject): PexelsResponse { val photosJson = json.getJSONArray("photos") val photos = (0 until photosJson.length()).map { index -> PexelsPhoto.fromJson(photosJson.getJSONObject(index)) } return PexelsResponse( photos = photos, totalResults = if (json.has("total_results")) json.getInt("total_results") else null, hasNextPage = photos.isNotEmpty() && getNullableJsonString(json, "next_page")?.isNotBlank() == true, )}The sample keeps totalResults nullable as defensive handling for an unexpected response that omits the documented field.
hasNextPage requires both a non-empty photo page and a usable next_page value, matching the iOS integration’s defensive
pagination behavior.
Build the Paginated Result#
Translate CE.SDK’s zero-based page to Pexels’ one-based page, select curated or search behavior from the query text, and return
the mapped page as a FindAssetsResult.
suspend fun findPexelsAssets( client: PexelsClient, sourceId: String, query: FindAssetsQuery,): FindAssetsResult { val pexelsPage = query.page + 1 val searchQuery = query.query?.takeIf { it.isNotBlank() } val response = if (searchQuery == null) { client.curatedPhotos(page = pexelsPage, perPage = query.perPage) } else { client.searchPhotos( query = searchQuery, page = pexelsPage, perPage = query.perPage, ) }
val assets = response.photos.map { photo -> toPexelsAsset(photo = photo, sourceId = sourceId) } return FindAssetsResult( assets = assets, currentPage = query.page, nextPage = if (response.hasNextPage) query.page + 1 else -1, total = response.totalResults ?: -1, )}nextPage is -1 when Pexels omits next_page. The response’s total_results becomes FindAssetsResult.total; the
sample uses -1 only as a defensive fallback.
Map Photos to Assets#
Derive MIME metadata from the full-resolution image URL and omit it when the extension is unknown:
fun getPexelsMimeType(uri: String): String? { val extension = Uri.parse(uri).lastPathSegment?.substringAfterLast('.', missingDelimiterValue = "")?.lowercase() return when (extension) { "jpg", "jpeg" -> MimeType.JPEG.key "png" -> MimeType.PNG.key else -> null }}Map each Pexels photo to CE.SDK’s Android Asset model:
fun toPexelsAsset( photo: PexelsPhoto, sourceId: String,) = Asset( id = photo.id.toString(), context = AssetContext(sourceId = sourceId), label = photo.alt?.takeIf { it.isNotBlank() }, locale = "en", meta = buildMap { put("uri", photo.src.original) put("thumbUri", photo.src.medium) getPexelsMimeType(photo.src.original)?.let { put("mimeType", it) } put("blockType", DesignBlockType.Graphic.key) put("fillType", FillType.Image.key) put("shapeType", ShapeType.Rect.key) put("kind", "image") put("width", photo.width.toString()) put("height", photo.height.toString()) }, credits = AssetCredits( name = photo.photographer, uri = Uri.parse(photo.url), ), utm = AssetUTM( source = "cesdk-android", medium = "referral", ),)The mapping provides:
uriandthumbUrifrom Pexels’ original and medium-size image URLs.- Image dimensions and the block, fill, shape, and kind hints used by
defaultApplyAsset(). - Photographer credits linked to the public Pexels photo page.
AssetContext.sourceIdso CE.SDK retains source provenance.- Referral metadata through
AssetUTM.
Create a Scene for Applying Assets#
engine.asset.applyAssetSourceAsset() inserts the returned asset into the active scene. The sample creates a square page
before querying Pexels; in your app, you can use any active scene that already contains a page.
val scene = engine.scene.create()val page = engine.block.create(DesignBlockType.Page)engine.block.setWidth(block = page, value = 1080F)engine.block.setHeight(block = page, value = 1080F)engine.block.appendChild(parent = scene, child = page)Query and Apply Images Programmatically#
Create and register the source on an existing Engine. Check the registered IDs first so repeated setup does not add the same
source twice.
val source = PexelsAssetSource(client = client)if (source.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(source)}An empty query loads curated photos. A non-empty query searches Pexels:
val curated = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(page = 0, perPage = 10),)
val search = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(page = 0, perPage = 10, query = "workspace"),)Pass the selected source ID explicitly when applying an asset:
val firstBlock = (search.assets.firstOrNull() ?: curated.assets.firstOrNull())?.let { asset -> engine.asset.applyAssetSourceAsset( sourceId = source.sourceId, asset = asset, )}applyAssetSourceAsset() returns the created block when CE.SDK can materialize the asset. Check for null before positioning
or resizing the result.
Add Pexels to the Asset Library UI#
To expose Pexels in the CE.SDK editor UI, keep one source instance across recompositions, register it after the editor has loaded, and add a section for that source to the Images asset library. For a complete editor surface, see the Design Editor Starter Kit.
Create the source outside the configuration builder so recomposition does not replace it:
val pexelsAssetSource = remember(pexelsClient) { PexelsAssetSource(client = pexelsClient)}Register the source from onLoaded. The duplicate check keeps repeated setup safe:
onLoaded = { val engine = editorContext.engine if (pexelsAssetSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(pexelsAssetSource) }}The asset library section points at the same source ID and expands into a searchable image grid. Starting with
AssetLibrary.getDefault() preserves the other default library categories:
assetLibrary = { remember { val pexelsSourceType = AssetSourceType(sourceId = pexelsAssetSource.sourceId) val pexelsSection = LibraryContent.Section( sourceTypes = listOf(pexelsSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = pexelsSourceType, assetType = AssetType.Image, title = "Pexels", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(pexelsSection), ) }}An empty query shows curated photos, while entering text in the grid switches the same source to Pexels search.
Handle Licensing and Attribution#
The Pexels license does not require attribution, but the Pexels API guidelines require a prominent Pexels link and ask integrations to credit photographers when possible. Preserve both levels of attribution:
- Source-level
creditsandlicenseidentify Pexels and link to its terms. - Per-asset
creditsidentify the photographer and link to the public photo page. AssetUTMadds the recommended referral parameters to outgoing credit links.
CE.SDK can surface asset credit metadata in asset details. Keep the provider link in app-owned UI so it stays visible without a custom dock or editor configuration builder:
const val PEXELS_PROVIDER_URI = "https://www.pexels.com/?utm_source=cesdk-android&utm_medium=referral"
@Composablefun PexelsProviderLink(onOpenUri: ((Uri) -> Unit)? = null) { val uriHandler = LocalUriHandler.current val providerUri = remember { Uri.parse(PEXELS_PROVIDER_URI) } TextButton( onClick = { onOpenUri?.invoke(providerUri) ?: uriHandler.openUri(providerUri.toString()) }, ) { Text("Photos provided by Pexels") }}Review Pexels’ current API guidelines and rate limits before shipping.
Troubleshooting#
- Authentication errors: Confirm the proxy adds the Pexels API key in the
Authorizationheader. For the direct sample, confirm the configured development key is present. - Rate limiting: Handle HTTP 429 on the proxy, cache results where permitted, and back off before retrying.
- Empty results: Confirm an empty CE.SDK query calls
/curated, a non-empty query calls/search, and the response includes aphotosarray. - Repeated or skipped pages: Confirm CE.SDK’s zero-based page is converted to Pexels’ one-based page exactly once.
- Images fail to insert: Verify
src.originalandsrc.mediumare reachable from the device and the mapped asset contains the image block metadata shown above. - Missing provider link: Keep the app-owned provider action visible whenever Pexels API content is available.
- Missing photographer credit: Confirm the Pexels response contains
photographerandurland that both are mapped to the asset’scredits. - Missing default library content: Start with
AssetLibrary.getDefault()and add the Pexels section to its Images category.
API Reference#
| Method | Purpose |
|---|---|
engine.asset.addSource(source=_) |
Register the Pexels AssetSource. |
engine.asset.findAllSources() |
Check whether the source is already registered. |
engine.asset.findAssets(sourceId=_, query=_) |
Query curated or searched Pexels results. |
engine.asset.applyAssetSourceAsset(sourceId=_, asset=_) |
Insert a returned Pexels asset through its source. |
AssetSource.findAssets(query=_) |
Implement Pexels-backed browsing and search. |
AssetSource.getGroups() |
Return optional source groups, or null when none exist. |
engine.scene.create() |
Create the active scene used by the programmatic apply call. |
engine.block.create(blockType=_) |
Create the page used by the sample scene. |
engine.block.setWidth(block=_, value=_) |
Set the sample page width. |
engine.block.setHeight(block=_, value=_) |
Set the sample page height. |
engine.block.appendChild(parent=_, child=_) |
Attach the page to the scene. |
EditorConfiguration.remember(builder=_) |
Configure source registration and asset library UI. |
AssetLibrary.getDefault(images=_) |
Keep the default asset library and customize Images. |
LibraryCategory.Images.addSection(section=_) |
Append the Pexels preview to the Images category. |
LibraryContent.Section(sourceTypes=_, assetType=_, expandContent=_) |
Define the compact Pexels preview. |
LibraryContent.Grid(titleRes=_, sourceType=_, assetType=_, title=_) |
Define the paginated Pexels image grid. |
AssetSourceType(sourceId=_) |
Connect library content to the Pexels source. |
Related Types#
| Type | Purpose |
|---|---|
AssetSource |
Base class for custom Android asset providers. |
FindAssetsQuery |
Search text, paging, and filter data from CE.SDK. |
FindAssetsResult |
Paginated asset response returned to CE.SDK. |
Asset |
One mapped Pexels photo in CE.SDK’s Android asset model. |
AssetContext |
Source provenance associated with an asset. |
AssetCredits |
Provider-level or photographer-level credit metadata. |
AssetLibrary |
Editor UI asset library configuration. |
LibraryCategory |
A category such as the default Images library. |
LibraryContent.Section |
A preview section inside an asset library category. |
Next Steps#
- Customize Asset Library - Configure asset panels and UI
- Asset Library Basics - Understand asset sources
- Integrate Unsplash Images - Add another stock image source
- From Remote Source - Explore other remote asset integrations
- Import Media Concepts - Learn core import concepts