Files
claudetools/.claude/skills/b2/references/api-reference.md
Mike Swanson 96fb4110ea Add b2 skill: Backblaze B2 management CLI (storage cost, prefix purge)
B2 Native API v3 client for the ACG B2 account: status, buckets, keys,
files, bucket-size, usage/cost ($0.00695/GB), gated create/delete bucket+key,
and gated lifecycle-based delete-prefix/lifecycle-remove for prefix purges.
Read-only by default; destructive ops require --confirm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:31:09 -07:00

9.2 KiB

Backblaze B2 Native API v3 Reference

Verified spec for the methods used by this skill, against the live ACG B2 account (accountId 46f69bc61163, region us-west-001). All facts below were confirmed against the live API.


Authorization (v3)

  • Authorize URL (fixed, global): https://api.backblazeb2.com/b2api/v3/b2_authorize_account
  • Auth: HTTP Basic. Username = key id, password = application key.
  • Method: GET in the B2 docs; this skill issues it as an HTTPS POST with the Basic header, which the endpoint accepts.

Authorize response (v3 shape)

{
  "accountId": "46f69bc61163",
  "authorizationToken": "<token, valid 24h>",
  "apiInfo": {
    "storageApi": {
      "apiUrl": "https://api001.backblazeb2.com",
      "s3ApiUrl": "https://s3.us-west-001.backblazeb2.com",
      "downloadUrl": "https://f001.backblazeb2.com",
      "recommendedPartSize": 100000000,
      "absoluteMinimumPartSize": 5000000,
      "capabilities": ["listBuckets", "listFiles", "..."],
      "bucketId": null,
      "namePrefix": null
    }
  }
}
  • v3 nesting: apiUrl / s3ApiUrl / downloadUrl / capabilities / bucketId / namePrefix live under apiInfo.storageApi. (v2 returned apiUrl / downloadUrl at the top level — do NOT parse them there for v3.)
  • bucketId == null and namePrefix == null means an account-wide key; non-null means the key is scoped to one bucket / name prefix.
  • The per-account apiUrl is dynamic and comes ONLY from this response — never from a config file.

Cache model

The skill caches authorizationToken + apiUrl + accountId (plus the other fields) in .claude/skills/b2/.cache/auth.json with an authorized_at timestamp. It is treated as valid for ~23h (B2 tokens last 24h). A 401 with code expired_auth_token or bad_auth_token triggers exactly one re-authorize

  • retry. The cached token is a secret; the cache dir is gitignored.

Subsequent calls

  • URL: POST <apiUrl>/b2api/v3/<method>
  • Header: Authorization: <authorizationToken> (raw token, not Basic).
  • Body: a JSON object.
  • Content-Type: application/json.

Error shape

B2 returns HTTP 4xx with a JSON body:

{ "status": 400, "code": "<string>", "message": "<text>" }

The skill surfaces status / code / message verbatim. On 401 with expired_auth_token / bad_auth_token it re-authorizes once and retries; all other errors raise. Destructive calls are never retried automatically.


Methods used

All POST to <apiUrl>/b2api/v3/<method>.

b2_list_buckets

  • Body: {"accountId": "<acct>", "bucketId": <opt>}
  • Returns: {"buckets": [{bucketName, bucketId, bucketType, revision, fileLockConfiguration:{isFileLockEnabled}, lifecycleRules, options}]}
  • Pass bucketId to scope the result to a single bucket.
  • revision (int) is the bucket's optimistic-concurrency version. NOTE: this account's b2_update_bucket does NOT accept ifRevisionMatch, so this value is informational only — it is read but never sent back on a write.
  • lifecycleRules (array) is the bucket's current lifecycle rule set (see the rule shape under b2_update_bucket). An empty array means no rules.

b2_list_keys

  • Body: {"accountId": "<acct>", "maxKeyCount": 1000, "startApplicationKeyId": <opt>}
  • Returns: {"keys": [{keyName, applicationKeyId, capabilities:[...], bucketId, namePrefix, expirationTimestamp}], "nextApplicationKeyId": <null when done>}
  • Paginate on nextApplicationKeyId.

b2_list_file_names

  • Body: {"bucketId": "<id>", "maxFileCount": 10000, "prefix": <opt>, "startFileName": <opt>}
  • Returns: {"files": [{fileName, contentLength, action, uploadTimestamp, ...}], "nextFileName": <null when done>}
  • Lists the latest name for each file. Use for quick listings; NOT for size/cost.

b2_list_file_versions

  • Body: {"bucketId": "<id>", "maxFileCount": 10000, "prefix": <opt>, "startFileName": <opt>, "startFileId": <opt>}
  • Returns: {"files": [{fileName, fileId, contentLength, action, ...}], "nextFileName": <null when done>, "nextFileId": ...}
  • Pagination: carry BOTH nextFileName AND nextFileId into the next request; stop when nextFileName is null.

action types

action meaning billed?
upload a real stored object YES
hide a hide marker (contentLength 0) no
start an unfinished large file no
folder a virtual folder placeholder no

For size/cost, sum contentLength over ALL versions where action == "upload". B2 bills every stored version, not just the latest, so size MUST use b2_list_file_versions, never b2_list_file_names.

b2_create_bucket (destructive — gated)

  • Body: {"accountId": "<acct>", "bucketName": "<name>", "bucketType": "allPrivate"}
  • bucketType defaults to allPrivate; the CLI allows allPublic via --type.

b2_create_key (destructive — gated)

  • Body: {"accountId": "<acct>", "keyName": "<name>", "capabilities": [...], "bucketId": <opt>, "namePrefix": <opt>, "validDurationInSeconds": <opt>}
  • Returns: {applicationKeyId, applicationKey, keyName, capabilities, bucketId, namePrefix}.
  • applicationKey is the SECRET and is shown ONCE on creation — it cannot be retrieved later. The CLI prints it with a prominent warning to store it in the vault immediately.
  • bucketId scopes the key to one bucket (the CLI resolves a bucket name to its id via b2_list_buckets).

b2_delete_bucket (destructive — gated)

  • Body: {"accountId": "<acct>", "bucketId": "<id>"}
  • B2 refuses to delete a non-empty bucket; that error is surfaced verbatim.

b2_delete_key (destructive — gated)

  • Body: {"applicationKeyId": "<id>"}

b2_update_bucket (destructive — gated)

Used for the prefix-purge feature: it writes the bucket's lifecycle rules.

  • Body: {"accountId": "<acct>", "bucketId": "<id>", "lifecycleRules": [...]}
  • Returns: the updated bucket object (same shape as a b2_list_buckets entry, including the NEW revision and the written lifecycleRules).
  • lifecycleRules REPLACES the entire array — it is NOT additive (caveat). To add/remove a single rule you MUST first read the current rules via b2_list_buckets (scoped to the bucketId), merge your change into the full array, then write the complete set back. Writing just the one rule you want silently drops every other rule.
  • No ifRevisionMatch on this account/endpoint. This B2 account's b2_update_bucket REJECTS ifRevisionMatch with HTTP 400 bad_request ("unknown field ... ifRevisionMatch"), so no optimistic-lock token is sent. Writes are therefore last-write-wins — the read-merge-write of the full rules array on every change is what keeps pre-existing rules intact. The skill does not implement a 409-conflict retry (there is no revision guard); it makes at most one defensive retry on a transient 5xx, never loops.
  • B2 allows up to 100 lifecycle rules per bucket.

Lifecycle rule shape

{ "fileNamePrefix": "MBS-<guid>/CBB_<machine>/",
  "daysFromUploadingToHiding": 1,
  "daysFromHidingToDeleting": 1 }
  • fileNamePrefix — files whose name starts with this string are governed by the rule. "" means the WHOLE BUCKET.
  • daysFromUploadingToHiding — after this many days, B2's daily lifecycle pass HIDES the file (creates a hide marker).
  • daysFromHidingToDeleting — after this many days hidden, B2 DELETES the file version permanently.
  • With both = 1, every version older than ~1 day is hidden then deleted on the next daily passes -> full server-side purge of the prefix within ~24-48h. This is the mechanism the skill uses to purge prefixes with 1.2M+ versions, where per-file b2_delete_file_version would be impractical. Irreversible — no recycle bin.

b2_get_file_info / b2_delete_file_version

  • b2_get_file_info body: {"fileId": "<id>"} (read).
  • b2_delete_file_version body: {"fileName": "<name>", "fileId": "<id>"} (destructive; exposed only via raw --confirm or the client helper).

Cost formula

GB   = bytes / 1_000_000_000          # decimal GB (billing unit, NOT 2^30)
cost = GB * RATE_PER_GB_USD           # RATE_PER_GB_USD = 0.00695 (ACG cost basis)

RATE_PER_GB_USD is ACG's cost basis, recorded in .claude/memory/reference_backblaze_storage_rate.md, and used by the GuruRMM mspbackups storage-cost calc. Override with --rate. The usage report sums upload-version bytes per bucket, converts to decimal GB, multiplies by the rate, sorts by cost desc, and prints a TOTAL row plus grand totals. For ACG-* buckets it derives a client label by stripping the ACG- prefix.


Account structure (live)

  • accountId 46f69bc61163, region us-west-001.
  • 12 buckets (all allPrivate): ACG-BST, ACG-Brett, ACG-Dataforth, ACG-GLAZTECH, ACG-IX, ACG-Internal, ACG-Lens, ACG-PST, ACG-REDNOUR, Horseshoe, MSPBackups20200311, VWP-Backup. Most hold MSP360/CloudBerry data (MBS-<guid>/CBB_<CLIENT>/...).
  • 2 application keys: cloudberrykey (00146f69bc611630000000003, the MSP360/CloudBerry key) and ClaudeTools (00146f69bc611630000000009, used by this skill).