API & SDK Changelog

Integrator-facing changes to the ZiB storage, encoding, compute, and webhook APIs. Point your integration team here. Items tagged Action required need a change on your side.

SDK: https://app.zibnetwork.com/sdk/zib-sdk.js — the hosted URL is unversioned, so a <script src> include always serves the latest.

2026-08-29

SDK v1.8.0

Signed reads. Nothing here changes behaviour today — it is the prerequisite for object-token enforcement, which cannot be switched on until this has been adopted.

New

v1.8.0 — fetchObject / fetchObjectStream accept a signed URL

Both now take a signed object URL — or a bare file_id plus an explicit exp/tok — wherever they took a bare file_id, and carry the signature through to /objects/{id}/serve-coords. Mint one server-side with GET /v1/api/object-url (Bearer, and you can only mint for objects you own). The token is bound to the file_id rather than to a path, so one URL covers both the object and its coordinates — there is no second token type to manage.

Not breaking. A bare file_id works exactly as before. Adopt at your own pace; the breaking moment is the enforcement flip, which is separate and which we will announce.

Fixed

The SDK never sent a token, so every SDK read was an unsigned read

Until now fetchObject called serve-coords with no token at all — it built that URL from the id alone and discarded any signature it had been given. Two consequences, both ours: object-token enforcement could never have been switched on without breaking the SDK itself, and the counter we were using to judge readiness only ever saw the byte path, so it was reporting a fraction of the real unsigned traffic as though it were the whole.

serve-coords is now gated on the same token as the byte path, lenient and counted exactly as that one was during its rollout. A presented token must be valid either way, so you can rehearse against it for real rather than getting a 200 that proves nothing.

Clarification

What a signed URL is, and is not

It bounds a leaked file_id from permanent to roughly twelve hours, and only the owning account can mint one. That is a real improvement and it is why we are doing it.

It is not access control. A signed URL reaches the browser, so anyone who can view the page can read the token out of it and use it until it expires — and exp is hour-quantised, so viewers share one within the hour. It is the signed-CDN-URL model, not a per-viewer grant. There is still no per-user access control on object reads, and nothing that must stay private from someone who might obtain the id should be stored in ZiB.

2026-08-28

Bucket-name resolution

Reported by an integrator, and the report was right about more than it knew. Bucket names are unique per tenant, not globally — and most endpoints that took a bucket name were resolving it without reference to who was asking.

Fixed

Every S3-path endpoint now resolves your own bucket

On 2026-08-24 we fixed this for GET /s3/{bucket}/{key} and said, on these pages, that an authenticated request resolves to your bucket of that name. That was true of object reads and of nothing else. PUT, DELETE, HEAD, bucket listing and all three multipart endpoints still resolved by name alone, so if an older tenant owned a bucket of the same name — and the four we create for you (video, posters, images, logos) collide by construction — you were handed their bucket and then refused ownership of it.

What this looked like to you: a 403 on your own bucket, for every S3-path operation on a colliding name, with correct credentials. Uploads through POST /v1/api/upload/initiate and the SDK were never affected — that path has been scoped since June, which is why this survived so long: the route most integrations use was already right.

Also fixed: bucket listing and HEAD could return 200 against another tenant's bucket of the same name when that bucket was public — object keys, sizes and content types, never object bytes. And POST /v1/api/encoding/request in its { bucket, key } form could register the object against the wrong tenant.

Action required

A bucket you do not own is now 404, not 403

These endpoints previously answered 403 Forbidden when the name resolved to somebody else's bucket. They now answer 404 NoSuchBucket, matching /v1/api/upload/initiate and S3 itself: you own no bucket of that name, and whether another tenant does is not something the response should tell you. If you branch on 403 to mean “bucket exists but is not mine”, that branch is now unreachable — treat 404 as “create it or check the name”.

Clarification

Buckets are private and cannot be made public — and that is not what protects an object

