Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

Backblaze B2 Cloud Storage

Connect Bubble to Backblaze B2's native API using an external proxy function that handles the dynamic `apiUrl` problem. Unlike the S3-compatible layer, the native B2 API returns a session-specific base URL you cannot hardcode. The proxy caches this token, exposes stable endpoints to Bubble, and enables exclusive features: per-file download authorization tokens and large-file chunked uploads.

What you'll learn

  • Why the native B2 API requires a proxy function while the S3-compatible layer does not
  • How to architect a proxy that caches B2's dynamic apiUrl and authorizationToken
  • How to use b2_get_download_authorization for time-limited per-file access tokens
  • How b2_get_upload_url works — and why upload URLs are single-use
  • How to wire Bubble's API Connector to proxy endpoints for uploads, downloads, and file listing
  • How to cache the B2 auth token in a Bubble Config data type to minimize proxy calls
  • When to use the native B2 API versus the simpler S3-compatible layer
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced19 min read3–5 hoursStorage & FilesLast updated July 2026RapidDev Engineering Team
TL;DR

Connect Bubble to Backblaze B2's native API using an external proxy function that handles the dynamic `apiUrl` problem. Unlike the S3-compatible layer, the native B2 API returns a session-specific base URL you cannot hardcode. The proxy caches this token, exposes stable endpoints to Bubble, and enables exclusive features: per-file download authorization tokens and large-file chunked uploads.

Quick facts about this guide
FactValue
ToolBackblaze B2 Cloud Storage
CategoryStorage & Files
MethodBubble API Connector
DifficultyAdvanced
Time required3–5 hours
Last updatedJuly 2026

When you need the native B2 API — and what it costs you in complexity

Most Bubble developers should use the S3-compatible Backblaze B2 integration (see the companion page: `/bubble-integrations/backblaze-b2`). The native B2 API is the right choice in two specific scenarios:

**Scenario 1: Per-file download authorization.** If your B2 bucket is private and you need to issue time-limited access tokens for individual files — not just presigned URLs — the native `b2_get_download_authorization` endpoint provides exactly this. The resulting token can be appended to a download URL and controls access at the file granularity. This is unavailable in the S3-compatible layer.

**Scenario 2: Large-file chunked uploads.** Files over approximately 100 MB benefit from B2's large-file API: `b2_start_large_file`, followed by `b2_get_upload_part_url` for each chunk, then `b2_finish_large_file`. The S3-compatible multipart upload exists but is less fine-grained.

The cost of using the native API is architectural complexity. The `b2_authorize_account` response includes a `apiUrl` field (e.g., `https://pod-000-1000-18.backblaze.com`) that changes per session and serves as the base URL for all subsequent calls. This URL cannot be hardcoded in Bubble's API Connector. A proxy function solves this: it runs authorization, caches the `apiUrl` and `authorizationToken` (valid up to 24 hours), and exposes three stable endpoints to Bubble: `/getUploadUrl`, `/getDownloadAuth`, and `/listFiles`.

Integration method

Bubble API Connector

Bubble's API Connector calls a stable proxy function that handles B2's dynamic `apiUrl` caching, exposes fixed endpoints for upload URL generation, download authorization, and file listing.

