Object storage: official SDK or raw REST, and what changes with AI

What an S3 SDK really does for you, what you write yourself without one, and why an agent generating the code usually does better against plain HTTP.

Split the question in two, because the two halves have different answers. Against an S3-protocol endpoint — Amazon S3, Cloudflare R2, MinIO — the official SDK earns its keep, mostly because request signing is fiddly and nobody should reimplement SigV4. 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 and more interesting. When an assistant is writing the integration, the failure modes invert: models produce correct fetch and requests calls at a high rate, and hallucinate SDK method names, mix major versions and invent options that were removed two releases ago. Plain HTTP has no version drift to be wrong about.

What the SDK is really doing

Strip the marketing and an object-storage SDK does six jobs: signs requests, retries the retryable ones with backoff, walks pagination tokens, orchestrates multipart uploads, streams bodies without buffering them, and turns error XML into typed exceptions. Any of those you still need without an SDK, you write.

On a Bearer-token API, job one disappears entirely:

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": 38,
    "etag": "4483bedcc4d33d51bea1b443b2ec0387",
    "content_type": "application/json",
    "metadata": { "report-kind": "summary" },
    "last_modified": "2026-07-26T00:48:58Z"
  }
}

No credential chain, no region resolution, no client construction. That’s the whole 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}`);

Note what the helper leans on: a uniform envelope. Every response carries ok, data and, when things go wrong, error.code — so one branch handles every route on the API, which is the thing a typed SDK usually sells you. 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"])

When the code is written by an agent

Here’s where the calculus genuinely shifts. An SDK is a moving target — method names change between majors, and a model trained across several of them will confidently blend v2 and v3 idioms. An HTTP contract published as data doesn’t move under the model’s feet, and it can be fetched at generation time:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | python3 -c "import json,sys; d=json.load(sys.stdin); [print(c['method'], c['path'], 'billable' if c['billing']['is_billable'] else 'free') for c in d['capabilities'] if c['module']=='storage']"

That prints every storage route with its verb and billing class, and the same document carries flows — named sequences with the request body fields for each step, so an agent can look up “browser direct upload” rather than guess it. Point your assistant at that URL before it writes a line and the hallucination surface shrinks to almost nothing.

Worth flagging one gap we hit while testing this: the flow summary for PUT /v1/storage/object/put/{bucket}/{key} lists content_type and metadata, but the request also carries the bytes themselves as base64 in data_base64, which the summary doesn’t spell out. Read the API reference for the payload, use the manifest for the route table.

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" },
  };
}

Metadata keys with an underscore break the upstream signature, so hyphenate them — report-kind, not report_kind. That’s the kind of rule an agent will not infer and a review has to catch.

Side by side

ConcernOfficial S3 SDKRaw REST
Request signingHandledNot needed with a Bearer token; unavoidable on S3
Retries and backoffBuilt in~20 lines you own
Multipart of a 5 GB fileManaged uploaderYou sequence create → parts → complete
Streaming a large downloadNative streamsDepends on the API’s shape
Dependency weightA package tree per languageNone
Correct on first try from an LLMVersion-sensitiveUsually, if the contract is fetchable
Ecosystem tools (rclone, mc, s3fs)YesNo

Where the SDK still wins, plainly

If you’re moving multi-gigabyte objects, want rclone to sync a directory, or need a filesystem mount, use an S3-protocol store with its SDK — that’s a real limitation of any token-native API, Infrai included, and 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 when 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 the multi-service angle matters more than it sounds: one Authorization header shape covers storage, queues, cron and email here, so a single helper like the one above is your entire client library for the platform, not just for files.

What it costs to try

Metadata reads, listings and presigns are free and rate-limited; object writes and reads are billable per call — verified 26 July 2026 at $0.0001 per write and $0.0002 per read, with $2 of trial credit on a new account, which is thousands of calls before anything appears on a bill. Rates trend down over time, so read the current ones from the same manifest your code generator uses:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | python3 -c "import json,sys; [print(c['id'], c['billing'].get('price_usd', 0)) for c in json.load(sys.stdin)['capabilities'] if c['id'].startswith('storage.object')]"

References

Browse more storage developer guides