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

237 lines
9.2 KiB
Markdown

# 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": "<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:
```json
{ "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
```json
{ "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).