# Backblaze B2 Cloud Storage

- Tool: Bubble
- Difficulty: Advanced
- Time required: 3–5 hours
- Last updated: July 2026

## 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.

## 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`.

## Before you start

- 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 bucket and key reference
BUCKET_NAME=my-bubble-private-files
BUCKET_ID=e73ede9969c64287527b  # from Backblaze console bucket details
KEY_ID=your_application_keyID_here
APPLICATION_KEY=your_applicationKey_here

# HTTP Basic Auth for b2_authorize_account:
# Authorization: Basic base64(keyID:applicationKey)
# This is handled inside your proxy function, not in Bubble directly
```

**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.

```
// Cloudflare Worker — Backblaze B2 Native API Proxy
// Environment vars: B2_KEY_ID, B2_APPLICATION_KEY, B2_BUCKET_NAME, B2_BUCKET_ID

let cachedAuth = null;

async function authorizeAccount(env) {
  if (cachedAuth && Date.now() < cachedAuth.expiresAt) {
    return cachedAuth;
  }

  const credentials = btoa(`${env.B2_KEY_ID}:${env.B2_APPLICATION_KEY}`);
  const res = await fetch('https://api.backblazeb2.com/b2api/v2/b2_authorize_account', {
    headers: { 'Authorization': `Basic ${credentials}` },
  });

  if (!res.ok) throw new Error(`B2 auth failed: ${res.status}`);

  const data = await res.json();
  cachedAuth = {
    apiUrl: data.apiUrl,
    authorizationToken: data.authorizationToken,
    downloadUrl: data.downloadUrl,
    expiresAt: Date.now() + 23 * 60 * 60 * 1000, // 23 hours
  };
  return cachedAuth;
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const auth = await authorizeAccount(env);

    // POST /getUploadUrl
    if (url.pathname === '/getUploadUrl' && request.method === 'POST') {
      const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_get_upload_url`, {
        method: 'POST',
        headers: {
          'Authorization': auth.authorizationToken,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ bucketId: env.B2_BUCKET_ID }),
      });
      const data = await res.json();
      return new Response(JSON.stringify({
        uploadUrl: data.uploadUrl,
        uploadAuthorizationToken: data.authorizationToken,
      }), { headers: { 'Content-Type': 'application/json' } });
    }

    // POST /getDownloadAuth
    if (url.pathname === '/getDownloadAuth' && request.method === 'POST') {
      const { fileName, validDurationInSeconds } = await request.json();
      const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_get_download_authorization`, {
        method: 'POST',
        headers: {
          'Authorization': auth.authorizationToken,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          bucketId: env.B2_BUCKET_ID,
          fileNamePrefix: fileName,
          validDurationInSeconds: validDurationInSeconds || 3600,
        }),
      });
      const data = await res.json();
      // Construct the full download URL
      const downloadUrl = `${auth.downloadUrl}/file/${env.B2_BUCKET_NAME}/${encodeURIComponent(fileName)}?Authorization=${data.authorizationToken}`;
      return new Response(JSON.stringify({ downloadUrl }), {
        headers: { 'Content-Type': 'application/json' },
      });
    }

    // GET /listFiles
    if (url.pathname === '/listFiles' && request.method === 'GET') {
      const res = await fetch(`${auth.apiUrl}/b2api/v2/b2_list_file_names`, {
        method: 'POST',
        headers: {
          'Authorization': auth.authorizationToken,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ bucketId: env.B2_BUCKET_ID, maxFileCount: 100 }),
      });
      const data = await res.json();
      const files = data.files.map(f => ({
        name: f.fileName,
        size: f.contentLength,  // NOTE: native B2 uses contentLength, not size
        uploadedAt: new Date(f.uploadTimestamp).toISOString(),
        fileId: f.fileId,
      }));
      return new Response(JSON.stringify({ files }), {
        headers: { 'Content-Type': 'application/json' },
      });
    }

    return new Response('Not found', { status: 404 });
  },
};
```

**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.

```
{
  "api_name": "B2 Native Proxy",
  "shared_headers": {
    "X-Proxy-Token": "<your_secret | Private: checked>"
  },
  "calls": [
    {
      "name": "Get Upload URL",
      "method": "POST",
      "url": "https://your-proxy.workers.dev/getUploadUrl",
      "body": {},
      "response_fields": ["uploadUrl", "uploadAuthorizationToken"]
    },
    {
      "name": "Get Download Auth",
      "method": "POST",
      "url": "https://your-proxy.workers.dev/getDownloadAuth",
      "body": {
        "fileName": "<dynamic>",
        "validDurationInSeconds": 3600
      },
      "response_fields": ["downloadUrl"]
    },
    {
      "name": "List Files",
      "method": "GET",
      "url": "https://your-proxy.workers.dev/listFiles",
      "response_fields": ["files (list)"]
    }
  ]
}
```

**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.

```
// Bubble 'Run JavaScript' (Toolbox plugin) — B2 native upload
// Inputs: uploadUrl, uploadAuthToken, contentType
// The file comes from a Bubble FileUploader element