Stated here because two pages implied otherwise. The Create Bucket note told you to “use Update Bucket Policy to make a bucket publicly readable”, pointing at an endpoint the very next entry correctly described as never having existed; that entry then offered to flip a bucket on request. Neither is true. No API sets buckets.public_read, every bucket on the platform is private, and we do not make them public.

Do not read that as a privacy guarantee, because it is not one. Object reads have never consulted the bucket flag — a private bucket does not make its objects private, and a public one would not have made them public. Objects are encrypted at rest and nodes hold only ciphertext, which stops a node operator reading your files; it does not stop anyone else, because GET /objects/{file_id}/serve-coords hands the decryption key to an unauthenticated caller by design, so that a browser can decrypt locally. The file_id is the read capability, exactly as the security callout in the docs has said. Nothing about buckets or encryption changes that, and we would rather say so plainly than let the two facts read as one.

Clarification

Unauthenticated S3-path reads are still ambiguous — by design

An anonymous GET s3.zibnetwork.com/{bucket}/{key} carries nothing to disambiguate a shared name, so it resolves to the oldest bucket with that name and always will. It is now deterministic rather than incidental, and we count the collisions. This path is not a public URL for your assets: resolve an object URL from the file_id instead (see Object URLs).

We are not making bucket names globally unique. Doing so would mean the first tenant to create images owns that word for everyone, and would break the per-creator buckets the platform provisions for you. Removing the ambiguity properly means putting the tenant in the address — a per-tenant hostname, as S3 does with virtual-hosted-style URLs — which is a separate change.

2026-08-25

Docs correctness pass

A month of platform changes, and a set of corrections to this documentation. Several claims on these pages did not match the code; they are listed here rather than quietly edited, because integrations were built against them.

Action required

Two webhook events do not exist — encoding.progress and file.replicated

Neither is emitted by anything, and neither ever has been. encoding.progress had a whole “Tracking encoding progress” section, a handler example and a documented 10s/5%-delta throttle policy; file.replicated was described as the signal backups could rely on. If you gate on either, you wait forever with no error to show for it. Poll GET /v1/api/pipeline/{file_id} for a 0–100 percentage, and use encoding.complete plus phase safe for durability. The complete set of encoding events is: encoding.queued, encoding.started, encoding.sharding, encoding.complete, encoding.failed.

Action required

Two documented endpoints do not exist

POST /v1/api/publisher/grants and PATCH /v1/api/buckets/{bucket_id}/policy both return 404. No route, no handler. Avail windows are recorded by us on request; bucket policy does not affect object reads at all.

Action required

Upload registrations now expire after 7 days

/v1/api/upload/{upload_id}/part/{n} and /complete now return 410 Gone past the expiry. The column had existed since the table was created and was read by nothing, which made a leaked upload_id usable indefinitely. Call initiate again to get a fresh registration.

Fixed

expires_in_seconds is now the real remaining life

It returned a constant 43200 while the signed expiry is quantised to the wall-clock hour, so for most of any hour it overstated the remaining life by up to 59 minutes. If you cached a resolved URL for exactly the reported window, it could expire mid-use. Cache to the top of the next hour instead.

Fixed

Replication is per file, not per chunk

These pages said “each chunk is replicated to at least three independent nodes”, which reads as chunks being scattered across different nodes. A node holds every chunk of a file or it is not a holder of that file. Chunk addressing is what lets a reader fetch from several holders at once and lets us serve a byte range — not a distribution scheme.

Action required

Store the file_id, not the object URL — and treat the file_id as a secret

Quick Start previously told you to store cdn_url. That was wrong and is why signed-URL enforcement was rolled back on 2026-08-19. Store the file_id and resolve at render via GET /v1/api/object-url. Separately: anyone who learns a file_id can read that object, and URL signing narrows that surface without closing it, so keep ids out of public listings.

Clarification

CDN purges were silently rejected 19–25 Aug — deletes and re-encodes could serve stale bytes

Between 2026-08-19 and 2026-08-25 14:59 UTC, every object cache purge we sent to Cloudflare was rejected. One of the two purge prefixes was built from a full signed URL — scheme and ?exp=&tok= included — and a prefix field accepts neither, so CF answered 1119 “URI schemes must not be provided”. It broke the day signed object URLs shipped and stayed broken for a week, because CF reports that failure in a 200 OK body rather than a status code.

What this could have looked like to you: if you deleted or re-encoded an object in that window — same file_id, changed or removed bytes — the old bytes could keep being served from a Cloudflare edge until the 24-hour object cache lifetime expired. A poster or sprite sheet that looked stale for a day last week is the likely symptom. Nothing was lost and no action is needed now; anything cached then has long since expired.

Not affected: re-uploading to the same (bucket, object_key). That mints a new file_id, whose URL was never in the cache to begin with — which is also why this stayed invisible in the flow most integrations exercise most often.

Fixed and verified in production: three stuck purges completed cleanly, and there have been no purge failures or 1119 responses since the deploy.

Fixed

S3-path reads resolve to your own bucket

Bucket names are not unique across tenants. An authenticated read of s3.zibnetwork.com/{bucket}/{key} now resolves to your bucket of that name; previously it could resolve to another tenant's and refuse you your own file. An unauthenticated read still cannot be disambiguated — do not use that path as a public asset URL.

2026-07-24

SDK v1.7.2

Faster retrieval and playback — multi-source reads and a new progressive streaming API.

New

New

v1.7.x — Faster delivery & playback

fetchObject is now multi-source (parallel chunks across holders); new fetchObjectStream(fileId) returns a progressive ReadableStream (windowed read-ahead, fast first frame) for streamed downloads and MediaSource playback.

New

v1.6.0 — Multi-source retrieval

fetchObject pulls a file's 5 MiB chunks in parallel from multiple holders and reassembles client-side (was single-holder).

2026-07-23

SDK v1.5.1 · backend + node

Storage-model rebuild follow-through: HLS-only playback, delete-cascade, content-type correctness, and a set of webhook fixes — plus explicit answers to the questions PopAds and rite3 raised.

Action required

Action required

The object catalogue was reset — re-upload your content

