Connect Bubble to the Shutterstock API using Basic Auth for instant search and preview access — no token exchange required. Encode your client_id:client_secret as Base64, add it as a Private header in Bubble's API Connector, and display 450 million+ stock images, videos, and music tracks in Repeating Groups. OAuth 2.0 is only needed if you are building a licensing and download flow.
| Fact | Value |
|---|---|
| Tool | Shutterstock API |
| Category | Media & Content |
| Method | Bubble API Connector |
| Difficulty | Intermediate |
| Time required | 45–60 minutes |
| Last updated | July 2026 |
Shutterstock in Bubble: Basic Auth first, OAuth only when you need licensing
Most Bubble apps that connect to Shutterstock only need to search the library and show watermarked previews so a user can pick an image — they don't need to download the licensed file from within the app. Shutterstock supports exactly this use-case with Basic Auth: you take your client_id and client_secret, combine them as client_id:client_secret, encode that string as Base64, and drop it into a single Private header in Bubble's API Connector. There is no token expiry, no background refresh workflow, and no paid subscription required for this path.
Bubble's API Connector sends all calls from Bubble's own servers, which means CORS is never an issue and the Authorization header is never visible to the browser — even though Basic Auth is technically a simpler scheme than OAuth. Once the API Connector group is configured, you can add three separate Data calls on the same group: one for image search, one for video search, and one for audio search, giving you a multi-tab media browser with minimal additional setup.
The main complexity in this integration is Shutterstock's nested response structure. Preview image URLs are buried inside assets.preview_1500.url (or assets.huge_thumb.url for smaller thumbnails), and Bubble can only auto-detect this nesting after you initialize the call against a real Shutterstock response that includes this field. If you initialize with an empty query or a minimal mock, Bubble won't know the field exists and your Image elements will show nothing.
OAuth 2.0 licensing is a separate chapter. If your Bubble app needs to let users license and download images — not just preview them — you must request a paid Shutterstock API subscription, set up a POST token exchange call, store the access_token in an App Data field, and pass it as a Bearer header on the licensing endpoint. This is covered in Step 6 of this guide.
Integration method
Two-phase API Connector setup: Basic Auth for search/preview (immediate, no token management), OAuth 2.0 Client Credentials for licensing flows (paid Shutterstock API plan required).
Prerequisites
- A Bubble app on any plan (Free plan is sufficient for search and preview; paid plan required only if you want scheduled Backend Workflows for token refresh)
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
- A Shutterstock developer account — sign up free at shutterstock.com/developers
- A Shutterstock developer app created at shutterstock.com/developers — this gives you a client_id and client_secret
- A Base64 encoder — any online tool works (search 'base64 encode online') or use Bubble's Toolbox plugin for in-app encoding
- A Shutterstock paid API subscription only if you intend to license and download assets (not required for search and preview)
Step-by-step guide
Register a Shutterstock developer app and collect your credentials
Open shutterstock.com/developers in your browser and sign in (or create a free account). Click 'Create new app' in your dashboard. Give the app a name that reflects your project — for example, 'My Bubble Media Picker'. Leave the callback URL blank for now; it is only needed for user-level OAuth flows, not client credentials. Once the app is created, Shutterstock shows you a client_id and a client_secret on the app detail page. Copy both values and keep them somewhere safe — you will need to combine them in the next step. Note: Shutterstock grants instant access to the search and preview endpoints as soon as you create a developer app. There is no approval wait like Getty Images. You can make your first API call within minutes of registration. If you plan to build licensing and download features later, you will need to apply for a paid Shutterstock API subscription separately. For now, the free developer app is all you need.
Pro tip: Save your client_id and client_secret in a secure note right now. You will use them to build the Base64 Basic Auth header in the next step, and you cannot retrieve the client_secret again from the Shutterstock dashboard after leaving the page — you would need to regenerate it.
Expected result: You have a Shutterstock developer account, a new app, and a client_id + client_secret pair ready to use.
Base64-encode your credentials and configure the API Connector
Shutterstock's Basic Auth header requires the exact string `client_id:client_secret` (colon separator, no spaces) encoded as Base64. Go to any online Base64 encoder (search 'base64 encode online'), paste `YOUR_CLIENT_ID:YOUR_CLIENT_SECRET` into the input field, and copy the encoded output. The output will look like a long string of letters, numbers, plus signs, and equal signs — for example: `dGVzdGlkOnRlc3RzZWNyZXQ=`. Make sure the encoded string has no line breaks or extra whitespace. Now open your Bubble app and go to the Plugins tab in the left sidebar. Click 'Add plugins', search for 'API Connector', find the plugin by Bubble, and click Install. Once installed, click 'API Connector' in the Plugins tab to open its configuration panel. Click 'Add another API'. In the API Name field, type 'Shutterstock'. Set the Authentication dropdown to 'No authentication' — you will handle auth through a manual header rather than Bubble's built-in auth scheme, which gives you more control. Expand the Shared headers section and click 'Add a shared header'. Set the key to `Authorization` and the value to `Basic YOUR_BASE64_STRING` (replace with your actual encoded string). Tick the **Private** checkbox next to this header — this is critical and ensures the header value is never sent to or exposed in the browser, even though Bubble proxies the call server-side. Set the base URL for this API to `https://api.shutterstock.com/v2`.
1{2 "api_name": "Shutterstock",3 "base_url": "https://api.shutterstock.com/v2",4 "authentication": "No authentication",5 "shared_headers": [6 {7 "key": "Authorization",8 "value": "Basic dGVzdGlkOnRlc3RzZWNyZXQ=",9 "private": true,10 "note": "Replace value with your own Base64-encoded client_id:client_secret"11 }12 ]13}Pro tip: If you prefer to do the Base64 encoding inside Bubble rather than using an external tool, install the Toolbox plugin and use the 'Run JavaScript' action: `return btoa('YOUR_CLIENT_ID:YOUR_CLIENT_SECRET');` — then copy the result into the header value. Either approach works.
Expected result: The Shutterstock API group appears in your API Connector with a Private Authorization header. No calls exist yet but the group is configured and ready.
Create the image search Data call and configure response parameters
Inside the Shutterstock API Connector group, click 'Add another call'. Name the call 'Search Images'. Set the method to GET and the endpoint path to `/images/search` (Bubble appends this to the base URL automatically, resulting in `https://api.shutterstock.com/v2/images/search`). Set the 'Use as' dropdown to 'Data' — this tells Bubble to treat the response as a list of records that can be bound to a Repeating Group or searched using Bubble's data expressions. Now add the following URL parameters (click 'Add a parameter' for each): - `query` — leave the default value as something meaningful like `nature`; mark it as a dynamic parameter so you can bind it to a search input later - `image_type` — set a default value of `photo` (other values: `illustration`, `vector`) - `orientation` — default `horizontal` (other values: `vertical`, `square`) - `per_page` — default `30` - `page` — default `1`; mark as dynamic for pagination - `safe` — default `true` Do NOT mark the `page` parameter as Private — it is a pagination control that Bubble needs to pass dynamically from a custom state.
1{2 "call_name": "Search Images",3 "method": "GET",4 "path": "/images/search",5 "use_as": "Data",6 "url_parameters": [7 { "key": "query", "default": "nature", "dynamic": true },8 { "key": "image_type", "default": "photo", "dynamic": true },9 { "key": "orientation", "default": "horizontal", "dynamic": true },10 { "key": "per_page", "default": "30" },11 { "key": "page", "default": "1", "dynamic": true },12 { "key": "safe", "default": "true" }13 ]14}Pro tip: If you want to add video search later, duplicate this call, rename it 'Search Videos', and change the path to `/videos/search`. The parameter structure is nearly identical. For audio, the path is `/audio/search` with different result fields. All three calls share the same Private Basic Auth header from the group level.
Expected result: A 'Search Images' Data call appears in the Shutterstock API Connector group with all URL parameters defined and the method set to GET.
Initialize the call with a real search result so Bubble detects nested preview URLs
This step is critical and one of the most common sources of problems in Bubble API Connector setups. Bubble cannot know the shape of an API response until it sees one — you must click 'Initialize call' and let Bubble receive an actual JSON response from Shutterstock, then use that response to auto-detect field names and types. With `query=nature` and `per_page=30` already set as defaults, click 'Initialize call' at the bottom of the call configuration panel. Bubble sends a live GET request to Shutterstock's API and shows you the raw JSON response. If you see a list of image objects with nested fields, the initialization succeeded. After initialization, Bubble shows you the detected fields. Look for the nested path `assets.preview_1500.url` — this is the URL of a 1500px-wide watermarked preview image. You should also see `assets.huge_thumb.url` (smaller, loads faster in Repeating Groups), `id`, `description`, and `contributor.id`. In the 'Data path' field below the call configuration, type `data` — this tells Bubble that the actual list of results lives inside the `data` key of the root response object, rather than at the root level. Without this, Bubble treats the entire response as a single record and your Repeating Group shows one row instead of thirty. If the initialization fails with a 401 error, your Base64 string is malformed — re-encode the `client_id:client_secret` string carefully and update the shared header value.
1{2 "note": "Sample Shutterstock image search response structure",3 "total_count": 45231890,4 "page": 1,5 "per_page": 30,6 "search_id": "abc123",7 "data": [8 {9 "id": "1234567890",10 "description": "Beautiful nature landscape",11 "contributor": { "id": "987654" },12 "assets": {13 "huge_thumb": {14 "height": 260,15 "url": "https://thumb7.shutterstock.com/display_pic.../1234567890/huge_thumb-1234567890.jpg",16 "width": 39017 },18 "preview_1500": {19 "height": 1000,20 "url": "https://thumb7.shutterstock.com/display_pic.../1234567890/1500w_1234567890.jpg",21 "width": 150022 }23 },24 "media_type": "image"25 }26 ]27}Pro tip: If you later change the URL parameters or add new fields to the call, you must click 'Initialize call' again to update Bubble's detected field list. Failing to re-initialize after schema changes is the most common reason Image elements suddenly show nothing after a call update.
Expected result: Bubble's API Connector shows a list of detected fields including `assets.preview_1500.url`, `assets.huge_thumb.url`, `id`, and `description`. The Data path is set to `data`.
Build the Repeating Group and image picker UI
Now wire the search call to your Bubble UI. Go to the page where you want the image picker — either a full page or a popup element. Add the following elements: 1. A text Input element for the search query. Set its placeholder to 'Search for images…'. Give it an ID of 'search-input'. 2. A Dropdown for image type with options: photo, illustration, vector. Default to 'photo'. 3. A Repeating Group. Set its Type of content to 'Shutterstock Search Images' (the Data type Bubble created when you initialized the call). Set its Data source to 'Get data from an external API → Shutterstock - Search Images'. In the data source popup, bind the `query` parameter to 'search-input's value' and the `image_type` parameter to the dropdown's value. 4. Inside the Repeating Group, add an Image element. Set its Dynamic image to 'Current cell's Shutterstock Search Images's assets huge_thumb url' — this uses the smaller thumbnail for fast loading in the grid. 5. Add a 'Save' or 'Select' button inside the Repeating Group cell. In its workflow, add a 'Make changes to a Thing' or 'Create a new Thing' action that stores `Current cell's Shutterstock Search Images's id` and `Current cell's Shutterstock Search Images's assets preview_1500 url` in your database. For pagination, add 'Previous page' and 'Next page' buttons that modify a custom state (type: number, initial value: 1) and bind the `page` URL parameter to that state's value. If you want a full-size preview popup when a user clicks a thumbnail, add a Popup element with a larger Image element and bind its Dynamic image to a custom state that stores the `assets.preview_1500.url` of the clicked result. Set the 'When image is clicked' workflow to 'Set state → preview URL' and 'Show popup'.
Pro tip: Do not trigger a new search on every keystroke — Shutterstock has rate limits that can be exhausted quickly during active typing. Use a Bubble custom state with a 500ms pause: only fire the API call after the user stops typing for half a second. You can approximate this with a 'Schedule API workflow' on a 0.5-second delay that gets cancelled and rescheduled on each keystroke.
Expected result: Your Bubble page shows a search input, image type dropdown, and a Repeating Group that displays Shutterstock thumbnail previews when you type a search term. Clicking 'Save' or 'Select' stores the image ID and preview URL in your database.
Add OAuth 2.0 token exchange for licensing flows (paid plan required)
If your Bubble app needs to let users license and download images — not just view previews — you must first obtain a paid Shutterstock API subscription from shutterstock.com/developers. Once you have it, you need to add a token exchange call to your API Connector. Create a second API Connector group named 'Shutterstock OAuth'. In this group, add a new call named 'Get Access Token'. Set the method to POST and the URL to `https://accounts.shutterstock.com/oauth/token`. Set the body type to 'Form-urlencoded'. Add three body parameters: `grant_type=client_credentials`, `client_id=[your client_id, marked Private]`, and `client_secret=[your client_secret, marked Private]`. Set 'Use as' to Action (not Data, since this call modifies state). Initialize the call with your real credentials to confirm it returns an `access_token` field. Create a Backend Workflow (Settings → API → enable Workflow API → Backend Workflows section) named 'Refresh Shutterstock Token'. In this workflow, run the 'Get Access Token' Action call, then create or modify an App Data row (a special 'AppSettings' data type works well) storing the `access_token` value. Add a Bubble condition to re-run this workflow before any licensing call by checking if the stored token is empty or if its creation time is older than 30 minutes. Note: Backend Workflows are a paid Bubble plan feature. If you are on the Free plan, you cannot schedule automatic token refresh. You can still refresh the token on page load using a front-end workflow, but tokens will expire during inactive sessions. For the actual licensing call, add a third call in the Shutterstock API Connector group named 'License Image'. Set the method to POST, URL to `/images/licenses`. Add a shared header `Authorization: Bearer [App Data access_token field]` marked Private. The body should be JSON with the structure `{ "images": [{ "image_id": "<dynamic>", "subscription_id": "<your subscription ID>", "format": "jpg", "size": "huge" }] }`. RapidDev's team has set up this exact token refresh and licensing architecture for Bubble clients — if you need help structuring the Backend Workflow logic, a free scoping call is available at rapidevelopers.com/contact.
1{2 "token_exchange_call": {3 "name": "Get Access Token",4 "method": "POST",5 "url": "https://accounts.shutterstock.com/oauth/token",6 "body_type": "Form-urlencoded",7 "use_as": "Action",8 "body_params": [9 { "key": "grant_type", "value": "client_credentials" },10 { "key": "client_id", "value": "<your_client_id>", "private": true },11 { "key": "client_secret", "value": "<your_client_secret>", "private": true }12 ]13 },14 "licensing_call": {15 "name": "License Image",16 "method": "POST",17 "path": "/images/licenses",18 "use_as": "Action",19 "shared_header_from_group": "Authorization: Bearer [App Data token field] — Private",20 "body": {21 "images": [22 {23 "image_id": "<dynamic>",24 "subscription_id": "<your_subscription_id>",25 "format": "jpg",26 "size": "huge"27 }28 ]29 }30 }31}Pro tip: Always gate the 'License Image' call behind a paid subscription check in your Bubble app — use a conditional that verifies the current user has a premium plan before showing the 'License' button. Calling the licensing endpoint without a valid Shutterstock API subscription returns a 403 error that can confuse users who expect a simple 'permission denied' message.
Expected result: A 'Refresh Shutterstock Token' Backend Workflow exists, a valid access_token is stored in App Data, and the License Image Action call successfully returns a download URL when triggered for a subscribed user.
Common use cases
Stock Image Picker for Content Creators
Embed a Shutterstock search popup inside a Bubble content-creation page. The user types a keyword, picks from a grid of watermarked previews, and the selected image's ID and preview URL are saved to a database field attached to their article or social post draft. This requires no Shutterstock subscription — only the free developer API.
Build a Bubble popup with a text input for the search query, a dropdown for image type (photo, illustration, vector), and a Repeating Group showing Shutterstock thumbnail previews. When the user clicks a thumbnail, save the image ID and preview URL to the current user's draft record and close the popup.
Copy this prompt to try it in Bubble
Multi-Media Asset Browser (Images, Video, Audio)
Create a three-tab Bubble page where users can switch between image, video, and audio search results from Shutterstock. Each tab uses a separate API Connector call on the same group (GET /v2/images/search, /v2/videos/search, /v2/audio/search) with the same Private Basic Auth header, giving access to the full Shutterstock library from a single configuration.
Design a Bubble page with three tabs — Images, Videos, Audio. Each tab has a search input and a Repeating Group. All three call Shutterstock's API using the same API Connector group and Basic Auth header. Show thumbnails for images, poster frames for videos, and title plus duration for audio tracks.
Copy this prompt to try it in Bubble
Licensed Download Flow for a Creative Platform
Build a paid feature inside a Bubble subscription app where users can license Shutterstock images and receive a download URL. This path requires a Shutterstock paid API plan, an OAuth 2.0 token exchange call, and a POST to /v2/images/licenses. Gate this feature behind a Bubble conditional that checks the user's subscription tier before allowing the license action.
Add a 'License this image' button to the image picker that is only visible to Premium subscribers. Clicking the button calls a Backend Workflow that exchanges OAuth credentials for a token, posts to the Shutterstock licensing endpoint, stores the returned download URL in the database, and sends the user an email with the link.
Copy this prompt to try it in Bubble
Troubleshooting
The API Connector returns a 401 Unauthorized error when I initialize the Search Images call
Cause: The Base64-encoded value in the Authorization header is malformed. Common causes: line breaks inserted during copy-paste, extra spaces around the colon separator in `client_id:client_secret`, or encoding only the client_id without the secret.
Solution: Go back to your Base64 encoder and re-encode the exact string `YOUR_CLIENT_ID:YOUR_CLIENT_SECRET` (colon between them, no spaces before or after). Paste the result fresh into the Private Authorization header value, making sure the full value reads `Basic ENCODED_STRING` with a single space between 'Basic' and the encoded string. Then try initializing again.
The Repeating Group shows one row instead of multiple image results, or shows a single nested object
Cause: The Data path in the API Connector call is not set to `data`. Without this, Bubble treats the entire response (which has a root object with `total_count`, `page`, and `data` array) as a single record.
Solution: Open the Search Images call in the API Connector, find the 'Data path' field, and type `data` (lowercase, no quotes). Click 'Initialize call' again after saving. Then return to the Repeating Group and confirm its Data source is pointing to the call — you may need to re-select it after the Data path change.
Image elements in the Repeating Group show nothing — the `assets.preview_1500.url` or `assets.huge_thumb.url` field does not appear in the list of available fields
Cause: The API Connector was initialized with an empty query response, a mock response, or a response where the `assets` object was missing. Bubble only detects fields that appear in the actual initialization response.
Solution: Make sure the `query` parameter has a real value (like `nature`) before clicking 'Initialize call'. After initialization, verify that `assets.huge_thumb.url` and `assets.preview_1500.url` appear in the detected field list. If they still don't appear, click 'Initialize call' one more time — occasionally Bubble needs a second attempt to fully parse deeply nested JSON. Re-select the Image element's Dynamic image source after re-initialization.
Licensing call returns a 403 Forbidden or a subscription error instead of a download URL
Cause: Your Shutterstock developer account does not have a paid API subscription. Search and preview access is free, but licensing (POST /v2/images/licenses) requires a separate paid Shutterstock API plan — it is not included with a regular Shutterstock Creative or Shutterstock Flex subscription.
Solution: Go to shutterstock.com/developers and check your API subscription status. If you do not have an API licensing plan, apply for one through the developer portal. In the meantime, you can still build the full search and preview UI — just disable or hide the 'License' button until the API plan is active.
There was an issue setting up your call — the initialize call button shows an error without returning any response
Cause: The base URL of the API Connector group may have a trailing slash, or the call path starts with a slash AND the base URL also ends with /v2/ — resulting in a double-slash URL like `https://api.shutterstock.com/v2//images/search`.
Solution: Check the API Connector group's base URL: it should be `https://api.shutterstock.com/v2` with no trailing slash. The call path should start with a forward slash: `/images/search`. Bubble combines them as `base_url + path`, so the final URL should read `https://api.shutterstock.com/v2/images/search`. Correct both and re-initialize.
Best practices
- Always mark the Authorization header as Private in Bubble's API Connector — even though Basic Auth is simpler than OAuth, the encoded client_id:client_secret must never appear in client-side requests or browser network logs.
- Use `assets.huge_thumb.url` (smaller images, ~390×260px) in your Repeating Group grid for fast loading, and switch to `assets.preview_1500.url` only in the full-size preview popup — this reduces bandwidth and improves perceived load time.
- Add a 500ms debounce on the search input so the API call fires only after the user stops typing, not on every keystroke — Shutterstock has rate limits that can be hit quickly during active typing in a Bubble environment.
- Store selected image IDs and preview URLs in a Bubble database Thing, not just in custom states — custom states reset on page refresh, and users will lose their selections.
- Set the Data path to `data` in the API Connector's Search Images call — without this, Bubble sees the response as a single record instead of a list and the Repeating Group displays incorrectly.
- Re-initialize the API Connector call any time you change the response structure, add new URL parameters, or update the endpoint path — Bubble caches the field schema from the last initialization and will not pick up new fields automatically.
- Add Privacy rules in Bubble's Data tab for any database type that stores Shutterstock image data — without privacy rules, all rows are publicly accessible to any authenticated user in your app.
- Gate the licensing endpoint behind a subscription check in your app's workflow conditions — displaying the 'License' button only to users who have a valid paid plan prevents confusing 403 errors for free users.
Alternatives
Pixabay is completely free with a single API key and no OAuth overhead — ideal for Bubble apps that need royalty-free stock without any licensing complexity. Shutterstock offers a vastly larger library (450M+ vs 4.5M assets) with commercial-grade licensing, but requires credential management and a paid subscription for downloads.
Getty Images requires a 1-3 day application approval and supports only OAuth 2.0 (no Basic Auth shortcut), making it more complex to set up than Shutterstock. Getty is stronger for editorial and news photography; Shutterstock covers a broader creative range including video and music and allows instant developer access.
Canva's API provides access to design templates and an embedded design editor, not a stock image search library. If your Bubble app needs users to create and edit visual content rather than just pick stock images, Canva is the better fit. For pure image search and asset retrieval, Shutterstock's API is more appropriate.
Frequently asked questions
Do I need a paid Shutterstock subscription to use the API in Bubble?
No — for search and watermarked preview access, a free Shutterstock developer account is all you need. You only need a paid Shutterstock API subscription if you want to license and download high-resolution images directly through your Bubble app. The search and preview endpoints work with the free developer tier.
Why use Basic Auth instead of OAuth 2.0 for search?
Basic Auth with a Base64-encoded client_id:client_secret gives you immediate access to Shutterstock's search and preview endpoints with zero token management overhead — no expiry, no refresh workflows, no App Data storage needed. Shutterstock explicitly supports Basic Auth for these endpoints. Switch to OAuth 2.0 only when your app needs to call the licensing endpoint, which requires a token.
Can I show full-resolution Shutterstock images in my Bubble app?
The `assets.preview_1500.url` field returns a 1500px watermarked preview — this is the largest size available to free developer accounts and it is watermarked. The original high-resolution download URL is only returned by the licensing endpoint (POST /v2/images/licenses) after a successful license purchase, which requires a paid Shutterstock API subscription. Never store or display the full-res URL without a valid license.
Does Bubble's API Connector work server-side, or do I need a backend proxy for Shutterstock?
Bubble's API Connector runs all calls from Bubble's own servers — not the user's browser. This means CORS is never an issue, and your Authorization header (including the Base64-encoded credentials) is never visible in the browser's network tab. You do not need an external proxy or Edge Function. The Private checkbox in the API Connector is an additional safeguard that prevents the header from being returned to the client even in API response data.
What happens if the Shutterstock API rate limit is hit in a Bubble app?
Shutterstock does not publicly document its rate limits, but heavy polling (for example, triggering a new search on every keystroke) can exhaust the quota quickly. If rate limits are hit, Bubble's API Connector will show an error in the call result or in the Logs tab. The fix is to add a debounce to the search input so calls fire only after the user pauses typing, and to use pagination rather than fetching large result sets in loops.
Can I search for Shutterstock videos and music in Bubble using the same setup?
Yes — with the same Private Basic Auth header already configured on the API Connector group, add two more calls: one for GET /v2/videos/search (use Data path `data`) and one for GET /v2/audio/search. The parameter structure is nearly identical to image search. Video results include a `assets.preview_mp4.url` field for an MP4 preview. Audio results include `title`, `description`, and `duration_seconds` fields.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation