- 09 Oct 2026
- Print
- DarkLight
How to Use the Backblaze B2 SDK for TypeScript and JavaScript
- Updated on 09 Oct 2026
- Print
- DarkLight
The Backblaze B2 SDK for TypeScript and JavaScript provides a high-level interface to the B2 Native API. The SDK is published on npm as @backblaze-labs/b2-sdk and is maintained by Backblaze.
You supply an application key and key ID for the account you are connecting to, create a B2Client, and call authorize(). From there, the high-level B2Client, Bucket, and B2Object classes give you typed access to B2 Native API features such as multipart uploads with SHA-1 verification, server-side copy, object lock, event notifications, and application key management.
If you instead want to use an S3-compatible toolchain, you can point the AWS SDK for JavaScript at your bucket's S3 endpoint, or use this SDK's @backblaze-labs/b2-sdk/s3 helpers to generate the configuration and presigned URLs that an S3 client needs. For a comparison of the two APIs, see Backblaze B2 APIs: S3-Compatible, B2 Native, and Partner API.
Note:
This SDK is currently incubating in Backblaze Labs and has not yet reached a 1.0 release. The interface may change before a general-availability release. Pin a version in production and review the changelog before upgrading.
Version 0.4.0 includes several breaking type changes; see the migration guide if you are upgrading from 0.3.
To use the Backblaze B2 SDK for TypeScript and JavaScript with Backblaze B2, you must:
Run a supported runtime: Node.js 22.3 or later, Bun, Deno 2.x, or a modern browser. The SDK is also compatible with edge runtimes that provide the required Web APIs, such as Cloudflare Workers and Vercel Edge. The runtime must provide fetch, Web Streams, crypto.subtle, and AbortSignal. Node.js versions earlier than 22.3 are not supported. For how each runtime is tested, see Runtime support.
Install the package with npm install @backblaze-labs/b2-sdk (or the pnpm/yarn equivalent).
Create a B2Client with your application key ID and application key, then call authorize() before making any other calls. Supplying credentials through environment variables, rather than hard-coding them, is recommended.
The following example demonstrates how to authorize, retrieve a bucket, and upload a small file after setting B2_APPLICATION_KEY_ID and B2_APPLICATION_KEY in your environment:
import { B2Client, BufferSource } from '@backblaze-labs/b2-sdk'
const { B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY } = process.env
if (!B2_APPLICATION_KEY_ID || !B2_APPLICATION_KEY) {
throw new Error('Missing B2 credentials')
}
const client = new B2Client({
applicationKeyId: B2_APPLICATION_KEY_ID,
applicationKey: B2_APPLICATION_KEY,
})
await client.authorize()
const bucket = await client.getBucket('my-app-data')
if (!bucket) throw new Error('bucket not found')
const data = new TextEncoder().encode('Hello, B2!')
const file = await bucket.upload({
fileName: 'hello.txt',
source: new BufferSource(data),
contentType: 'text/plain',
})
console.log(`Uploaded: ${file.fileName} (${file.contentLength} bytes)`)After authorize() succeeds, the client holds the authorization state for subsequent calls. By default this state is kept in memory. For long-running or restart-prone Node.js processes, see Persist authorization across restarts to cache it on disk.
The complete, type-signature-level API reference is generated from source on every push to main and published at backblaze-labs.github.io/b2-sdk-typescript.
Work with buckets
Use the client to list, create, update, and delete buckets. getBucket returns null when the bucket does not exist, so check the result before using it.
import { BucketType } from '@backblaze-labs/b2-sdk'
// List all buckets
const buckets = await client.listBuckets()
// Get a bucket by name
const bucket = await client.getBucket('my-bucket')
if (!bucket) throw new Error('bucket not found')
// Update bucket settings
await bucket.update({
bucketType: BucketType.AllPublic,
lifecycleRules: [
{
fileNamePrefix: 'logs/',
daysFromUploadingToHiding: 30,
daysFromHidingToDeleting: null,
},
],
})
// Delete a bucket (the bucket must be empty)
await bucket.delete()The SDK exports BucketType, RetentionMode, LegalHoldValue, Capability, EventType, and EncryptionMode as as const objects. These provide type-safe alternatives to the raw string literals used by the B2 Native API.
Note:
Both styles are accepted at the type level. You can pass BucketType.AllPublic or the string 'allPublic'. We recommend the named constants, such as BucketType.AllPublic, because your editor can autocomplete them and catch typing errors.
Upload files
Files up to the account's recommended part size (returned by authorize(), typically 100 MB) upload in a single request. Larger files automatically use the B2 multipart upload protocol. When the source supports random access, such as a Uint8Array, Blob, or File, parts upload in parallel. The partSize option sets the size of each part in a multipart upload; it does not change which files use multipart.
Note:
Byte-level onProgress callbacks are throttled to at most one every 100 ms; part-completion and final callbacks are always delivered immediately. Set progressIntervalMillis: 0 to receive a callback for every chunk.
import { BufferSource, BlobSource } from '@backblaze-labs/b2-sdk'
// From a Uint8Array
await bucket.upload({
fileName: 'data.bin',
source: new BufferSource(myUint8Array),
})
// From a Blob or File (browser)
const selectedFile = fileInput.files?.[0]
if (!selectedFile) throw new Error('no file selected')
await bucket.upload({
fileName: 'photo.jpg',
source: new BlobSource(selectedFile),
contentType: 'image/jpeg',
})
// Large file with explicit part size, concurrency, progress, and timeout
await bucket.upload({
fileName: 'backup.tar.gz',
source: new BlobSource(largeBlob),
concurrency: 8,
partSize: 64 * 1024 * 1024,
onProgress: (event) => {
console.log(`${event.bytesTransferred}/${event.totalBytes} bytes`)
},
signal: AbortSignal.timeout(300_000),
})Resume a failed multipart upload
There are two ways to continue a multipart upload that failed partway through. Both upload into an existing unfinished large file, and both require the same part layout as the original upload, so pass the same partSize (and the same contentType and file info) that you used originally.
resume: true finds the newest unfinished large file with the same name and compatible upload options, and uploads into it. It re-sends every part; it does not skip parts that are already on the server. If no compatible unfinished file is found, a new upload starts.
resumeFileId targets a specific unfinished large file by ID. The SDK re-reads each local part, recomputes its SHA-1, and skips parts whose SHA-1 matches the part already on the server. If the ID does not identify a compatible unfinished file for this upload, the SDK throws ResumeFileIdMismatchError. If the resumeFileId is passed for a file small enough for a single-request upload the request is also rejected.
// Continue into a compatible unfinished upload (re-sends every part)
await bucket.upload({
fileName: 'backup.tar.gz',
source: new BlobSource(largeBlob),
partSize: 64 * 1024 * 1024,
resume: true,
})
// Or target a specific unfinished large file and skip parts that already match
await bucket.upload({
fileName: 'backup.tar.gz',
source: new BlobSource(largeBlob),
partSize: 64 * 1024 * 1024,
resumeFileId: knownLargeFileId,
})Stream uploads with WritableStream
To upload data that is not fully in memory, pipe any ReadableStream<Uint8Array> directly into B2. The SDK buffers incoming data into parts of partSize bytes, uploads up to concurrency parts in parallel, and applies backpressure to the source, so memory use stays at roughly (concurrency + 1) × partSize.
const { writable, done } = bucket.file('logs.ndjson').createWriteStream({
partSize: 16 * 1024 * 1024,
concurrency: 4,
})
// Pipe from any ReadableStream source: fetch().body, fs.createReadStream(...) via toWeb, etc.
const response = await fetch('https://example.com/export.ndjson')
if (!response.body) throw new Error('response has no body')
await response.body.pipeTo(writable)
const fileVersion = await done
console.log(`Streamed upload finished: ${fileVersion.fileName} (${fileVersion.contentLength} bytes)`)Note:
Streaming uploads cannot be resumed, because the total size and per-part SHA-1 values are not known in advance. When resume support matters, use the buffered upload path described in Resume a failed multipart upload.
Download files
Download a file by name, request a specific byte range, or stream a large file in parallel ranges. For a parallel ranged download, createReadStream needs the file's ID and total size, which you can get from head().
// Download by file name
const result = await bucket.download('hello.txt')
const text = await new Response(result.body).text()
// Download a byte range
const partial = await bucket.download('large-file.bin', {
range: 'bytes=0-1023',
})
// Parallel ranged download for large files. Each range is a separate request
// that uses the client's retry policy, so a transient failure such as a 503
// is retried for that range only.
const obj = bucket.file('big-dataset.parquet')
const { headers } = await obj.head()
const stream = obj.createReadStream(headers.fileId, headers.contentLength, {
concurrency: 4,
rangeSize: 10 * 1024 * 1024,
})Manage files
The Bucket class provides methods for listing, inspecting, hiding, copying, and deleting files. Use paginateFileNames to iterate every file without handling pagination yourself, and head to retrieve metadata without transferring the file body.
// List a single page of files
const listing = await bucket.listFileNames({ prefix: 'photos/', pageSize: 100 })
// Iterate all files (async generator; handles pagination automatically)
for await (const file of bucket.paginateFileNames({ prefix: 'logs/' })) {
console.log(file.fileName, file.contentLength)
}
// Look up the latest visible version by name (null if missing or hidden)
const info = await bucket.getFileInfoByName('hello.txt')
// Fetch metadata only, without transferring the body (HTTP HEAD)
const { headers } = await bucket.head('hello.txt')
console.log(headers.contentLength, headers.contentSha1)
// Hide a file (soft delete)
await bucket.hideFile('old-config.json')
// Restore visibility by removing the latest hide marker
await bucket.unhideFile('old-config.json')
// Delete a specific file version
await bucket.deleteFileVersion('file.txt', fileId)Copy files server-side
Use copyFile for a single-call server-side copy. It issues one B2 Native API b2_copy_file request, so it is subject to that operation's maximum file size. For larger files, use copyLargeFile, which splits the source into parts copied in parallel with the B2 Native API's b2_copy_part operation; no file bytes pass through the client. When the source is no larger than the part size, copyLargeFile performs a single b2_copy_file copy instead.
// Server-side copy (single call)
await bucket.copyFile({
sourceFileId: originalFileId,
fileName: 'copy-of-file.txt',
})
// Server-side multipart copy for large files
await bucket.copyLargeFile({
sourceFileId: originalFileId,
fileName: 'big-replica.bin',
partSize: 64 * 1024 * 1024,
concurrency: 4,
})Delete files in bulk
For cleanup at scale, deleteMany deletes a known set of file versions with bounded concurrency, and deleteAll stream-deletes every version matching a prefix (or the whole bucket if no prefix is given). deleteAll yields one event per version and never holds the full file list in memory. Pass dryRun: true to preview what would be deleted; in a dry run, each matching version is reported as a skip event and nothing is deleted.
// Delete a known set of file versions with bounded concurrency
const result = await bucket.deleteMany(
[
{ fileName: 'a.txt', fileId: id1 },
{ fileName: 'b.txt', fileId: id2 },
],
{ concurrency: 10 },
)
console.log(`deleted=${result.deleted} errors=${result.errors.length}`)
// Stream-delete every version matching a prefix
for await (const event of bucket.deleteAll({ prefix: 'tmp/', dryRun: false })) {
if (event.type === 'delete') console.log('deleted', event.fileName)
else if (event.type === 'skip') console.log('would delete', event.fileName)
else if (event.type === 'error') console.warn('failed', event.fileName, event.message)
}Manage application keys
Application keys can be created, listed, and deleted through the client. You can restrict a key to a bucket, a name prefix, a set of capabilities, and an expiration window.
import { Capability } from '@backblaze-labs/b2-sdk'
const key = await client.createKey({
capabilities: [Capability.ReadFiles, Capability.WriteFiles],
keyName: 'my-app-key',
bucketIds: [bucket.id],
namePrefix: 'uploads/',
validDurationInSeconds: 86400,
})
const { keys } = await client.listKeys()
await client.deleteKey(key.applicationKeyId)Check capabilities before acting
To fail fast on the client instead of waiting for a server 401 or 403, check the current authorization's capabilities with hasCapabilities, which returns { ok, missing }. SDK operations do not perform this check automatically. If you want a typed error, throw B2InsufficientCapabilityError yourself. This error extends Error, not B2Error.
import { Capability } from '@backblaze-labs/b2-sdk'
import { B2InsufficientCapabilityError } from '@backblaze-labs/b2-sdk/errors'
const required = [Capability.ReadFiles, Capability.WriteFiles]
const auth = client.accountInfo.getAuth()
if (!auth) throw new Error('Not authorized. Call authorize() first.')
const available = auth.apiInfo.storageApi.allowed.capabilities
const { ok, missing } = client.hasCapabilities(required)
if (!ok) {
throw new B2InsufficientCapabilityError(required, available, missing)
}Encrypt files server-side
The SDK supports both server-side encryption modes that B2 offers: SSE-B2 with Backblaze-managed keys, and SSE-C with customer-provided keys.
import { BufferSource, EncryptionKey, SSE_B2, sseCustomer } from '@backblaze-labs/b2-sdk'
const data = new TextEncoder().encode('secret data')
// SSE-B2: Backblaze-managed keys
await bucket.upload({
fileName: 'encrypted.dat',
source: new BufferSource(data),
serverSideEncryption: SSE_B2,
})
// SSE-C with precomputed base64 key and MD5 digest
await bucket.upload({
fileName: 'secret.dat',
source: new BufferSource(data),
serverSideEncryption: sseCustomer(base64Key, base64KeyMd5),
})
// SSE-C with a generated 256-bit key. EncryptionKey computes the MD5 internally.
const key = await EncryptionKey.generate()
await bucket.upload({
fileName: 'secret.dat',
source: new BufferSource(data),
serverSideEncryption: key,
})Note
EncryptionKey.generate() creates a random key using the runtime's Web Crypto API; use EncryptionKey.fromBytes() to wrap an existing 32-byte key. In both cases, the SDK computes the key's MD5 and redacts the key material in JSON.stringify, toString, and Node's util.inspect output. The key remains readable as a property of the object, so do not log or serialize it by other means. Store SSE-C keys securely: you need the same key to download the file.
Apply object lock and legal hold
Set file retention and legal hold through the bucket. To shorten governance-mode retention, the application key must have the bypassGovernance capability, and the call must pass the { bypassGovernance: true } option. Bypassing has no effect on compliance-mode retention.
import { LegalHoldValue, RetentionMode } from '@backblaze-labs/b2-sdk'
await bucket.updateFileRetention('important.pdf', fileId, {
mode: RetentionMode.Governance,
retainUntilTimestamp: Date.now() + 365 * 24 * 60 * 60 * 1000,
})
// Shorten a governance-mode retention. The key needs the bypassGovernance capability.
await bucket.updateFileRetention(
'important.pdf',
fileId,
{ mode: RetentionMode.Governance, retainUntilTimestamp: Date.now() + 24 * 60 * 60 * 1000 },
{ bypassGovernance: true },
)
await bucket.updateFileLegalHold('evidence.pdf', fileId, LegalHoldValue.On)Configure event notifications
Register notification rules on a bucket to have B2 send a webhook when matching events occur.
import { EventType } from '@backblaze-labs/b2-sdk'
const { B2_WEBHOOK_SECRET } = process.env
if (!B2_WEBHOOK_SECRET) throw new Error('Missing B2 webhook secret')
await bucket.setNotificationRules([
{
name: 'upload-notify',
eventTypes: [EventType.ObjectCreatedAll],
isEnabled: true,
isSuspended: false,
objectNamePrefix: '',
suspensionReason: '',
targetConfiguration: {
targetType: 'webhook',
url: 'https://my-app.com/webhooks/b2',
hmacSha256SigningSecret: B2_WEBHOOK_SECRET,
},
},
])On the receiving side, verify the X-Bz-Event-Notification-Signature header before you trust the payload. The @backblaze-labs/b2-sdk/notifications subpath provides HMAC-SHA256 helpers so you do not have to implement constant-time signature checking yourself.
import {
B2_WEBHOOK_SIGNATURE_HEADER,
requireValidWebhook,
} from '@backblaze-labs/b2-sdk/notifications'
const { B2_WEBHOOK_SECRET } = process.env
if (!B2_WEBHOOK_SECRET) throw new Error('Missing B2 webhook secret')
// Inside your HTTP handler.
const body = new Uint8Array(await request.arrayBuffer())
// B2_WEBHOOK_SECRET must be the exact hmacSha256SigningSecret string
// configured on the notification rule.
const payload = await requireValidWebhook({
body,
signature: request.headers.get(B2_WEBHOOK_SIGNATURE_HEADER) ?? undefined,
secret: B2_WEBHOOK_SECRET,
})
for (const event of payload.events) {
console.log(event.eventType, event.objectName)
}requireValidWebhook throws on a missing or invalid signature and returns the parsed payload on success. To branch on a boolean instead — for example, to log the failure reason without throwing — use the lower-level verifyWebhookSignature, which returns { valid, reason, payload }.
Note:
The body you pass to the verifier must be the raw request bytes. Any JSON re-serialization changes the bytes and invalidates the HMAC signature.
Share files with a download authorization
To grant temporary access to a prefix without exposing your application key, generate a short-lived download authorization token. The first argument is the file name prefix; the second is the validity period in seconds.
const auth = await bucket.getDownloadAuthorization('photos/', 3600)
Persist authorization across restarts
By default, the client keeps its authorization state in memory, so a restarted process must call authorize() again. In Node.js, FileAccountInfo persists the authorization response to a JSON file on disk. It implements the same AccountInfo interface as the default InMemoryAccountInfo and is a drop-in replacement. Upload URL pools remain in memory.
import { B2Client } from '@backblaze-labs/b2-sdk'
import { FileAccountInfo } from '@backblaze-labs/b2-sdk/auth/file'
const { B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY } = process.env
if (!B2_APPLICATION_KEY_ID || !B2_APPLICATION_KEY) {
throw new Error('Missing B2 credentials')
}
const accountInfo = new FileAccountInfo('/var/cache/my-app/b2-auth.json', {
onWriteError: ({ error }) => console.error('Failed to write B2 auth cache', error),
})
await accountInfo.load()
const client = new B2Client({
applicationKeyId: B2_APPLICATION_KEY_ID,
applicationKey: B2_APPLICATION_KEY,
accountInfo,
})
if (accountInfo.getAuth() === null) {
await client.authorize()
}
await accountInfo.flushed()Note:
load() returns silently when the file is missing or cannot be read; a fresh authorize() then populates new state. flushed() waits for pending writes to finish, but it also resolves when a write fails. To detect failed writes, pass an onWriteError callback as shown above.
Important:
The cache file contains your authorization token, which grants API access with your application key's capabilities while it remains valid. The application key itself is not stored. The SDK writes the file with owner-only (0600) permissions where the platform supports them. Treat the file as sensitive, and keep it out of source control, logs, and shared backups.
Import only what you need: subpath exports
The SDK is organized into subpath exports so bundlers can tree-shake unused code. Most applications need only the main entry; the other subpaths are available when you need them.
// High-level facade (most users need only this)
import { B2Client, Bucket, B2Object } from '@backblaze-labs/b2-sdk'
// Low-level 1:1 bindings for the B2 native endpoints the SDK uses
import { RawClient } from '@backblaze-labs/b2-sdk/raw'
// Error types for catch blocks
import {
B2Error,
ExpiredAuthTokenError,
CapExceededError,
B2InsufficientCapabilityError,
} from '@backblaze-labs/b2-sdk/errors'
// Auth backends
import { InMemoryAccountInfo } from '@backblaze-labs/b2-sdk/auth'
import { FileAccountInfo } from '@backblaze-labs/b2-sdk/auth/file'
// Streaming utilities and the SSE-C key wrapper
import {
IncrementalSha1,
BufferSource,
BlobSource,
EncryptionKey,
} from '@backblaze-labs/b2-sdk/streams'
// Sync engine (local <-> B2)
import { synchronize, LocalFolder, B2Folder } from '@backblaze-labs/b2-sdk/sync'
// S3-compatible client config and SigV4 presigning helpers
// (@aws-sdk/client-s3 is optional; install it only to construct an S3Client)
import {
createS3ClientConfig,
presignS3GetObjectUrl,
presignS3PutObjectUrl,
} from '@backblaze-labs/b2-sdk/s3'
// In-memory B2 server for tests (no network required)
import { B2Simulator } from '@backblaze-labs/b2-sdk/simulator'Use S3-compatible tooling
When you want S3 compatibility — for tooling that already speaks S3, browser direct uploads, or for the Bandwidth Alliance proxy pattern — import the @backblaze-labs/b2-sdk/s3 subpath:
createS3ClientConfig() returns the endpoint, region, credentials, and path-style setting for an AWS S3Client. The S3 endpoint comes from the client's authorization state, and the region is derived from standard s3.<region>.backblazeb2.com endpoints; pass region explicitly for any other endpoint. You must supply your application key ID and application key, because the client's account info (client.accountInfo) does not store the application key.
presignS3GetObjectUrl() and presignS3PutObjectUrl() generate AWS Signature Version 4 pre-signed URLs for downloading and uploading a single object. They sign the URLs themselves and do not require the AWS SDK.
@aws-sdk/client-s3 is an optional peer dependency. Install it only if you construct an S3Client from createS3ClientConfig().
import { S3Client } from '@aws-sdk/client-s3'
import {
createS3ClientConfig,
presignS3GetObjectUrl,
presignS3PutObjectUrl,
} from '@backblaze-labs/b2-sdk/s3'
const { B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY } = process.env
if (!B2_APPLICATION_KEY_ID || !B2_APPLICATION_KEY) {
throw new Error('Missing B2 credentials')
}
// `client` is an authorized B2Client created with the same key pair.
const s3Credentials = {
accountInfo: client.accountInfo,
applicationKeyId: B2_APPLICATION_KEY_ID,
applicationKey: B2_APPLICATION_KEY,
}
// Configuration for the AWS SDK (requires @aws-sdk/client-s3)
const s3 = new S3Client(createS3ClientConfig(s3Credentials))
// Presigned URLs (no AWS SDK required)
const downloadUrl = await presignS3GetObjectUrl({
...s3Credentials,
bucketName: 'my-bucket',
fileName: 'photos/cat.jpg',
expiresIn: 900,
})
const uploadUrl = await presignS3PutObjectUrl({
...s3Credentials,
bucketName: 'my-bucket',
fileName: 'uploads/report.pdf',
contentType: 'application/pdf',
expiresIn: 900,
})Note: A presigned URL is a bearer credential: anyone who has it can perform the signed operation until it expires. Keep expiresIn as short as your workflow allows.
For details on the S3-Compatible API, including endpoint formats and supported operations, see Introduction to the S3-Compatible API.
Configure reliability and security
Retry behavior
The SDK automatically retries transient errors with exponential backoff and jitter:
401 expired_auth_token — the SDK re-authorizes with the client's credentials and retries the request. Uploads instead retry with a fresh upload URL.
408, 429, 500, 502, 503, and 504 — retried with backoff. When the response includes a Retry-After header with a delay in seconds, the SDK waits that long instead, up to the maximum retry delay.
Network errors — retried with backoff when the request is safe to replay.
Permanent errors (403 cap_exceeded, 404 not_found, and similar) — thrown immediately.
By default, the SDK retries up to 5 times, starting with a 1-second delay and capping each delay at 64 seconds. The following example overrides those defaults with custom values:
const client = new B2Client({
applicationKeyId: '...',
applicationKey: '...',
retry: {
maxRetries: 10,
maxRetryDelayMs: 120_000,
initialRetryDelayMs: 500,
},
})SSRF guard
The default transport includes a URL guard that defends against URL-substitution attacks, in which a compromised or hostile endpoint returns an upload or download URL pointing at an internal service to trick the SDK into making an authenticated request to it. Once the client is authorized (after authorize(), or at construction when the account info already holds cached authorization), the guard allows only hosts in the authorized B2 realm plus any suffixes you add with allowedHostSuffixes; a request to any other host throws B2SsrfError. While the guard is active, literal IP addresses (including cloud metadata addresses such as 169.254.169.254), localhost, *.internal, and *.local are always rejected, even if you add them to allowedHostSuffixes.
// Extend the allow-list without disabling the guard
new B2Client({
applicationKeyId,
applicationKey,
allowedHostSuffixes: ['internal-proxy.example'],
})Note: Passing allowedHostSuffixes: [] disables the guard entirely and should be reserved for trusted tests or controlled local harnesses. Passing a custom transport also opts out of the guard, since hardening then becomes the responsibility of your transport.
Identify your traffic with a User-Agent
Requests made by the SDK in runtimes that permit setting the User-Agent header include a User-Agent that Backblaze can match in server logs. The default has this form, where the runtime token is node, bun, deno, or browser:
b2-sdk-typescript/<version> (typescript; @backblaze-labs/b2-sdk; node/<version>; <platform>; <arch>)
Both b2-sdk-typescript/ and @backblaze-labs/b2-sdk are stable, documented tokens that you can use to identify SDK traffic when the User-Agent header is available. To prepend your own application identifier, set the userAgent option:
const client = new B2Client({
applicationKeyId,
applicationKey,
userAgent: 'my-app/1.0',
})
// → "my-app/1.0 b2-sdk-typescript/<version> (typescript; @backblaze-labs/b2-sdk; node/<version>; <platform>; <arch>)"The userAgent option applies to the default transport. If you supply a custom transport, set the header in your transport.
Supply a custom transport
The SDK uses a pluggable transport layer. The default FetchTransport uses the native fetch API. To route requests through your own implementation, provide an object that implements HttpTransport. The SDK still applies its retry and re-authorization handling to a custom transport, but the SSRF guard and the userAgent option apply only to the default transport.
import type { HttpRequest, HttpResponse, HttpTransport } from '@backblaze-labs/b2-sdk'
class MyTransport implements HttpTransport {
async send(request: HttpRequest): Promise<HttpResponse> {
const response = await fetch(request.url, {
method: request.method,
headers: request.headers ?? {},
body: request.body ?? null,
signal: request.signal ?? null,
})
return {
status: response.status,
headers: response.headers,
body: response.body,
json: <T>() => response.json() as Promise<T>,
text: () => response.text(),
arrayBuffer: () => response.arrayBuffer(),
}
}
}
const client = new B2Client({
applicationKeyId: '...',
applicationKey: '...',
transport: new MyTransport(),
})Handle errors
Errors returned by the B2 API are thrown as typed B2Error subclasses that carry the B2 error code, the HTTP status, and whether the error is retriable. Errors that originate on the client side — such as network failures, URLs rejected by the SSRF guard, and B2InsufficientCapabilityError — extend Error rather than B2Error. Catch the specific subclass you care about, fall back to B2Error for other API errors, and re-throw anything else.
import { BucketType } from '@backblaze-labs/b2-sdk'
import {
B2Error,
CapExceededError,
DuplicateBucketNameError,
} from '@backblaze-labs/b2-sdk/errors'
try {
await client.createBucket({ bucketName: 'my-app-test-bucket', bucketType: BucketType.AllPrivate })
} catch (err) {
if (err instanceof DuplicateBucketNameError) {
console.log('Bucket already exists')
} else if (err instanceof CapExceededError) {
console.log('Usage cap exceeded; review the caps set on your account')
} else if (err instanceof B2Error) {
console.log(`B2 error: ${err.code} (status ${err.status}, retryable: ${err.retryable})`)
} else {
throw err
}
}Test without a network
The SDK ships an in-memory B2 simulator so you can unit-test your integration without network access or live credentials. Pass the simulator's transport to a B2Client and use the client exactly as you would in production.
import { describe, it, expect, beforeEach } from 'vitest'
import { B2Client, BucketType } from '@backblaze-labs/b2-sdk'
import { B2Simulator } from '@backblaze-labs/b2-sdk/simulator'
import { BufferSource } from '@backblaze-labs/b2-sdk/streams'
describe('my app', () => {
let client: B2Client
beforeEach(async () => {
const sim = new B2Simulator()
client = new B2Client({
applicationKeyId: 'test',
applicationKey: 'test',
transport: sim.transport(),
})
await client.authorize()
})
it('uploads and retrieves a file', async () => {
const bucket = await client.createBucket({
bucketName: 'test-bucket',
bucketType: BucketType.AllPrivate,
})
await bucket.upload({
fileName: 'test.txt',
source: new BufferSource(new TextEncoder().encode('hello')),
})
const listing = await bucket.listFileNames()
expect(listing.files).toHaveLength(1)
const [uploadedFile] = listing.files
expect(uploadedFile?.fileName).toBe('test.txt')
})
})Runtime support
Runtime | Version | Status |
Node.js | 22.3+ | Primary target. The test suite runs in CI on Node.js 22.22.2 and 24 across Linux, Windows, and macOS. |
Bun | latest | The test suite runs in CI. |
Deno | 2.x | Typechecked in CI with deno check; the test suite is not run under Deno. |
Browsers | Chromium, Firefox, WebKit | The test suite runs in CI in each engine via Playwright. |
Cloudflare Workers | — | Compatible with environments that provide the required Web APIs. Not currently exercised by the SDK's CI suite. |
Vercel Edge | — | Compatible with environments that provide the required Web APIs. Not currently exercised by the SDK's CI suite. |
The SDK requires fetch, Web Streams, crypto.subtle, and AbortSignal.
Learn more
The B2 Native API powers thousands of applications and integrations. If you are new to the B2 Native API, see Introduction to the B2 Native API and the B2 Native API how-to guides. For the full SDK reference, see backblaze-labs.github.io/b2-sdk-typescript, and for the source and issue tracker, see the repository on GitHub.
If you have issues using this SDK with Backblaze B2, let us know by emailing us at b2feedback@backblaze.com.