# 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) ```json { "accountId": "46f69bc61163", "authorizationToken": "", "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 /b2api/v3/` - **Header:** `Authorization: ` (raw token, not Basic). - **Body:** a JSON object. - **Content-Type:** `application/json`. ### Error shape B2 returns HTTP 4xx with a JSON body: ```json { "status": 400, "code": "", "message": "" } ``` 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 `/b2api/v3/`. ### b2_list_buckets - **Body:** `{"accountId": "", "bucketId": }` - **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": "", "maxKeyCount": 1000, "startApplicationKeyId": }` - **Returns:** `{"keys": [{keyName, applicationKeyId, capabilities:[...], bucketId, namePrefix, expirationTimestamp}], "nextApplicationKeyId": }` - Paginate on `nextApplicationKeyId`. ### b2_list_file_names - **Body:** `{"bucketId": "", "maxFileCount": 10000, "prefix": , "startFileName": }` - **Returns:** `{"files": [{fileName, contentLength, action, uploadTimestamp, ...}], "nextFileName": }` - Lists the **latest** name for each file. Use for quick listings; NOT for size/cost. ### b2_list_file_versions - **Body:** `{"bucketId": "", "maxFileCount": 10000, "prefix": , "startFileName": , "startFileId": }` - **Returns:** `{"files": [{fileName, fileId, contentLength, action, ...}], "nextFileName": , "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": "", "bucketName": "", "bucketType": "allPrivate"}` - `bucketType` defaults to `allPrivate`; the CLI allows `allPublic` via `--type`. ### b2_create_key (destructive — gated) - **Body:** `{"accountId": "", "keyName": "", "capabilities": [...], "bucketId": , "namePrefix": , "validDurationInSeconds": }` - **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": "", "bucketId": ""}` - B2 **refuses to delete a non-empty bucket**; that error is surfaced verbatim. ### b2_delete_key (destructive — gated) - **Body:** `{"applicationKeyId": ""}` ### b2_update_bucket (destructive — gated) Used for the prefix-purge feature: it writes the bucket's lifecycle rules. - **Body:** `{"accountId": "", "bucketId": "", "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 ```json { "fileNamePrefix": "MBS-/CBB_/", "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": ""}` (read). - `b2_delete_file_version` body: `{"fileName": "", "fileId": ""}` (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-/CBB_/...`). - 2 application keys: `cloudberrykey` (`00146f69bc611630000000003`, the MSP360/CloudBerry key) and `ClaudeTools` (`00146f69bc611630000000009`, used by this skill).