Prerequisites

  • A Backblaze account (backblaze.com) — free tier includes 10 GB storage
  • A Bubble app on a paid plan (Starter or above) — Backend Workflows are required and not available on the Free plan
  • A server-side proxy function deployed: Cloudflare Worker, Supabase Edge Function, or Firebase Cloud Function
  • The Bubble API Connector plugin installed (Plugins tab → Add plugins → 'API Connector' by Bubble)
  • Basic familiarity with deploying a server-side function and setting environment variables
  • Understanding of when to use native B2 API vs S3-compatible layer (if you haven't decided yet, read the companion page backblaze-b2 first)

Step-by-step guide

1

Create your B2 private bucket and application key

Log in at secure.backblaze.com and navigate to B2 Cloud Storage → Buckets → Create a Bucket. Name the bucket and set it to Private (allPrivate). For the native B2 API, you typically want private buckets since the primary reasons to use the native API — download authorization tokens — only apply to private files. Once the bucket is created, note the Bucket Name and Bucket ID (shown in bucket details). The Bucket ID is a string like `e73ede9969c64287527b` and is used in native B2 API calls alongside the bucket name. Next, create an application key: Account → App Keys → Add a New Application Key. Scope it to the specific bucket you just created. Enable both read and write capabilities. Click 'Create New Key'. Copy both the `keyID` and `applicationKey` immediately — the `applicationKey` is shown only once. These are your credentials for `b2_authorize_account` using HTTP Basic Auth (username = keyID, password = applicationKey, encoded as Base64 for the Authorization header).

b2-credentials.env
1# B2 bucket and key reference
2BUCKET_NAME=my-bubble-private-files
3BUCKET_ID=e73ede9969c64287527b # from Backblaze console bucket details
4KEY_ID=your_application_keyID_here
5APPLICATION_KEY=your_applicationKey_here
6
7# HTTP Basic Auth for b2_authorize_account:
8# Authorization: Basic base64(keyID:applicationKey)
9# This is handled inside your proxy function, not in Bubble directly

Pro tip: Unlike the S3-compatible layer, the native B2 API identifies buckets by both name and ID. Save both values from the bucket details page. You will use the bucket name in download URLs and the bucket ID in some API calls.

Expected result: Private B2 bucket created with its Bucket ID noted. Application key pair (keyID + applicationKey) saved securely. Ready to build the proxy function.

2

Deploy the proxy function with b2_authorize_account caching

The proxy's core job is solving the dynamic `apiUrl` problem. On first request (or when the cached token has expired), it calls `b2_authorize_account` and stores the response's `apiUrl`, `authorizationToken`, and `downloadUrl` in memory (or in a KV store for Cloudflare Workers, or a lightweight database row). Subsequent requests within the same session reuse the cached values without re-authorizing. The proxy exposes three stable endpoints: - `POST /getUploadUrl` — returns a one-time upload URL and upload token for a single file upload - `POST /getDownloadAuth` — returns a time-limited download authorization token for a specific file - `GET /listFiles` — returns an array of file objects from the bucket Deploy this as a Cloudflare Worker (easiest for caching with Workers KV), a Supabase Edge Function (Deno), or a Firebase Cloud Function. Set `B2_KEY_ID`, `B2_APPLICATION_KEY`, `B2_BUCKET_NAME`, and `B2_BUCKET_ID` as environment variables in your hosting platform's dashboard. Note your proxy's deployed base URL — this is what you will put in Bubble's API Connector.

b2-native-proxy-worker.js
1// Cloudflare Worker — Backblaze B2 Native API Proxy
2// Environment vars: B2_KEY_ID, B2_APPLICATION_KEY, B2_BUCKET_NAME, B2_BUCKET_ID
3
4let cachedAuth = null;
5
6async function authorizeAccount(env) {
7 if (cachedAuth && Date.now() < cachedAuth.expiresAt) {
8 return cachedAuth;
9 }
10
11 const credentials = btoa(`${env.B2_KEY_ID}:${env.B2_APPLICATION_KEY}`);
12 const res = await fetch('https://api.backblazeb2.com/b2api/v2/b2_authorize_account', {
13 headers: { 'Authorization': `Basic ${credentials}` },
14 });
15
16 if (!res.ok) throw new Error(`B2 auth failed: ${res.status}`);
17
18 const data = await res.json();
19 cachedAuth = {
20 apiUrl: data.apiUrl,
21 authorizationToken: data.authorizationToken,
22 downloadUrl: data.downloadUrl,
23 expiresAt: Date.now() + 23 * 60 * 60 * 1000, // 23 hours
24 };
25 return cachedAuth;
26}
27
28export default {
29 async fetch(request, env) {
30 const url = new URL(request.url);
31 const auth = await authorizeAccount(env);
32
33 // POST /getUploadUrl
34 if (url.pathname === '/getUploadUrl' && request.method === 'POST') {
35 const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_get_upload_url`, {
36 method: 'POST',
37 headers: {
38 'Authorization': auth.authorizationToken,
39 'Content-Type': 'application/json',
40 },
41 body: JSON.stringify({ bucketId: env.B2_BUCKET_ID }),
42 });
43 const data = await res.json();
44 return new Response(JSON.stringify({
45 uploadUrl: data.uploadUrl,
46 uploadAuthorizationToken: data.authorizationToken,
47 }), { headers: { 'Content-Type': 'application/json' } });
48 }
49
50 // POST /getDownloadAuth
51 if (url.pathname === '/getDownloadAuth' && request.method === 'POST') {
52 const { fileName, validDurationInSeconds } = await request.json();
53 const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_get_download_authorization`, {
54 method: 'POST',
55 headers: {
56 'Authorization': auth.authorizationToken,
57 'Content-Type': 'application/json',
58 },
59 body: JSON.stringify({
60 bucketId: env.B2_BUCKET_ID,
61 fileNamePrefix: fileName,
62 validDurationInSeconds: validDurationInSeconds || 3600,
63 }),
64 });
65 const data = await res.json();
66 // Construct the full download URL
67 const downloadUrl = `${auth.downloadUrl}/file/${env.B2_BUCKET_NAME}/${encodeURIComponent(fileName)}?Authorization=${data.authorizationToken}`;
68 return new Response(JSON.stringify({ downloadUrl }), {
69 headers: { 'Content-Type': 'application/json' },
70 });
71 }
72
73 // GET /listFiles
74 if (url.pathname === '/listFiles' && request.method === 'GET') {
75 const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_file_names`, {
76 method: 'POST',
77 headers: {
78 'Authorization': auth.authorizationToken,
79 'Content-Type': 'application/json',
80 },
81 body: JSON.stringify({ bucketId: env.B2_BUCKET_ID, maxFileCount: 100 }),
82 });
83 const data = await res.json();
84 const files = data.files.map(f => ({
85 name: f.fileName,
86 size: f.contentLength, // NOTE: native B2 uses contentLength, not size
87 uploadedAt: new Date(f.uploadTimestamp).toISOString(),
88 fileId: f.fileId,
89 }));
90 return new Response(JSON.stringify({ files }), {
91 headers: { 'Content-Type': 'application/json' },
92 });
93 }
94
95 return new Response('Not found', { status: 404 });
96 },
97};

Pro tip: Note the `contentLength` field name in the native B2 response for file listing — the native API uses `contentLength` (not `size` as in S3-compatible responses). The proxy maps this to `size` in the clean JSON response so Bubble's API Connector doesn't get confused.

Expected result: Proxy function is deployed with three stable endpoints. You have the proxy base URL. Test each endpoint manually before proceeding to Bubble configuration.

3

Configure the Bubble API Connector to call your proxy

In your Bubble app, go to Plugins tab → Add plugins → search 'API Connector' (by Bubble) → Install. Open the API Connector configuration. Click 'Add another API'. Name it 'B2 Native Proxy'. If your proxy requires authentication (recommended — add a shared secret header), add a header like `X-Proxy-Token` with your secret value and check the 'Private' checkbox. This header value will never reach the browser. Add three separate API calls inside this API: **Call 1: Get Upload URL** - Method: POST - URL: `https://your-proxy.workers.dev/getUploadUrl` - Body Type: JSON (no body required, or send an empty `{}`) - Initialize with real values — the proxy will return `uploadUrl` and `uploadAuthorizationToken` **Call 2: Get Download Auth** - Method: POST - URL: `https://your-proxy.workers.dev/getDownloadAuth` - Body: `{ "fileName": "<dynamic>", "validDurationInSeconds": 3600 }` - Initialize with a real filename — returns `downloadUrl` **Call 3: List Files** - Method: GET - URL: `https://your-proxy.workers.dev/listFiles` - Initialize call — returns array of file objects with `name`, `size`, `uploadedAt` For each call, click 'Initialize call' and provide real values. Bubble needs a successful real response to detect the response schema. If you skip initialization or provide fake values that cause errors, Bubble's workflow data picker will show no fields from the response.

b2-api-connector-config.json
1{
2 "api_name": "B2 Native Proxy",
3 "shared_headers": {
4 "X-Proxy-Token": "<your_secret | Private: checked>"
5 },
6 "calls": [
7 {
8 "name": "Get Upload URL",
9 "method": "POST",
10 "url": "https://your-proxy.workers.dev/getUploadUrl",
11 "body": {},
12 "response_fields": ["uploadUrl", "uploadAuthorizationToken"]
13 },
14 {
15 "name": "Get Download Auth",
16 "method": "POST",
17 "url": "https://your-proxy.workers.dev/getDownloadAuth",
18 "body": {
19 "fileName": "<dynamic>",
20 "validDurationInSeconds": 3600
21 },
22 "response_fields": ["downloadUrl"]
23 },
24 {
25 "name": "List Files",
26 "method": "GET",
27 "url": "https://your-proxy.workers.dev/listFiles",
28 "response_fields": ["files (list)"]
29 }
30 ]
31}

Pro tip: If 'Initialize call' fails with 'There was an issue setting up your call', verify your proxy is deployed and responding. Open the proxy URL in a browser tab — even a 'Not found' or 'Method not allowed' response confirms the worker is live. Then check your proxy token header is correctly configured.

Expected result: Three API calls configured in the B2 Native Proxy API: Get Upload URL, Get Download Auth, and List Files. Each has been initialized with real responses, so Bubble shows the response fields in the data picker.

4

Build the upload workflow using single-use B2 upload URLs

Unlike presigned URLs (which are time-limited but can be reused within the window), B2 native upload URLs from `b2_get_upload_url` are SINGLE-USE. Each upload requires a fresh upload URL. Build your workflow accordingly. Add a File Uploader element to your Bubble page and a Button labeled 'Upload to B2'. In the Button's workflow: Step 1: Call API Connector → B2 Native Proxy → Get Upload URL. This returns `uploadUrl` and `uploadAuthorizationToken`. Step 2: Run JavaScript (requires Toolbox plugin by Bubble). Pass these inputs: `uploadUrl` (from Step 1 result), `uploadAuthToken` (from Step 1 result), `contentType` (from file element). The JavaScript reads the file from the file uploader element and POSTs it to the one-time `uploadUrl` with the B2-specific upload headers. Step 3: On JavaScript success, 'Make changes to a Thing' to save the file's name (or path) to the Bubble database. Store the filename/key — not the upload URL (which is already expired and single-use). The B2 native upload requires specific headers that differ from S3 presigned URL uploads: `Authorization` is set to the upload-specific `uploadAuthorizationToken` (not a Bearer token), `X-Bz-File-Name` is the URL-encoded filename, `Content-Type` is the file MIME type, and `Content-Length` is the file size in bytes.

b2-native-upload.js
1// Bubble 'Run JavaScript' (Toolbox plugin) — B2 native upload
2// Inputs: uploadUrl, uploadAuthToken, contentType
3// The file comes from a Bubble FileUploader element
4
5(async function() {
6 const uploadUrl = properties.uploadUrl;
7 const uploadAuthToken = properties.uploadAuthToken;
8 const contentType = properties.contentType || 'application/octet-stream';
9
10 const fileInput = document.querySelector('#fileUploader input[type="file"]');
11 const file = fileInput ? fileInput.files[0] : null;
12
13 if (!file) {
14 bubble_fn_result({ success: false, error: 'No file selected' });
15 return;
16 }
17
18 // B2 native upload requires URL-encoded filename in header
19 const encodedFileName = encodeURIComponent(file.name);
20
21 try {
22 const response = await fetch(uploadUrl, {
23 method: 'POST', // B2 native upload uses POST, not PUT
24 headers: {
25 'Authorization': uploadAuthToken, // upload-specific token, NOT Bearer
26 'X-Bz-File-Name': encodedFileName, // MUST be URL-encoded
27 'Content-Type': contentType,
28 'Content-Length': file.size.toString(),
29 },
30 body: file,
31 });
32
33 if (response.ok) {
34 const result = await response.json();
35 bubble_fn_result({ success: true, fileName: result.fileName, fileId: result.fileId });
36 } else {
37 const errText = await response.text();
38 bubble_fn_result({ success: false, error: errText });
39 }
40 } catch (err) {
41 bubble_fn_result({ success: false, error: err.message });
42 }
43})();

Pro tip: Notice that B2 native uploads use POST (not PUT like S3 presigned URLs) and require `X-Bz-File-Name` URL-encoded. Spaces and special characters in filenames MUST be percent-encoded — `My File.pdf` should become `My%20File.pdf`. Failing to encode causes upload errors.

Expected result: The upload button workflow successfully requests a single-use upload URL from the proxy, uploads the file to B2 using the native upload protocol, and saves the filename to the Bubble database. Verify the file appears in the Backblaze console.

5

Implement time-limited download authorization for private files

This is the primary reason to use the native B2 API. `b2_get_download_authorization` returns a token that grants access to a specific file (or file prefix) for a specified duration — enabling you to share private files with temporary access without making the whole bucket public. In Bubble, wherever you display a link to a private B2 file (in a repeating group row, a file detail page, or a download button), add a workflow step that calls API Connector → B2 Native Proxy → Get Download Auth. Pass the `fileName` (the stored key from your database) and the desired `validDurationInSeconds` (e.g., 3600 for 1 hour). The proxy returns a complete `downloadUrl` with the authorization token already appended as a query parameter. Use this URL in an 'Open external website' action, an Image element URL, or a 'Download' link. For a repeating group showing a file list, use the 'Get data from external API' data source on the group itself to call 'List Files' from the proxy. For individual file access links, trigger 'Get Download Auth' on demand (button click) rather than loading all download URLs on page load — this reduces WU consumption and keeps URLs fresher.

b2-download-auth-workflow.txt
1// Bubble workflow — generate a timed download URL on button click
2// Step 1: Call API → B2 Native Proxy → Get Download Auth
3// fileName: Current user's file record's 'b2_file_name' field
4// validDurationInSeconds: 3600
5// Step 2: Open external website (new tab)
6// URL: Result of step 1's 'downloadUrl'
7
8// The proxy constructs:
9// {downloadUrl}/file/{bucketName}/{encodedFileName}?Authorization={token}
10// Example:
11// https://f004.backblazeb2.com/file/my-bucket/My%20File.pdf?Authorization=4_0022...
12
13// Privacy rules reminder:
14// Data → Privacy → 'UserFile' type:
15// - 'b2_file_name' field: condition = Current User = This UserFile's owner
16// Without this, any logged-in user could call the download auth with another user's filename

Pro tip: Set privacy rules on your Bubble data type that stores b2_file_name. If the field is readable by all logged-in users, any user can request a download authorization token for any file. Restrict read access to the file owner. Data tab → Privacy → create a rule for your file data type.

Expected result: Clicking a download button generates a time-limited B2 download URL specific to that file. The URL expires after the specified duration. Other users cannot access the file without their own authorization token. RapidDev's team has implemented this pattern across many Bubble apps — if you need help configuring the exact privacy rules for your data model, reach out at rapidevelopers.com/contact.

6

Schedule token refresh and add error handling for expired tokens

The B2 `authorizationToken` from `b2_authorize_account` is valid for up to 24 hours, but can be invalidated earlier if the application key is modified in the Backblaze console. Your proxy's in-memory cache handles the 23-hour refresh cycle automatically. However, you should also add error handling for the case where B2 returns a 401 (expired or invalidated token) mid-session. In the proxy, add a re-auth fallback: if any B2 API call returns a 401, clear the cache and call `b2_authorize_account` again before retrying the original request. This handles edge cases like key rotation without requiring manual intervention. For Bubble itself, if any API Connector call to your proxy returns an error, add conditional error handling in the workflow using the 'Only when' condition on downstream steps. Use a custom state to track upload/download status and display appropriate error messages to users. If you are using Cloudflare Workers for the proxy, note that Worker instances are ephemeral — cached auth data in JavaScript variables is lost when the Worker is recycled. For production apps with high traffic, use Cloudflare Workers KV to persist the cached token across Worker instances.

b2-proxy-with-kv.js
1// Enhanced proxy with retry-on-401 pattern and KV storage
2export default {
3 async fetch(request, env, ctx) {
4 // Try to get cached token from KV store
5 let auth = null;
6 const cached = await env.B2_CACHE.get('auth', 'json');
7 if (cached && Date.now() < cached.expiresAt) {
8 auth = cached;
9 } else {
10 auth = await fetchNewAuth(env);
11 // Store in KV with 23-hour TTL
12 ctx.waitUntil(
13 env.B2_CACHE.put('auth', JSON.stringify(auth), { expirationTtl: 82800 })
14 );
15 }
16
17 // Attempt the actual B2 call
18 const result = await handleRequest(request, env, auth);
19
20 // If 401, re-auth and retry once
21 if (result.status === 401) {
22 auth = await fetchNewAuth(env);
23 ctx.waitUntil(
24 env.B2_CACHE.put('auth', JSON.stringify(auth), { expirationTtl: 82800 })
25 );
26 return handleRequest(request, env, auth);
27 }
28
29 return result;
30 },
31};
32
33async function fetchNewAuth(env) {
34 const credentials = btoa(`${env.B2_KEY_ID}:${env.B2_APPLICATION_KEY}`);
35 const res = await fetch('https://api.backblazeb2.com/b2api/v2/b2_authorize_account', {
36 headers: { 'Authorization': `Basic ${credentials}` },
37 });
38 const data = await res.json();
39 return {
40 apiUrl: data.apiUrl,
41 authorizationToken: data.authorizationToken,
42 downloadUrl: data.downloadUrl,
43 expiresAt: Date.now() + 23 * 60 * 60 * 1000,
44 };
45}

Pro tip: For Cloudflare Workers KV, you must create a KV namespace in the Cloudflare dashboard and bind it to your Worker as `B2_CACHE` in the Worker's bindings configuration. Without KV, cached tokens are lost between Worker invocations in high-traffic scenarios.

Expected result: The proxy handles token expiry gracefully by re-authorizing on 401 errors. For Cloudflare Workers deployments with KV storage, the cached B2 auth token persists across Worker restarts and instances. Your Bubble app's workflows handle API errors with appropriate user-facing error states.

Common use cases

Private document access with time-limited download tokens

Allow specific users to access private B2 files for a limited time window — for example, a signed contract or medical record available for 1 hour. A Backend Workflow calls the proxy `/getDownloadAuth` endpoint with the filename and duration, gets a time-limited token, constructs a download URL, and passes it to the user's session. When the token expires, the URL stops working automatically.

Bubble Prompt

Copy this prompt to try it in Bubble

Large media file uploads with chunked upload sessions

Upload video files, large datasets, or multi-hundred-MB archives reliably using B2's large-file API. The proxy handles the upload session creation, returns per-chunk upload URLs, and finalizes the file once all parts are received. This bypasses Bubble's 30-second workflow timeout for large file processing.

Bubble Prompt

Copy this prompt to try it in Bubble

File management dashboard with metadata and expiring links

Build an internal file browser showing Bubble users their uploaded B2 files with metadata (name, size, timestamp). The proxy's `/listFiles` endpoint returns clean JSON arrays. Clicking a file generates a fresh time-limited download URL on demand from `/getDownloadAuth` — no permanent public URLs exposed.

Bubble Prompt

Copy this prompt to try it in Bubble

Troubleshooting

Every call to the B2 API returns a different base URL — the proxy stops working after a while

Cause: The dynamic `apiUrl` in the B2 `b2_authorize_account` response is session-specific and changes between authorization sessions. If the proxy fetches a new auth on every call without caching, the `apiUrl` in subsequent calls may reference a stale host.

Solution: Ensure your proxy caches the entire auth response (`apiUrl`, `authorizationToken`, `downloadUrl`) for the session duration (up to 23 hours). Only call `b2_authorize_account` again when the cached token has expired or when a B2 call returns 401. For Cloudflare Workers, use Workers KV to persist the cache across instance restarts.

Upload to B2 returns error about X-Bz-File-Name or upload fails with 400

Cause: Either the `X-Bz-File-Name` header is missing, the filename is not URL-encoded, or the upload URL is being reused for a second file (upload URLs are single-use).

Solution: 1. Ensure `X-Bz-File-Name` is present in the upload request headers. 2. URL-encode the filename with `encodeURIComponent()` — spaces must be `%20`. 3. Always fetch a fresh upload URL from the proxy before each upload — never reuse the same `uploadUrl` for multiple files.

typescript
1// WRONG — spaces not encoded, reusing URL
2const fileName = 'My Document.pdf'; // spaces will fail
3fetch(sameUploadUrl, { headers: { 'X-Bz-File-Name': fileName } });
4
5// CORRECT
6const encodedFileName = encodeURIComponent('My Document.pdf'); // 'My%20Document.pdf'
7const { uploadUrl, uploadAuthorizationToken } = await fetchFreshUploadUrl();
8fetch(uploadUrl, { headers: { 'X-Bz-File-Name': encodedFileName } });

Bubble API Connector shows 'There was an issue setting up your call' on Initialize

Cause: The Initialize call requires a real successful response from your proxy. If the proxy is not deployed, the URL is wrong, or the proxy requires an auth header that isn't configured in the API Connector, initialization fails.

Solution: 1. Verify the proxy is deployed by opening its URL in a browser. 2. Check that any required auth headers (e.g., `X-Proxy-Token`) are configured in the API Connector's shared headers with the 'Private' checkbox. 3. For the 'Get Download Auth' call, provide a real existing filename from your B2 bucket during initialization — the proxy must return a successful response for Bubble to detect the fields.

Download authorization token works the first time but the URL stops working after an hour

Cause: This is expected behavior. The `validDurationInSeconds` passed to `b2_get_download_authorization` controls how long the token is valid. After expiry, the URL returns a 401 from B2.

Solution: Generate fresh download URLs on demand (when the user requests access) rather than storing them. If you stored a download URL in your Bubble database, delete it and modify your workflow to always call 'Get Download Auth' at the moment a user needs the file, not at upload time. The file itself remains in B2 permanently — only the access token expires.

Backend Workflow or API Workflow is unavailable — option is greyed out in Bubble

Cause: API Workflows (Backend Workflows) are not available on Bubble's Free plan. This entire native B2 integration pattern requires server-side workflow calls.

Solution: Upgrade to Bubble Starter ($32/month) or above to enable Backend Workflows and API Workflows. This is a hard requirement for the native B2 API proxy pattern. If upgrading isn't possible right now, consider the simpler S3-compatible B2 integration (see the companion page backblaze-b2) which uses the same proxy pattern but with AWS SDK compatibility.

Best practices

  • Never hardcode your B2 keyID or applicationKey anywhere in Bubble — not in text fields, not in API Connector headers, not in page conditions. These credentials live only in your proxy function's environment variables, set through the hosting platform's secrets dashboard.
  • Fetch a new upload URL for every file upload without exception. `b2_get_upload_url` returns single-use URLs — attempting to reuse one for a second upload will fail and is a common source of 'upload failed' errors in multi-file scenarios.
  • Store filenames, not download URLs, in your Bubble database. Download authorization tokens expire. Generate fresh download URLs at the moment a user needs access, not at upload time. Store only the immutable object identifier (the filename or fileId).
  • Enable privacy rules on the Bubble data type that holds file references. If any logged-in user can read another user's stored filename, they can call your proxy to generate download authorization tokens for files they don't own. Lock down field-level access using Data tab → Privacy rules.
  • Use `fileNamePrefix` in `b2_get_download_authorization` to authorize access to an entire folder of files at once. If each user's files are stored under a prefix like `users/{userId}/`, you can issue one token covering all their files rather than one per file — reducing proxy calls and WU consumption.
  • Add the 401 re-auth retry pattern to your proxy for production reliability. The B2 authorizationToken is valid 24 hours but can be invalidated if the application key changes. A proxy that re-authorizes on 401 and retries the original request handles this automatically without user-visible errors.
  • For Cloudflare Workers deployments, use Workers KV to persist cached B2 auth across Worker instances. Without KV, high-traffic scenarios can cause multiple concurrent Workers to each call `b2_authorize_account` simultaneously, burning B2 API quota unnecessarily.
  • Log the Bubble Workflow Logs (Logs tab) during development to trace each proxy call's WU cost and response time. The native B2 proxy pattern adds a proxy round-trip to each operation — monitor that latency is acceptable for your use case before going live.

Alternatives

Frequently asked questions

When should I use the native B2 API versus the S3-compatible layer?

Use the native B2 API (this page) in two scenarios: (1) you need `b2_get_download_authorization` to issue per-file time-limited access tokens for private bucket files, or (2) you need the large-file chunked upload API for reliable uploads of files over approximately 100 MB. For all other use cases — storing user uploads, serving media, backups — the S3-compatible layer at the companion page `/bubble-integrations/backblaze-b2` is simpler and equally capable.

Why can't I put the B2 API calls directly in Bubble's API Connector without a proxy?

Two reasons. First, `b2_authorize_account` returns a dynamic `apiUrl` that changes per session and cannot be hardcoded as the API Connector's base URL. Second, your B2 keyID and applicationKey must be used in an HTTP Basic Auth header for authorization — while Bubble's API Connector can mark headers 'Private', the dynamic base URL problem alone makes a direct connection unworkable without workarounds. A proxy solves both issues cleanly.

Are B2 upload URLs really single-use? What happens if I retry a failed upload with the same URL?

Yes, `b2_get_upload_url` returns single-use upload URLs. If an upload fails partway through and you retry with the same URL, the retry will fail too. Always request a fresh upload URL from the proxy for each upload attempt — including retries. This is a significant behavioral difference from S3 presigned PUT URLs, which are time-limited but reusable within their validity window.

Do I need a paid Bubble plan for this integration?

Yes — Backend Workflows (API Workflows) are required and unavailable on Bubble's Free plan. The native B2 proxy call happens in a Backend Workflow step. Upgrade to at least the Starter plan ($32/month). Your Backblaze B2 storage can remain on the free tier (10 GB) during development and early production.

How long is the B2 authorization token valid, and what happens when it expires?

The `authorizationToken` from `b2_authorize_account` is valid for up to 24 hours. Your proxy caches it for 23 hours to build in a safety margin. If the token is invalidated earlier (e.g., because you modified the application key in the Backblaze console), subsequent B2 API calls will return 401. A well-built proxy handles this by catching 401 responses, clearing the cache, re-authorizing, and retrying the request automatically.

Is there a size limit on files I can upload using the native B2 API?

For the simple upload path (`b2_get_upload_url` + direct POST), the practical limit is determined by available memory and network reliability rather than a strict API limit, but Backblaze recommends using the large-file API for files over approximately 100 MB. The large-file API (`b2_start_large_file` → `b2_get_upload_part_url` per chunk → `b2_finish_large_file`) supports files up to 10 TB in theory, with each part between 5 MB and 5 GB.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.