(async function() {
  const uploadUrl = properties.uploadUrl;
  const uploadAuthToken = properties.uploadAuthToken;
  const contentType = properties.contentType || 'application/octet-stream';

  const fileInput = document.querySelector('#fileUploader input[type="file"]');
  const file = fileInput ? fileInput.files[0] : null;

  if (!file) {
    bubble_fn_result({ success: false, error: 'No file selected' });
    return;
  }

  // B2 native upload requires URL-encoded filename in header
  const encodedFileName = encodeURIComponent(file.name);

  try {
    const response = await fetch(uploadUrl, {
      method: 'POST',  // B2 native upload uses POST, not PUT
      headers: {
        'Authorization': uploadAuthToken,  // upload-specific token, NOT Bearer
        'X-Bz-File-Name': encodedFileName,  // MUST be URL-encoded
        'Content-Type': contentType,
        'Content-Length': file.size.toString(),
      },
      body: file,
    });

    if (response.ok) {
      const result = await response.json();
      bubble_fn_result({ success: true, fileName: result.fileName, fileId: result.fileId });
    } else {
      const errText = await response.text();
      bubble_fn_result({ success: false, error: errText });
    }
  } catch (err) {
    bubble_fn_result({ success: false, error: err.message });
  }
})();
```

**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.

```
// Bubble workflow — generate a timed download URL on button click
// Step 1: Call API → B2 Native Proxy → Get Download Auth
//   fileName: Current user's file record's 'b2_file_name' field
//   validDurationInSeconds: 3600
// Step 2: Open external website (new tab)
//   URL: Result of step 1's 'downloadUrl'

// The proxy constructs:
// {downloadUrl}/file/{bucketName}/{encodedFileName}?Authorization={token}
// Example:
// https://f004.backblazeb2.com/file/my-bucket/My%20File.pdf?Authorization=4_0022...

// Privacy rules reminder:
// Data → Privacy → 'UserFile' type:
// - 'b2_file_name' field: condition = Current User = This UserFile's owner
// Without this, any logged-in user could call the download auth with another user's filename
```

**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.

```
// Enhanced proxy with retry-on-401 pattern and KV storage
export default {
  async fetch(request, env, ctx) {
    // Try to get cached token from KV store
    let auth = null;
    const cached = await env.B2_CACHE.get('auth', 'json');
    if (cached && Date.now() < cached.expiresAt) {
      auth = cached;
    } else {
      auth = await fetchNewAuth(env);
      // Store in KV with 23-hour TTL
      ctx.waitUntil(
        env.B2_CACHE.put('auth', JSON.stringify(auth), { expirationTtl: 82800 })
      );
    }

    // Attempt the actual B2 call
    const result = await handleRequest(request, env, auth);

    // If 401, re-auth and retry once
    if (result.status === 401) {
      auth = await fetchNewAuth(env);
      ctx.waitUntil(
        env.B2_CACHE.put('auth', JSON.stringify(auth), { expirationTtl: 82800 })
      );
      return handleRequest(request, env, auth);
    }

    return result;
  },
};

async function fetchNewAuth(env) {
  const credentials = btoa(`${env.B2_KEY_ID}:${env.B2_APPLICATION_KEY}`);
  const res = await fetch('https://api.backblazeb2.com/b2api/v2/b2_authorize_account', {
    headers: { 'Authorization': `Basic ${credentials}` },
  });
  const data = await res.json();
  return {
    apiUrl: data.apiUrl,
    authorizationToken: data.authorizationToken,
    downloadUrl: data.downloadUrl,
    expiresAt: Date.now() + 23 * 60 * 60 * 1000,
  };
}
```

**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.

## 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.

## 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.

### 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.

### 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.

## 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.

```
// WRONG — spaces not encoded, reusing URL
const fileName = 'My Document.pdf';  // spaces will fail
fetch(sameUploadUrl, { headers: { 'X-Bz-File-Name': fileName } });

// CORRECT
const encodedFileName = encodeURIComponent('My Document.pdf');  // 'My%20Document.pdf'
const { uploadUrl, uploadAuthorizationToken } = await fetchFreshUploadUrl();
fetch(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.

## 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.

---

Source: https://www.rapidevelopers.com/bubble-integrations/backblaze-b2-cloud-storage
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/backblaze-b2-cloud-storage
