//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use the Pixabay API to search and display free stock photos and videos. Pixabay images are released under their own license, which allows free commercial use without attribution (though attribution is appreciated).
Step 1: Get an API key
.env.local:PIXABAY_API_KEY=your_api_key_hereStep 2: Create an API client
// lib/pixabay.ts
interface PixabayImage {
id: number;
pageURL: string;
type: string;
tags: string;
previewURL: string;
previewWidth: number;
previewHeight: number;
webformatURL: string;
webformatWidth: number;
webformatHeight: number;
largeImageURL: string;
imageWidth: number;
imageHeight: number;
views: number;
downloads: number;
likes: number;
user: string;
userImageURL: string;
}
interface PixabaySearchResponse {
total: number;
totalHits: number;
hits: PixabayImage[];
}
type ImageType = "all" | "photo" | "illustration" | "vector";
type Orientation = "all" | "horizontal" | "vertical";
type Category =
| "backgrounds" | "fashion" | "nature" | "science" | "education"
| "feelings" | "health" | "people" | "religion" | "places"
| "animals" | "industry" | "computer" | "food" | "sports"
| "transportation" | "travel" | "buildings" | "business" | "music";
interface SearchParams {
query: string;
page?: number;
perPage?: number;
imageType?: ImageType;
orientation?: Orientation;
category?: Category;
}
const PIXABAY_BASE = "https://pixabay.com/api/";
export async function searchImages({
query,
page = 1,
perPage = 20,
imageType = "photo",
orientation = "all",
category,
}: SearchParams): Promise<PixabaySearchResponse> {
const params = new URLSearchParams({
key: process.env.PIXABAY_API_KEY!,
q: query,
page: String(page),
per_page: String(perPage),
image_type: imageType,
orientation,
safesearch: "true",
});
if (category) params.set("category", category);
const res = await fetch(`${PIXABAY_BASE}?${params}`, {
next: { revalidate: 3600 },
});
if (!res.ok) {
throw new Error(`Pixabay API error: ${res.status}`);
}
return res.json();
}A stock photo browser with category filtering:
// app/stock-photos/page.tsx
import { searchImages, type Category } from "@/lib/pixabay";
import { CategoryFilter } from "./category-filter";
import { PhotoGrid } from "./photo-grid";
const categories: Category[] = [
"nature", "people", "animals", "food", "travel",
"buildings", "business", "sports", "science", "fashion",
];
interface PageProps {
searchParams: Promise<{ q?: string; category?: string; page?: string }>;
}
export default async function StockPhotosPage({ searchParams }: PageProps) {
const params = await searchParams;
const query = params.q || "landscape";
const category = params.category as Category | undefined;
const page = Number(params.page) || 1;
const results = await searchImages({ query, page, perPage: 18, category });
return (
<main className="mx-auto max-w-6xl px-4 py-8">
<h1 className="mb-2 text-3xl font-bold">Stock Photos</h1>
<p className="mb-6 text-gray-500">
{results.totalHits.toLocaleString()} free photos found
</p>
<CategoryFilter categories={categories} activeCategory={category} query={query} />
<PhotoGrid images={results.hits} />
<div className="mt-8 flex justify-center gap-4">
{page > 1 && (
<a
href={`/stock-photos?q=${query}${category ? `&category=${category}` : ""}&page=${page - 1}`}
className="rounded-lg bg-gray-200 px-4 py-2 hover:bg-gray-300"
>
Previous
</a>
)}
{page * 18 < results.totalHits && (
<a
href={`/stock-photos?q=${query}${category ? `&category=${category}` : ""}&page=${page + 1}`}
className="rounded-lg bg-gray-200 px-4 py-2 hover:bg-gray-300"
>
Next
</a>
)}
</div>
</main>
);
}// app/stock-photos/category-filter.tsx
"use client";
import { useRouter } from "next/navigation";
interface CategoryFilterProps {
categories: string[];
activeCategory?: string;
query: string;
}
export function CategoryFilter({ categories, activeCategory, query }: CategoryFilterProps) {
const router = useRouter();
function selectCategory(category: string | null) {
const params = new URLSearchParams({ q: query });
if (category) params.set("category", category);
router.push(`/stock-photos?${params}`);
}
return (
<div className="mb-6 flex flex-wrap gap-2">
<button
onClick={() => selectCategory(null)}
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${
!activeCategory
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
All
</button>
{categories.map((cat) => (
<button
key={cat}
onClick={() => selectCategory(cat)}
className={`rounded-full px-4 py-1.5 text-sm font-medium capitalize transition-colors ${
activeCategory === cat
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{cat}
</button>
))}
</div>
);
}// app/stock-photos/photo-grid.tsx
import Image from "next/image";
interface PixabayImage {
id: number;
webformatURL: string;
largeImageURL: string;
tags: string;
user: string;
likes: number;
webformatWidth: number;
webformatHeight: number;
}
export function PhotoGrid({ images }: { images: PixabayImage[] }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{images.map((image) => (
<figure key={image.id} className="group relative overflow-hidden rounded-xl bg-gray-100">
<div className="relative aspect-[4/3]">
<Image
src={image.webformatURL}
alt={image.tags}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover"
/>
</div>
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-between bg-gradient-to-t from-black/50 to-transparent p-3 text-sm text-white opacity-0 transition-opacity group-hover:opacity-100">
<span>by {image.user}</span>
<span>{image.likes} likes</span>
</div>
</figure>
))}
</div>
);
}key query parameter (not a header)./api/) accepts parameters including q (search term), image_type, orientation, category, min_width, min_height, colors, per_page (3-200), and page.totalHits (capped at 500 for non-whitelisted users), total (true total), and hits (array of image objects).previewURL (150px), webformatURL (640px), largeImageURL (1280px), and fullHDURL / imageURL (available with full API access)./api/videos/ with similar parameters and returns video objects with multiple resolution URLs.Video search:
interface PixabayVideo {
id: number;
pageURL: string;
tags: string;
videos: {
large: { url: string; width: number; height: number };
medium: { url: string; width: number; height: number };
small: { url: string; width: number; height: number };
tiny: { url: string; width: number; height: number };
};
user: string;
}
interface PixabayVideoResponse {
total: number;
totalHits: number;
hits: PixabayVideo[];
}
export async function searchVideos(query: string, page = 1): Promise<PixabayVideoResponse> {
const params = new URLSearchParams({
key: process.env.PIXABAY_API_KEY!,
q: query,
page: String(page),
per_page: "12",
});
const res = await fetch(`https://pixabay.com/api/videos/?${params}`, {
next: { revalidate: 3600 },
});
return res.json();
}image_type, orientation, and category.totalHits is capped at 500 for standard API users. To access more results, apply for full API access.webformatURL images have a Pixabay watermark in some cases for non-logged-in requests. Ensure your API key is valid.per_page parameter accepts values between 3 and 200. Values outside this range return an error.cdn.pixabay.com and pixabay.com to remotePatterns in next.config.ts for next/image to work.| Approach | Pros | Cons |
|---|---|---|
| Pixabay API | Free commercial use, no attribution required, videos too | Lower quality average, 500-hit cap |
| Unsplash API | Higher quality photos, larger library | Attribution required |
| Pexels API | Good quality, generous limits, no attribution required | Smaller library |
| Flickr API | Massive library, Creative Commons options | Complex licensing per photo |
key query parameter, not a header.previewURL -- 150px wide.webformatURL -- 640px wide.largeImageURL -- 1280px wide.fullHDURL and imageURL are available with full API access only.totalHits is capped at 500 for standard (non-whitelisted) API users.total shows the true total, but you cannot paginate past 500 results.const res = await fetch(`https://pixabay.com/api/videos/?${params}`, {
next: { revalidate: 3600 },
});Use the /api/videos/ endpoint with similar parameters as the image search.
ImageType, Orientation, and Category.images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.pixabay.com" },
{ protocol: "https", hostname: "pixabay.com" },
],
}useRouter().push() to navigate with updated query parameters.selectCategory function builds a URLSearchParams object with the query and optional category.searchParams and fetches filtered results.Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