Production storage was rebuilt for the chunked model. Objects uploaded before the rebuild are gone: their file_ids now 404 on /objects and /stream/*, and they will not come back. Re-upload your assets. New uploads are chunked (5 MiB per-chunk-gcm-v1) and ≥3-replicated.

Action required

DASH removed — go HLS-only

ZiB encodes HLS-TS (mpegts) only; the fMP4/CMAF ladder was retired. /stream/<file_id>/manifest.mpd 404s, and the encoder no longer advertises a dash_manifest_url (it was previously emitting one that didn't resolve). Drop DASH from your player config and serve /stream/<file_id>/master.m3u8 only. Bitmovin/hls.js/native-HLS/video.js all play HLS.

Action required

Poll /pipeline on the top-level status, not stages.encoding.phase

GET /v1/api/pipeline/{file_id} exposes a top-level status (queued | uploading | encoding | ai_processing | complete | failed) and a phase only on stages.upload. There is no stages.encoding.phase, and status never returns safe. Branch on status === 'complete' / 'failed'; detect a supersede via stages.upload.phase === 'cancelled'. (The phase/safe vocabulary belongs to GET /v1/api/my-jobs.)

Action required

Use /compute-api/{file_id} for key:secret auth

POST /v1/api/compute/{file_id} is wallet-JWT only. Tenants using Bearer access_key:secret_key must call POST /v1/api/compute-api/{file_id} and poll GET /v1/api/compute-api/job/{job_id}. Same request/response shape.

Action required

file.ready / file.deleted go to the account-level webhook only

A per-request webhook_url (on /encoding/request or nested in /upload/initiate's pipeline) receives only the job-lifecycle events (encoding.*, transcription.complete, vision.complete). file.ready, file.deleted, and upload.reassigned are delivered to your account-level webhook (customers.file_ready_webhook_url / default_webhook_url). Set one if you rely on those events.

Action required

Transcription tier names

Accepted values are fast | balanced | accurate (aliases fastestfast, recommendedbalanced). The tenant path does not validate the string — an unrecognised tier is coerced to the node's default Whisper model rather than rejected, so use the exact names.

Fixed

Fixed

DELETE now cascades to derived artifacts

DELETE /v1/api/file/{id} now also removes the derived artifacts that each carry their own file_id — poster, sprite sheets, sprite-VTT, SRT/VTT, sidecar, and the 9:16 rendition (+ its poster). Previously these orphaned and leaked storage. The delete response now includes a deleted.derived_artifacts count.

Fixed

content_type is stored and served correctly

Objects are now served with their real Content-Type instead of always application/octet-stream. The SDK (v1.5.1) auto-detects the plaintext MIME (File.type, else a magic-byte sniff) and sends it on /complete; your server can also pass content_type to /upload/initiate. The backend can't sniff at ingest — uploads are encrypted client-side — so it stores what the client sends and only falls back to serve-time sniffing when neither is present.

Fixed

Webhook secret rotation no longer causes a 401-storm

Rotating your webhook secret (POST /v1/api/customers/{id}/webhook/secret) now keeps the old secret valid for a 24h grace window: every webhook is dual-signed with the new secret in X-ZiB-Signature and the old one in X-ZiB-Signature-Previous, so you can deploy the new secret with zero downtime — validate against either during the window.

Fixed

Webhook signature consistency + dead events revived

upload.reassigned now signs with the standard sha256=<hex> prefix (it previously sent bare hex). file.failed now actually delivers (to the account-level webhook) — it previously reached nobody. Correction (2026-08-25): this entry also claimed file.replicated was fixed. It was not. The delivery path was wired up for both events, but nothing ever calls it with file.replicated, so that event has never been sent and still is not. Half the fix landed and the note read as though all of it had.

Clarifications

Clarification

HLS keys, CORS, DRM

/stream/<file_id>/key now requires the signed ?exp=&tok= the variant playlist embeds — a bare request returns 403. Players are unaffected: they follow the URI in the manifest, which the backend signs. All stream routes serve Access-Control-Allow-Origin: *. There is no DRM — file_id is the only capability token. Current encodes are AES-128 encrypted: the variant playlist carries an #EXT-X-KEY tag and the player fetches the 16-byte key once from /key, then decrypts segments locally.

Clarification

Manifest is live immediately; segments may 503 briefly

master.m3u8 is servable the instant encoding.complete fires — there is no replication-wait gate. Individual segments serve from the encoder node immediately; if that node is briefly offline before replicas verify, serve_segment returns a (non-cacheable) 503, not a 404 — treat it as retry.

Clarification

AI tiers hard-fail at initiate when no node is available

Requesting transcription/vision when no node currently advertises that capability returns 503 at /upload/initiate (encode is a soft preference and won't 503). There is no defer/queue yet — retry, or under-declare AI. A capability-preflight is on the roadmap.

Clarification

≥3 replication applies to new uploads only

The ≥3-replica durability guarantee is applied to uploads made under the new model (and encode-completed masters), not retroactively to pre-rebuild objects. There is currently no per-object replica-health read for a plain object.

Clarification

Thumbnails + 9:16 are available on /encoding/request

Pass thumbnails (poster + sprite sheet, default on) and aspect_variants: ["9:16"] to POST /v1/api/encoding/request. Both arrive on the same encoding.complete payload (thumbnails + aspect_variants keys) and on /pipeline.

Clarification

Telemetry: video.js/VHS is auto-metered

video.js (VHS) fetches segments on the main thread, so the SDK's PerformanceObserver auto-captures them — no manual recordSegment needed for the observed-bytes channel (needs Timing-Allow-Origin on cross-origin fetches). The worker-fetch gap (bitmovin, hls.js with enableWorker, shaka, dash.js) is the only case that still needs recordFetch/recordSegment.

Questions? These changes are documented in full under Developer Docs.