React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

pixabayapifree-imagesstock-photosvideo

Pixabay API

Recipe

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

  1. Create an account at pixabay.com.
  2. Visit pixabay.com/api/docs to find your API key.
  3. Store the key in .env.local:
PIXABAY_API_KEY=your_api_key_here

Step 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();
}

Working Example

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>
  );
}

Deep Dive

How It Works

  • The Pixabay API is a REST API that returns JSON. Authentication is via a key query parameter (not a header).
  • The image search endpoint (/api/) accepts parameters including q (search term), image_type, orientation, category, min_width, min_height, colors, per_page (3-200), and page.
  • Response includes totalHits (capped at 500 for non-whitelisted users), total (true total), and hits (array of image objects).
  • Each hit includes multiple image URLs: previewURL (150px), webformatURL (640px), largeImageURL (1280px), and fullHDURL / imageURL (available with full API access).
  • The video search endpoint is at /api/videos/ with similar parameters and returns video objects with multiple resolution URLs.
  • Rate limit is 100 requests per minute. Exceeding it returns a 429 status code.

Variations

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();
}

TypeScript Notes

  • The Pixabay API does not provide an official TypeScript SDK. Define your own types based on the API documentation.
  • Use union types for constrained parameters like image_type, orientation, and category.
  • Export the types alongside the API functions so consuming components can reference them.

Gotchas

  • The API key is passed as a query parameter, not a header. Never use this in client-side code, as the key will be visible in network requests. Always call from Server Components or API routes.
  • 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.
  • The per_page parameter accepts values between 3 and 200. Values outside this range return an error.
  • Pixabay requires that hotlinking to images from their CDN is temporary. For production use, download and serve images from your own infrastructure.
  • Add cdn.pixabay.com and pixabay.com to remotePatterns in next.config.ts for next/image to work.

Alternatives

ApproachProsCons
Pixabay APIFree commercial use, no attribution required, videos tooLower quality average, 500-hit cap
Unsplash APIHigher quality photos, larger libraryAttribution required
Pexels APIGood quality, generous limits, no attribution requiredSmaller library
Flickr APIMassive library, Creative Commons optionsComplex licensing per photo

FAQs

How does Pixabay API authentication differ from Unsplash and Pexels?
  • Pixabay passes the API key as a key query parameter, not a header.
  • This means the key is visible in the URL, so never use it in client-side code.
  • Always call from Server Components or API routes.
What image sizes does the Pixabay API return in each hit?
  • previewURL -- 150px wide.
  • webformatURL -- 640px wide.
  • largeImageURL -- 1280px wide.
  • fullHDURL and imageURL are available with full API access only.
Is attribution required for Pixabay images?
  • No, attribution is not legally required under the Pixabay license.
  • Images are free for commercial use without attribution.
  • However, attribution is appreciated.
What is the rate limit for the Pixabay API?
  • 100 requests per minute.
  • Exceeding it returns a 429 status code.
Gotcha: What is the totalHits cap, and how does it affect pagination?
  • totalHits is capped at 500 for standard (non-whitelisted) API users.
  • total shows the true total, but you cannot paginate past 500 results.
  • Apply for full API access to remove this cap.
What per_page values does the Pixabay API accept?
  • Between 3 and 200 inclusive.
  • Values outside this range return an error.
How do you search for videos instead of images with Pixabay?
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.

What TypeScript types should you define for the Pixabay API?
  • Define your own types since Pixabay has no official TypeScript SDK.
  • Use union types for constrained params like ImageType, Orientation, and Category.
  • Export types alongside API functions for consuming components.
Gotcha: Can you hotlink directly to Pixabay CDN images in production?
  • Pixabay requires that hotlinking to their CDN is temporary only.
  • For production use, download and serve images from your own infrastructure.
What next.config.ts changes are needed for Pixabay images with next/image?
images: {
  remotePatterns: [
    { protocol: "https", hostname: "cdn.pixabay.com" },
    { protocol: "https", hostname: "pixabay.com" },
  ],
}
How does the category filter component update the URL in the example?
  • It uses useRouter().push() to navigate with updated query parameters.
  • The selectCategory function builds a URLSearchParams object with the query and optional category.
  • The page Server Component reads searchParams and fetches filtered results.