To integrate Replit with Shutterstock API, create an app on developers.shutterstock.com to get a client ID and secret, store them in Replit Secrets (lock icon π), and call the Shutterstock REST API for searching images, videos, and music. Use HTTP Basic Auth (client ID + secret) for read-only search operations, or implement OAuth 2.0 for user-account actions like licensing. Use an Autoscale deployment for stock media search APIs.
Shutterstock API Integration from Replit
Shutterstock is one of the world's largest stock media marketplaces, with over 400 million images, videos, and music tracks available for licensing. The Shutterstock API enables developers to build media search experiences, integrate stock content into workflows, automate asset sourcing for content teams, and build tools that help users find and license media programmatically. Use cases range from creative workflow tools and CMS plugins to automated social media content sourcing and design tools.
Shutterstock's API has two authentication modes. HTTP Basic Auth (client ID + secret) is sufficient for all read operations β searching the catalog, retrieving image metadata, and getting preview URLs (low-resolution watermarked images). OAuth 2.0 is required for write operations that act on behalf of a specific user β licensing images, downloading purchased assets, and managing subscriptions. For a Replit integration that powers a search interface, Basic Auth is all you need.
The API is organized around media types: images (v2/images/search), videos (v2/videos/search), and music (v2/music/search). Each search endpoint returns results with ID, description, contributor, aspect ratio, keywords, preview URL, and URLs for different preview sizes. Licensed downloads are retrieved from a separate endpoint that requires a valid Shutterstock subscription or credits on the user's account.
Integration method
Shutterstock provides a REST API for searching and licensing stock media. For server-to-server search operations (image search, video search, contributor data), use HTTP Basic Auth with your app's client ID and secret encoded as Base64 in the Authorization header. For operations that require user accounts (purchasing licenses on behalf of a user), implement OAuth 2.0. Your Replit server stores the API credentials in Replit Secrets and makes authenticated requests to the Shutterstock API.
Prerequisites
- A Replit account with a Node.js or Python Repl ready
- A Shutterstock account at shutterstock.com
- A developer app registered at developers.shutterstock.com with a client ID and secret
Step-by-step guide
Create a Shutterstock Developer App
Create a Shutterstock Developer App
Navigate to developers.shutterstock.com and sign in with your Shutterstock account. If this is your first time, complete the developer registration. Click 'Create App' or navigate to 'My Apps' and create a new application. Fill in the app details: Application Name (e.g., My Replit Integration), and a brief description of your use case. For the callback URL (required even if you are only using Basic Auth for now), you can enter https://localhost β this is only used for OAuth flows. After creating the app, you will see your Client ID (also called Consumer Key) and Client Secret (also called Consumer Secret). These are the credentials you will use for Basic Auth API calls. Copy both values. Shutterstock has different API access tiers. The free developer tier allows a limited number of searches per month and returns preview (watermarked) images. To license and download full-resolution images, you need an active Shutterstock subscription or credits on your account, and the licensed user must have agreed to Shutterstock's licensing terms. For testing search functionality, the free tier is sufficient β you can search the full catalog and retrieve preview URLs without a subscription. The preview URLs serve low-resolution, watermarked JPEGs that are suitable for user-facing search interfaces but cannot be used in production media. Verify your app is working by making a simple test request: the API endpoint is https://api.shutterstock.com/v2/images/search?query=nature&per_page=3 with your credentials in the Authorization header.
Pro tip: Shutterstock's free tier has rate limits. For production applications with high search volume, you may need to apply for a commercial API plan. Check developers.shutterstock.com/plans for current limits and commercial options.
Expected result: A Shutterstock developer app exists with a Client ID and Client Secret. Basic Auth API calls to https://api.shutterstock.com/v2/images/search return search results.
Store Shutterstock Credentials in Replit Secrets
Store Shutterstock Credentials in Replit Secrets
Click the lock icon (π) in the left Replit sidebar to open the Secrets pane. Add the following secrets: SHUTTERSTOCK_CLIENT_ID: your Shutterstock app's Client ID. SHUTTERSTOCK_CLIENT_SECRET: your Shutterstock app's Client Secret. For Basic Auth, the Authorization header is constructed by Base64-encoding clientId:clientSecret. In Node.js: Buffer.from(`${clientId}:${clientSecret}`).toString('base64'). In Python: base64.b64encode(f'{client_id}:{client_secret}'.encode()).decode(). Unlike some APIs where the client secret is highly sensitive (it can create OAuth tokens), Shutterstock's Basic Auth client credentials primarily enable catalog search. They cannot be used to license images or make purchases β those require user OAuth tokens linked to a paying Shutterstock account. That said, the client credentials should still be kept in Secrets rather than code files, following the principle of least exposure. If you plan to implement OAuth 2.0 for licensing, you will also need to store SHUTTERSTOCK_REDIRECT_URI and obtain user access tokens through the OAuth flow.
1// check-shutterstock-secrets.js2const required = ['SHUTTERSTOCK_CLIENT_ID', 'SHUTTERSTOCK_CLIENT_SECRET'];3for (const key of required) {4 if (!process.env[key]) {5 throw new Error(`Missing secret: ${key}. Set it in Replit Secrets (lock icon π).`);6 }7}8const auth = Buffer.from(9 `${process.env.SHUTTERSTOCK_CLIENT_ID}:${process.env.SHUTTERSTOCK_CLIENT_SECRET}`10).toString('base64');11console.log('Shutterstock auth header ready.');12console.log('Client ID:', process.env.SHUTTERSTOCK_CLIENT_ID);Pro tip: Construct the Basic Auth header once at server startup and reuse it for all requests rather than recalculating it on every request. This is a minor optimization but makes the code cleaner.
Expected result: SHUTTERSTOCK_CLIENT_ID and SHUTTERSTOCK_CLIENT_SECRET appear in Replit Secrets. The check script builds and displays the auth header prefix without errors.
Search Images and Videos with Shutterstock API (Node.js)
Search Images and Videos with Shutterstock API (Node.js)
Install required packages in the Shell tab: npm install axios express. The Shutterstock API base URL is https://api.shutterstock.com. All search endpoints are under /v2/. The image search endpoint (GET /v2/images/search) accepts many query parameters: query (search terms), per_page (1-500, default 20), page (for pagination), image_type (photo, illustration, vector), orientation (horizontal, vertical, square), sort (popular, newest, relevance), safe (safe search on/off), and color (hex color code for dominant color filtering). Each image result includes: id, description, contributor.id, aspect, media_type, and assets object with preview URLs at different sizes (small_thumb, large_thumb, huge_thumb). The preview property contains the direct URL to the watermarked preview image. The is_editorial flag indicates images that can only be used for editorial purposes (news, documentary, educational) and not for commercial use. For video search, the endpoint is GET /v2/videos/search with similar parameters. Video results include duration, aspect, and assets with low-resolution preview URLs. Handle pagination with the page parameter. Each response includes a total_count field indicating the total number of matching results. Calculate total pages as Math.ceil(totalCount / perPage). Rate limiting: Shutterstock returns X-Ratelimit-Limit, X-Ratelimit-Remaining, and X-Ratelimit-Reset headers. Check these headers and implement backoff if remaining approaches zero.
1// shutterstock.js β Shutterstock API integration for Node.js on Replit2const axios = require('axios');3const express = require('express');45const app = express();6app.use(express.json());78// Build Basic Auth header once9const authHeader = 'Basic ' + Buffer.from(10 `${process.env.SHUTTERSTOCK_CLIENT_ID}:${process.env.SHUTTERSTOCK_CLIENT_SECRET}`11).toString('base64');1213const sstockApi = axios.create({14 baseURL: 'https://api.shutterstock.com',15 headers: {16 'Authorization': authHeader,17 'Accept': 'application/json'18 }19});2021// Search images22app.get('/api/images/search', async (req, res) => {23 const {24 query, per_page = 20, page = 1,25 image_type, orientation, sort = 'popular',26 safe = 'true', color27 } = req.query;28 29 if (!query) return res.status(400).json({ error: 'query parameter required' });30 31 const params = {32 query, per_page: Math.min(parseInt(per_page), 100),33 page: parseInt(page), sort, safe34 };35 if (image_type) params.image_type = image_type;36 if (orientation) params.orientation = orientation;37 if (color) params.color = color.replace('#', '');38 39 try {40 const response = await sstockApi.get('/v2/images/search', { params });41 const { data } = response;42 43 // Return simplified results with preview URLs44 const images = data.data.map(img => ({45 id: img.id,46 description: img.description,47 contributor: img.contributor?.id,48 aspect: img.aspect,49 image_type: img.media_type,50 is_editorial: img.is_editorial,51 preview_url: img.assets?.preview?.url,52 thumb_url: img.assets?.small_thumb?.url,53 large_thumb_url: img.assets?.large_thumb?.url54 }));55 56 res.json({57 images,58 total_count: data.total_count,59 page: data.page,60 per_page: data.per_page,61 total_pages: Math.ceil(data.total_count / data.per_page)62 });63 } catch (err) {64 if (err.response?.status === 429) {65 const reset = err.response.headers['x-ratelimit-reset'];66 return res.status(429).json({ error: 'Rate limit exceeded', resetAt: reset });67 }68 res.status(err.response?.status || 500).json({ error: err.response?.data || err.message });69 }70});7172// Search videos73app.get('/api/videos/search', async (req, res) => {74 const { query, per_page = 10, page = 1, duration_from, duration_to } = req.query;75 if (!query) return res.status(400).json({ error: 'query required' });76 77 const params = { query, per_page: Math.min(parseInt(per_page), 50), page: parseInt(page) };78 if (duration_from) params.duration_from = duration_from;79 if (duration_to) params.duration_to = duration_to;80 81 try {82 const response = await sstockApi.get('/v2/videos/search', { params });83 const videos = response.data.data.map(v => ({84 id: v.id,85 description: v.description,86 duration: v.duration,87 aspect: v.aspect,88 preview_url: v.assets?.preview_mp4?.url,89 thumb_url: v.assets?.thumb_webm?.url90 }));91 res.json({ videos, total_count: response.data.total_count });92 } catch (err) {93 res.status(err.response?.status || 500).json({ error: err.response?.data || err.message });94 }95});9697// Get image details by ID98app.get('/api/images/:imageId', async (req, res) => {99 try {100 const response = await sstockApi.get(`/v2/images/${req.params.imageId}`);101 res.json(response.data);102 } catch (err) {103 res.status(err.response?.status || 500).json({ error: err.response?.data || err.message });104 }105});106107app.listen(3000, '0.0.0.0', () => console.log('Shutterstock server running on port 3000'));Pro tip: Cache search results for identical queries to reduce API calls and stay within rate limits. A simple in-memory cache with a 15-minute TTL per query string handles most repeated searches without hitting the API each time.
Expected result: GET /api/images/search?query=nature returns image results with preview URLs. GET /api/videos/search?query=ocean returns video results. GET /api/images/{id} returns detailed image metadata.
Python Integration for Shutterstock API
Python Integration for Shutterstock API
For Python Replit projects, install requests and flask: pip install requests flask. Python's requests library supports Basic Auth through the auth parameter, which accepts a (username, password) tuple and handles Base64 encoding automatically β pass (client_id, client_secret) as the auth tuple. The search parameters and response structure are identical to the Node.js implementation. The images search endpoint returns a data array of image objects with nested assets. Access preview URLs at response['data'][i]['assets']['preview']['url']. For applications that need to cache results, use Python's functools.lru_cache() for simple in-memory caching or the cachetools library for TTL-based caching. Caching search results prevents redundant API calls for common queries and helps stay within rate limits. For production applications that process many image searches, implement query normalization (lowercase, strip extra spaces) before caching so 'Nature' and 'nature' hit the same cache entry.
1# shutterstock_api.py β Shutterstock API integration for Python on Replit2import os3import requests4from flask import Flask, request, jsonify56app = Flask(__name__)78CLIENT_ID = os.environ['SHUTTERSTOCK_CLIENT_ID']9CLIENT_SECRET = os.environ['SHUTTERSTOCK_CLIENT_SECRET']10BASE_URL = 'https://api.shutterstock.com'1112# requests handles Basic Auth encoding with the auth tuple13session = requests.Session()14session.auth = (CLIENT_ID, CLIENT_SECRET)15session.headers.update({'Accept': 'application/json'})1617def search_images(query: str, per_page: int = 20, page: int = 1,18 image_type: str = None, orientation: str = None,19 sort: str = 'popular') -> dict:20 params = {'query': query, 'per_page': min(per_page, 100),21 'page': page, 'sort': sort, 'safe': 'true'}22 if image_type:23 params['image_type'] = image_type24 if orientation:25 params['orientation'] = orientation26 27 response = session.get(f'{BASE_URL}/v2/images/search', params=params)28 response.raise_for_status()29 data = response.json()30 31 images = [{32 'id': img['id'],33 'description': img.get('description', ''),34 'aspect': img.get('aspect'),35 'image_type': img.get('media_type'),36 'is_editorial': img.get('is_editorial', False),37 'preview_url': img.get('assets', {}).get('preview', {}).get('url'),38 'thumb_url': img.get('assets', {}).get('small_thumb', {}).get('url')39 } for img in data.get('data', [])]40 41 return {42 'images': images,43 'total_count': data.get('total_count', 0),44 'page': data.get('page', page),45 'per_page': data.get('per_page', per_page)46 }4748@app.route('/api/images/search')49def image_search():50 query = request.args.get('query')51 if not query:52 return jsonify({'error': 'query parameter required'}), 40053 try:54 results = search_images(55 query=query,56 per_page=int(request.args.get('per_page', 20)),57 page=int(request.args.get('page', 1)),58 image_type=request.args.get('image_type'),59 orientation=request.args.get('orientation'),60 sort=request.args.get('sort', 'popular')61 )62 return jsonify(results)63 except requests.HTTPError as e:64 if e.response.status_code == 429:65 return jsonify({'error': 'Rate limit exceeded'}), 42966 return jsonify({'error': str(e)}), e.response.status_code6768if __name__ == '__main__':69 app.run(host='0.0.0.0', port=3000)Pro tip: Use requests.HTTPError handling with explicit status code checks β a 429 response means you have hit the rate limit and should back off, while a 401 means credentials are wrong. Each error type requires a different response.
Expected result: GET /api/images/search?query=mountains returns Shutterstock image results with preview URLs. 429 responses are handled with rate limit error messages.
Common use cases
Stock Media Search API
Build a search API that queries Shutterstock's catalog and returns image results with preview URLs. Use this as the backend for a design tool, content management system, or creative workflow application that needs to surface relevant stock imagery from natural language searches.
Build an Express endpoint that accepts a search query and optional filters (orientation, image_type, color), queries Shutterstock images API, and returns the top 20 results with thumbnail URLs and metadata.
Copy this prompt to try it in Replit
Automated Content Sourcing
Automate the process of finding relevant stock images for articles, blog posts, or social media content. Feed keywords or article topics to the Shutterstock search API and retrieve candidate images for human review and selection, reducing the time spent manually browsing stock media sites.
Create a script that reads a list of article headlines from a CSV, searches Shutterstock for relevant images for each headline, and returns a JSON report with suggested images, previews, and Shutterstock IDs for editorial review.
Copy this prompt to try it in Replit
Stock Media Asset Library
Build a curated asset library for your team by searching and saving Shutterstock image metadata (ID, description, preview URL) to a database. Allow team members to browse and select pre-approved stock media without leaving your internal tools.
Build a stock media library with a search API that queries Shutterstock, stores selected image metadata in a database, and serves a GET /library endpoint with the saved assets and their Shutterstock preview URLs.
Copy this prompt to try it in Replit
Troubleshooting
401 Unauthorized β Invalid client credentials
Cause: SHUTTERSTOCK_CLIENT_ID or SHUTTERSTOCK_CLIENT_SECRET in Replit Secrets contains a typo or extra whitespace, or the credentials belong to a different account than expected.
Solution: Open Replit Secrets, delete and re-enter both credentials. Log into developers.shutterstock.com to verify the app still exists and the credentials match. The Basic Auth format must be Base64(clientId:clientSecret) with a colon separator.
1// Verify credentials format2const raw = `${process.env.SHUTTERSTOCK_CLIENT_ID}:${process.env.SHUTTERSTOCK_CLIENT_SECRET}`;3console.log('Credentials string length:', raw.length);4console.log('Has colon:', raw.includes(':'));429 Too Many Requests β Rate limit exceeded
Cause: Your app has exceeded the Shutterstock API's rate limit for the free developer tier. Rate limits apply per app per time window.
Solution: Check the X-Ratelimit-Reset response header for when the limit resets. Implement request caching for repeated queries. For higher volume, apply for a commercial API plan at developers.shutterstock.com/plans.
1// Add rate limit info to error response2catch (err) {3 if (err.response?.status === 429) {4 const reset = new Date(err.response.headers['x-ratelimit-reset'] * 1000);5 return res.status(429).json({ error: 'Rate limited', resetAt: reset.toISOString() });6 }7}Preview images are watermarked and low resolution
Cause: This is expected behavior. Shutterstock's preview URLs serve watermarked, low-resolution images intended for layout and selection purposes only. Full resolution unlicensed images are not available through the API.
Solution: Preview images are intentional β they allow users to see what an image looks like before purchasing. To get full-resolution images, users must license them through a Shutterstock subscription or credits. Implement the OAuth 2.0 flow and licensing endpoint for paid downloads.
Empty results for valid search queries
Cause: Safe search is filtering out results, or the image_type or orientation filters are too restrictive for the query.
Solution: Try the same query on shutterstock.com to confirm results exist. Remove optional filters (image_type, orientation, color) to widen the search. Check that the query text is not accidentally URL-encoded twice.
1// Debug search parameters2console.log('Search params:', JSON.stringify(params));3console.log('Total count:', response.data.total_count);Best practices
- Store SHUTTERSTOCK_CLIENT_ID and SHUTTERSTOCK_CLIENT_SECRET in Replit Secrets (lock icon π) β keep them out of code files
- Cache search results for identical queries to reduce API usage and stay within rate limits β a 15-minute TTL cache handles most repeated searches
- Always check the X-Ratelimit-Remaining header and implement graceful backoff when it approaches zero
- Use the safe=true parameter for all user-facing searches to filter out inappropriate content
- Clearly communicate to users that preview images are watermarked samples and require a license for production use
- For high-traffic applications, apply for a commercial API plan rather than relying on the free developer tier
- Deploy as Autoscale on Replit β search requests are stateless and Autoscale handles variable traffic efficiently
- Normalize search queries (lowercase, trim whitespace) before caching so minor variations share cache entries
Alternatives
Getty Images specializes in premium editorial and celebrity photography, while Shutterstock focuses on high-volume commercial stock at lower per-image cost.
Pixabay offers completely free stock images and videos with a simpler API key authentication β ideal for projects with no budget for licensed stock media.
Vimeo is for user-uploaded video hosting with OAuth, while Shutterstock provides professionally produced commercial stock videos for licensing.
Frequently asked questions
How do I connect Replit to Shutterstock API?
Create an app at developers.shutterstock.com to get a Client ID and Secret, store them in Replit Secrets (lock icon π), and make HTTP requests to api.shutterstock.com with a Basic Auth header constructed by Base64-encoding your clientId:clientSecret. Use axios (Node.js) or the requests library (Python) with the auth parameter.
Does Shutterstock API require OAuth or just an API key?
For search operations (browsing the catalog, getting preview URLs), HTTP Basic Auth with your app's Client ID and Secret is sufficient. OAuth 2.0 is only required for operations that act on behalf of a user account β specifically licensing and downloading full-resolution images. Most Replit integrations only need Basic Auth for search functionality.
Can I use Shutterstock images for free with the API?
The API is free to use for search and preview purposes. The preview URLs return low-resolution, watermarked images suitable for layout selection. To use images in actual projects or publications, you need a Shutterstock license β either a subscription or on-demand credits purchased through shutterstock.com or via the API's licensing endpoint.
How do I store my Shutterstock credentials in Replit?
Click the lock icon (π) in the Replit sidebar and add SHUTTERSTOCK_CLIENT_ID and SHUTTERSTOCK_CLIENT_SECRET. Access them with process.env.SHUTTERSTOCK_CLIENT_ID (Node.js) or os.environ['SHUTTERSTOCK_CLIENT_ID'] (Python). Build the Basic Auth header by Base64-encoding both values joined with a colon.
What is the Shutterstock API rate limit?
Shutterstock's free developer tier has rate limits that vary by endpoint and are documented at developers.shutterstock.com/rate-limiting. Check the X-Ratelimit-Limit, X-Ratelimit-Remaining, and X-Ratelimit-Reset response headers on every API call. For higher volume, apply for a commercial API plan. Caching search results reduces API usage significantly.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation