Object storage: official SDK or raw REST, and what changes with AI
What a storage SDK really does for you, what you write yourself without one, and why an agent generating the code usually does better against plain HTTP it can fetch.
Split the question in two, because the halves have different answers. Against an S3-protocol endpoint the official SDK earns its keep, mostly because request signing is fiddly and nobody should reimplement SigV4 by hand. Against a token-authenticated HTTP API like Infrai’s storage surface there is no SDK to install: every call is a URL, a Bearer header and JSON, so “SDK or REST” collapses into “what would the SDK have done for me, and who does it now”.
The second half of the question is newer. When an assistant writes the integration, the failure modes invert — models produce correct fetch and requests calls at a high rate, and hallucinate SDK method names, blend major versions and invent options that were removed two releases ago. A plain HTTP contract has no version drift to be wrong about, and it can be fetched at generation time.
What the SDK is really doing
Strip the marketing and a storage SDK does six jobs: signs requests, retries the retryable ones with backoff, walks pagination cursors, orchestrates multipart uploads, streams bodies without buffering them, and turns error XML into typed exceptions. Every one you still need without an SDK, you write.
On a Bearer-token API, job one disappears:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/storage/object/head/kb-restapi-0726/reports/2026-07/summary.json" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"found": true,
"status": "found",
"key": "reports/2026-07/summary.json",
"size_bytes": 24,
"etag": "d96ac24b5cd1fe15b6ad0a33d6aa12c0",
"content_type": "application/json",
"metadata": { "report-kind": "summary", "tenant-id": "t_9001" },
"last_modified": "2026-07-27T12:09:38Z"
}
}
No credential chain, no region resolution, no client construction. That’s the appeal of the token model — and also the reason there’s nothing to install.
The jobs you inherit
Retries are the one people skip and regret. Twenty lines, once, in a helper every other call goes through:
import process from "node:process";
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function call(path, { method = "GET", body } = {}, attempt = 1) {
const res = await fetch(`${API}${path}`, {
method,
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
if (res.status === 429 || res.status >= 500) {
if (attempt >= 4) throw new Error(`${method} ${path} gave up after ${attempt} attempts (HTTP ${res.status})`);
await sleep(2 ** attempt * 250);
return call(path, { method, body }, attempt + 1);
}
const payload = await res.json();
if (!res.ok || payload.ok === false) {
const err = payload.error ?? {};
throw new Error(`${method} ${path} failed: ${err.code ?? res.status} ${err.message ?? ""}`);
}
return payload.data;
}
const head = await call("/v1/storage/object/head/kb-restapi-0726/reports/2026-07/summary.json", { method: "GET" });
console.log(`${head.size_bytes} bytes, etag ${head.etag}, type ${head.content_type}`);
Notice what the helper leans on: a uniform envelope. Every response carries ok, data and, when things go wrong, error.code with a retryable flag — so one branch handles every route on the API, which is most of what a typed client sells you. The retry rule is the one worth copying verbatim: retry 429 and 5xx, never a 4xx, because a 4xx here means the request was wrong and it will be just as wrong in 500 milliseconds.
Python is the same shape in fewer lines:
#!/usr/bin/env python3
import os
import sys
import requests
API = "https://api.infrai.cc"
KEY = os.environ.get("INFRAI_API_KEY")
if not KEY:
sys.exit("INFRAI_API_KEY is not set")
url = f"{API}/v1/storage/object/list/kb-restapi-0726"
res = requests.get(url, headers={"Authorization": f"Bearer {KEY}"}, params={"prefix": "reports/", "limit": 100}, timeout=20)
res.raise_for_status()
payload = res.json()
if not payload.get("ok"):
sys.exit(f"list failed: {payload['error']['code']}")
for item in payload["data"]["items"]:
print(item["key"], item["size_bytes"])
Pagination is the other inherited job: read data.next_cursor, pass it back as cursor, stop when it’s null. That’s the loop the SDK’s paginator was hiding.
When the code is written by an agent
Here’s where the calculus genuinely shifts. An SDK is a moving target. A published contract isn’t, and you can hand it to the model at the moment it writes:
curl -sS "https://api.infrai.cc/v1/discovery?namespace=storage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
That returns every storage route with its verb, path and billing block, plus flows — named sequences with the request body fields for each step, so an agent can look up a documented upload flow instead of guessing one. Per-capability detail is a segment deeper, and it’s where the request schema actually lives: GET /v1/discovery filtered to one capability gives you the required fields, the enums and the aliases. For PUT /v1/storage/object/put/{bucket}/{key} that’s data_base64 required, with data and file_base64 as accepted aliases, plus optional content_type, metadata, cache_control and storage_class.
Point your assistant at that before it writes a line and the hallucination surface shrinks to nearly nothing — the enums are inlined, so it doesn’t have to invent the value of op or fit either.
type PutBody = {
data_base64: string;
content_type: string;
metadata?: Record<string, string>;
};
export function putBody(bytes: Uint8Array, contentType: string): PutBody {
return {
data_base64: Buffer.from(bytes).toString("base64"),
content_type: contentType,
metadata: { report_kind: "summary", tenant_id: "t_9001" },
};
}
One behaviour to know rather than discover: metadata keys are normalised to the hyphenated header form, so report_kind goes in and report-kind comes back on head and list. Write whichever you prefer, read the hyphenated one.
Side by side
| Concern | Official S3-protocol SDK | Raw REST on a token API |
|---|---|---|
| Request signing | Handled for you | Not needed — one header |
| Retries and backoff | Built in | ~20 lines you own |
| Multipart of a 5 GB file | Managed uploader | You sequence create → parts → complete |
| Streaming a large download | Native streams | Presign and let the client stream it |
| Dependency weight | A package tree per language | None |
| Correct on first try from an LLM | Version-sensitive | Usually, if the contract is fetchable |
Ecosystem tools (rclone, mc, s3fs) | Yes | No |
Where the SDK still wins, plainly
If you’re moving multi-gigabyte objects daily, want rclone to sync a directory, or need a filesystem mount, use a protocol-native store with its SDK. That’s a real limitation of any token-native API, this one included — no amount of fetch gets you s3fs. Supabase’s storage client is a good middle example: a thin SDK over REST that you can drop and call directly the day it gets in the way.
For everything else — put an object, list a prefix, mint a link, expire old files — REST is less code, fewer upgrades and easier to review. And there’s a portability argument that outlasts both: nothing above is a proprietary call shape. It’s HTTP, JSON and a Bearer header, with presigned URLs in the standard query-signature form, so there’s no lock-in at the call site and migrating means changing a base URL and a header, not rewriting an integration. The same helper covers storage, queues, cron and email on one credential, so it isn’t a file client — it’s your whole client library for the platform, and the next capability you need is already reachable through it without another account.
What it costs to try
Metadata reads, listings, presigns and every bucket-administration route are free and rate-limited. Two meters move: PUT /v1/storage/object/put/{bucket}/{key} bills $0.0001 per call, and GET /v1/storage/object/get/{bucket}/{key} bills by egress volume at $0.104 per GB rather than per request — both read on 27 July 2026. That distinction matters when you’re choosing between an SDK’s streaming download and a presigned URL: the meter counts bytes either way, so hand the client a signed URL and let it pull the object straight from storage.
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
That returns real spend per capability, which beats any figure printed on a page. Rates trend down over time and discount campaigns run, so read them from the same manifest your code generator is already using. A call against a bucket that doesn’t exist answers STORAGE_BUCKET_NOT_FOUND with retryable: false — worth wiring into the helper above so a typo fails fast instead of burning four attempts.