Skip to Content
ProvidersUnsplash

Unsplash

Category: Stock Images
Integration type: Platform-level API key
External API: Unsplash API v1


Purpose

Unsplash provides high-quality professional photography free for commercial use under the Unsplash License. It complements Pixabay with a more curated, higher-resolution image library — better suited for blog featured images, hero sections, and premium client work.

Unsplash is a platform-level integration. The free API tier allows 50 requests/hour; production apps can apply for higher limits.


Config Structure

Platform config (env vars)

UNSPLASH_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # From unsplash.com/oauth/applications UNSPLASH_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # For OAuth (not needed for search) UNSPLASH_API_BASE_URL=https://api.unsplash.com

Unsplash uses the Authorization: Client-ID <access_key> pattern for API calls.


Integration Pattern

Tool layer (packages/tools/src/unsplash.ts)

import axios from 'axios'; class UnsplashTool { constructor( private accessKey: string, private baseUrl: string = 'https://api.unsplash.com', ) {} private headers() { return { Authorization: `Client-ID ${this.accessKey}` }; } async searchPhotos(options: { query: string; page?: number; // Default: 1 perPage?: number; // 1–30, Default: 10 orderBy?: 'relevant' | 'latest'; orientation?: 'landscape' | 'portrait' | 'squarish'; color?: string; // e.g. 'black_and_white', 'blue', 'green' contentFilter?: 'low' | 'high'; // Default: 'low' (SFW) }): Promise<UnsplashSearchResult> { const response = await axios.get(`${this.baseUrl}/search/photos`, { headers: this.headers(), params: { query: options.query, page: options.page ?? 1, per_page: options.perPage ?? 10, order_by: options.orderBy ?? 'relevant', orientation: options.orientation, color: options.color, content_filter: options.contentFilter ?? 'low', }, }); return { total: response.data.total, totalPages: response.data.total_pages, photos: response.data.results.map((p: any) => this.mapPhoto(p)), }; } async getPhoto(photoId: string): Promise<UnsplashPhoto> { const response = await axios.get(`${this.baseUrl}/photos/${photoId}`, { headers: this.headers(), }); return this.mapPhoto(response.data); } async getRandomPhoto(options: { query?: string; orientation?: 'landscape' | 'portrait' | 'squarish'; count?: number; // 1–30 } = {}): Promise<UnsplashPhoto[]> { const response = await axios.get(`${this.baseUrl}/photos/random`, { headers: this.headers(), params: { query: options.query, orientation: options.orientation, count: options.count ?? 1, }, }); const data = Array.isArray(response.data) ? response.data : [response.data]; return data.map(p => this.mapPhoto(p)); } // Trigger a download event — required by Unsplash API guidelines when an image is actually used async triggerDownload(downloadUrl: string): Promise<void> { await axios.get(downloadUrl, { headers: this.headers() }); } private mapPhoto(p: any): UnsplashPhoto { return { id: p.id, description: p.description ?? p.alt_description, width: p.width, height: p.height, color: p.color, // Dominant colour as hex urls: { raw: p.urls.raw, // Original, unprocessed full: p.urls.full, // Full resolution JPEG regular: p.urls.regular, // ~1080px wide small: p.urls.small, // ~400px wide thumb: p.urls.thumb, // ~200px wide }, photographer: { name: p.user.name, username: p.user.username, profileUrl: p.user.links.html, }, downloadUrl: p.links.download_location, // For triggering download event pageUrl: p.links.html, }; } async verify(): Promise<void> { const response = await axios.get(`${this.baseUrl}/me`, { headers: this.headers(), }); // /me returns 401 for Client-ID auth — that's expected; check rate limit headers instead // A 200 on /photos proves the key works const testResponse = await axios.get(`${this.baseUrl}/photos`, { headers: this.headers(), params: { per_page: 1 }, }); if (!Array.isArray(testResponse.data)) { throw new Error('Unsplash API verification failed'); } } }

Required: trigger download on use

Unsplash API guidelines require that when a photo is actually used (not just searched), you call the download_location URL. This lets Unsplash track photo usage and credit photographers:

// When a photo is selected and about to be embedded const photo = await unsplash.getPhoto(selectedPhotoId); await unsplash.triggerDownload(photo.downloadUrl); // Then download and store const imageBuffer = await axios.get(photo.urls.regular, { responseType: 'arraybuffer' }); const s3Key = `media/${tenantId}/unsplash/${activityId}/${photo.id}.jpg`; await s3.upload({ key: s3Key, body: Buffer.from(imageBuffer.data), contentType: 'image/jpeg' });

Attribution Requirements

The Unsplash License is free for commercial use but requires photo credit in a reasonable place:

Photo by Photographer Name on Unsplash 

For blog posts, this should be added as a caption or footer credit. The photographer object in UnsplashPhoto contains all attribution fields.


Pixabay vs Unsplash — Selection Guide

Use casePreferred source
Blog featured image (premium look)Unsplash
Social post background (quick)Pixabay
Illustrations / vectorsPixabay only
Very specific niche subjectPixabay (larger database)
High-resolution (print quality)Unsplash
Zero attribution obligationPixabay (stricter commercial freedom)

The agent can be instructed to prefer one over the other per content type. The default search order is: Unsplash first for blog posts; Pixabay first for social and GBP posts.


Test Cases

Unit tests (packages/tools/src/unsplash.test.ts)

TestApproach
searchPhotos() sends Client-ID auth headerMock axios.get; assert Authorization: Client-ID ...
searchPhotos() maps all URL variantsMock urls object; assert regular, full, thumb
searchPhotos() maps alt_description when description is nullMock { description: null, alt_description: 'A dog' }
triggerDownload() GETs the download location URLAssert called with downloadUrl
getRandomPhoto() wraps single photo in arrayMock non-array response; assert [photo] returned
verify() calls /photos endpointAssert GET to /photos with per_page: 1
Throws on 403 (invalid access key)Mock 403; assert error

Rate Limits

PlanRate limit
Demo50 requests/hour
Production (approved)5000 requests/hour

Apply for production access via the Unsplash developer portal after launching. Demo mode watermarks images in the API response; production mode does not.


© 2026 Leadmetrics — Internal use only