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. Every release is also published at a pinned URL that never changes, e.g. https://app.zibnetwork.com/sdk/zib-sdk-1.9.6.js. Pin it in production.

2026-10-31

SDK v1.9.6 · publisher catalogue · uploads · credentials · billing
Fixed

v1.9.6 — installVhsMultiSource plays on video.js 8

installVhsMultiSource did not play on video.js 8 in SDK 1.9.1–1.9.5, or in any earlier release (they return the same request object). It did not fill in the response the way video.js 8's VHS reads it, so every key and segment it answered failed and playback never started. If you use video.js 8 with this helper, move to 1.9.6: https://app.zibnetwork.com/sdk/zib-sdk-1.9.6.js. No API change.

New

v1.9.6 — opt-in quality menu for video.js 8

ZiBStorage.videojsHtml5Options(), ZiBStorage.installQualityMenu() and ZiBStorage.shouldRetryNativeHls() add an Auto / 1080p / 720p / … menu. Desktop Safari shows it only if the player is created with videojsHtml5Options(…); a player on Safari's native HLS gets no menu. iPhone and iPad never show it, by design. With these options desktop Safari offers no AirPlay for the video; iPhone and iPad keep it. Nothing changes unless you call them. See Video Encoding → Quality menu for video.js. The SDK compute methods still call the dashboard-only route in 1.9.6 (see Standalone compute).

New

Catalogue: each title carries its trailer

  • Every title in GET /v1/api/publisher/catalog has a new top-level trailer: null, or { file_id, ready, hls_url, poster_url }. Nothing else in the entry changes. It is outside playback because a coming_soon title can have a trailer.
  • null when the title has no trailer or its upload never finished. ready: false means it is not playable yet: show nothing or “coming soon” and re-read later.
  • Play it by minting a session with file_id: trailer.file_id; title_id always mints the main video. A replaced trailer switches to the new file_id once the new encode is ready, so read it from the catalogue each time. poster_url is the title's image.
Fixed

Catalogue ?availability= is applied before ?limit=

When it first shipped, ?availability= filtered only the titles that ?limit= had already returned, so a filtered page could come back short or empty. It now filters first: ?availability=coming_soon&limit=20 returns up to 20 coming_soon titles. No change needed if you raised your limit as a workaround.
New

Catalogue: coming_soon titles, creator_id, signed creator logos and series art

  • Published titles whose main video is not ready are now listed, with availability: "coming_soon", file_id: null, playback: null and their usual signed art. Titles with a main video are "live". New filter: ?availability=coming_soon|live.
  • Show coming_soon titles in every normal row (New, Trending, Most Votes, search) with their poster. The player shows “Video coming soon” and must not mint a playback session for them.
  • Each catalogue entry gains "type": "title" and creator_id. Key creator logos on creator_id, not the slug. creator_logo_url is now signed.
  • /publisher/series now signs the series and season posters and the episode thumbnails, and gains creator_id. The fallback handling for unsigned series art is no longer needed.
Action required

Render art live from the catalogue; never store art URLs

A stored cdn.zibnetwork.com/objects/… or cdn.mosnet.io/objects/… URL expires or stops loading, and the result is a blank poster. Store title and creator ids, and render art from your latest catalogue or series read (cached to the top of the hour).

Fixed

playback.poster_url is the title's image

playback.poster_url in the catalogue is now the title's own signed image (the horizontal thumbnail, else the vertical one, else null). It used to point at the title's video file.

Action required

A completed upload session is closed

Once /v1/api/upload/{upload_id}/complete succeeds, the session is closed and its id cannot reopen it. A later part or complete call for it returns 409 {"code":"upload_closed", "file_id": …}: the session has already completed or can no longer accept parts. Do not retry. Treat the file as uploaded only when you receive its file.ready webhook. A 409 alone does not prove the upload succeeded. To upload again, initiate a new registration. (An earlier version of this entry said to confirm with GET /v1/api/object-url; that was wrong, because it answers for a registered file whether or not the upload finished.)

Action required

READ scoped credentials are GET/HEAD only

A scoped credential from /v1/api/credentials/create with READ permission is now refused (403) for any write: PUT, POST, DELETE, upload initiate, PIN upload and file delete. Only a credential with WRITE can write; permissions is required, and anything other than WRITE is read-only. Scoped credentials are now documented under Server-side Integration.

Clarification

Egress: one viewer, one item, billed once per 10 minutes

Repeated requests from one viewer for the same segment or object within 10 minutes are billed once. ZiB may also apply an hourly cap on billed egress per tenant; delivery past it in that hour is not billed.

Clarification

Request logging

API requests are now logged with client IP, method, route template, status and latency. Ids in the path and the query string are not recorded. See Security Model.

2026-10-30

SDK v1.9.4 / v1.9.5 · publishers · docs corrections

A publisher integration audited the Publishers section against its own use. These corrections are listed here because integrations were built against the old text.

Action required

Publisher series and playback sessions are gated by carriage

  • GET /v1/api/publisher/series?id= returns 404 {"error":"series not found"} unless the series is published and you carry its creator. The body is the same as for an unknown id. Only published episodes are listed. Seasons are not filtered by status, so check playable: true on episodes and the season's status before showing its trailer.
  • POST /v1/api/publisher/playback-session with title_id returns 404 "title not found" unless the title is published and carried. With file_id it returns 404 "file not found" unless the file is the primary or trailer file of a published, carried title, the trailer of a published, carried series, or the trailer of a published season of one. Before, any file id could be minted.
  • These 404s are checked before the balance, so a 402 now means the content was allowed and your balance is zero or below. What to do on a 404 (not retryable; hide the title and re-read the catalogue) is under Publishers → Mint a playback session → Handling a 404.
Fixed

A folder named video is not auto-encoded

The platform default pipeline applies only to ZiB Studio creators' own video folders. An upload to a folder of yours named video with no pipeline declared no longer gets it. To encode on upload, declare pipeline.encode: true.

Clarification

v1.9.5 — SDK documentation only: no secret key in browser examples

The SDK's own quick-start comment now shows the server minting a registration with /v1/api/upload/initiate and the browser calling ZiBStorage.uploadDirect(file, registration). The constructor and waitForPinUpload are marked server-side only in the JSDoc and zib-sdk.d.ts. No behaviour changed. The compute methods still call the dashboard-only route (see “Standalone compute” below).

Action required

Never ship your secret key to a browser

Some examples in these docs and the playground showed new ZiBStorage({ accessKey, secretKey }) or SDK compute calls under a browser <script> header. Your secret key has full access to your account, so it must stay on your server. Your server mints short-lived, single-purpose values for the browser: an upload registration, a signed object URL, a playback session token. Every example that uses the secret is now marked server-side, and the playground shows a warning on its own credential fields.

Also corrected: the docs said S3 credentials and the webhook secret are shown only once. The dashboard can display both again today. Save them to your secret store when you create them anyway, and do not rely on reading them back. (Publisher API secrets are shown only once.)

New

v1.9.4 — uploadDirect returns signed_url + signed_url_expires_at

uploadDirect now also returns signed_url, the backend-signed object URL from the upload's complete answer, which the SDK used to discard. It also returns signed_url_expires_at (unix seconds, about 12 h out). Use it for immediate display, such as a preview right after upload. Never store it: past its expiry it returns 403, even while unsigned reads are still served, because a presented token is always checked. Both fields are null if the backend sent no signed URL.

Unchanged: cdn_url is still the bare URL the SDK builds. Persist file_id and resolve a fresh URL at render time with GET /v1/api/object-url. waitForPinUpload and the S3-path instance methods return no signed_url.

Fixed

v1.9.3 — the CDN fallback is no longer cut off at the holder TTFB budget

When every listed holder missed a segment, the multi-source loaders fell back to the CDN but aborted that request at the 1.5 s per-holder time-to-first-byte budget. The CDN path usually takes 1–2 s, so the segment failed and the player stalled. The fallback now gets only the overall fetch timeout (8 s). Holder fetches keep the 1.5 s budget.

Action required

Mint playback by title_id, not file_id

POST /v1/api/publisher/playback-session accepts { title_id } and resolves the title's current encode when it mints, so a re-encode never strands a stored id. The docs showed only { file_id }, which still works but goes stale after a re-encode. The response also carries the resolved file_id and a key_url, and ttl_minutes (default 180, 5–720) is accepted.

Fixed

Licensing windows were removed in June; availability is always “live”

Carriage has been creator-controlled since 2026-06-22: a creator allows a publisher to carry all of their published content. Catalogue availability is always "live", window is always null, and ?availability=licensed returns nothing. The docs still described licensed titles, avail windows and recorded grants. That text is gone, and the 2026-08-25 note that windows are “recorded on request” is superseded.

New

GET /v1/api/publisher/series is documented

GET /v1/api/publisher/series?id=<series_id> (API key) returns a series with its seasons and episodes, resolved live. It is now documented. (It is now also gated by carriage and lists only published episodes, and since 2026-10-31 its art is signed; see the entries above.)

Clarification

Publisher art, posters and lookups

  • Catalogue thumbnail_*_url are signed when the response is built (11–12 h, hour-quantised). Re-fetch the catalogue for fresh ones. /v1/api/object-url only signs your own objects, so it is not a publisher path.
  • playback.poster_url is a signed URL for the title's video file, not an image. Use the thumbnails for art. (Since 2026-10-31 it is the title's image; see that entry.)
  • There is no per-title publisher lookup. This is a known gap.
Action required

Standalone compute: use /v1/api/compute-api with an access key

The docs showed POST /v1/api/compute/{id} with Bearer access_key:secret and the SDK's submitTranscription / submitVisionAI / getComputeStatus. That route only accepts a signed-in dashboard session, so both return 401 with an access key (SDK 1.9.4 and 1.9.5). The access-key routes are POST /v1/api/compute-api/{file_id} and GET /v1/api/compute-api/job/{job_id}, and they are now documented with the body, response, tiers and webhook behaviour.

  • Transcription tiers are not validated: fast / balanced / accurate (aliases fastest / recommended) are recognised and anything else runs as balanced. Vision must be exactly standard or hq, or the job can stay queued indefinitely.
  • Standalone transcription.complete / vision.complete go only to the request's webhook_url (no account default), are signed only with its webhook_secret, and have no failure event. Delivery is 3 attempts, 1 s then 2 s apart; the vision page said 1/2/4 s.
Clarification

Series art and links that outlive 12 hours

  • Series and season posters had no signed source; /publisher/series returned them unsigned. (Signed since 2026-10-31; see that entry.)
  • og:image, feeds, cached VAST and email: point the markup at a route on your own domain that resolves a fresh signed URL and proxies the bytes. The Object URLs section said “we will raise the token lifetime for that path”; no such mechanism exists, and that sentence is gone.
Fixed

Re-upload: when an encode starts by itself

The docs said a re-upload never auto-starts an encode. It does if the new upload declared pipeline.encode: true. (A folder of yours named video does not get the Studio default pipeline; see the entry above.) Calling /v1/api/encoding/request as well is always safe.

2026-10-29

SDK v1.9.2 · hostnames · /s3/ · encoding auth · docs corrections

Platform changes from the last week, plus another correctness pass on these docs. Corrections are listed here rather than quietly edited, because integrations were built against the old text.

Action required

New hostnames: cdn.mosnet.io and api.mosnet.io — the old names keep working

Since 2026-10-27, URLs the backend generates use cdn.mosnet.io. That covers signed object URLs from /v1/api/object-url, key and subtitle URIs inside HLS manifests, and URLs in webhook and status payloads. api.mosnet.io is the same API with the same credentials, and the docs' examples now use it. cdn.zibnetwork.com and api.zibnetwork.com keep serving the same paths, so stored URLs still work. Storage-node hosts are unchanged (zb-*.zibnetwork.com), and SDK 1.9.2 still defaults to the zibnetwork.com hosts; pass apiEndpoint / cdnEndpoint to change that. Retiring the old names is coming, with no date yet. It will be announced here first.

The action: if your site sends a Content-Security-Policy, add https://cdn.mosnet.io to img-src, media-src and connect-src. Otherwise posters and playback break in the browser (Safari first) while every server-side check passes. The full host list is in the new Hostnames & CSP section.

Action required

The /s3/ path is deprecated and closing for private data

Do not build on s3.zibnetwork.com/s3/… or the SDK methods that call it (listBuckets, listObjects, deleteObject, upload, uploadMultipart, uploadAuto). Use /v1/api/upload/initiate + uploadDirect to upload, GET /v1/api/object-url to read, and DELETE /v1/api/file/{file_id} (or DELETE /v1/api/storage/{bucket}/{key}) to delete. The credential ZiB verifies is Authorization: Bearer access_key:secret_key. AWS SigV4 signing is not a supported way to authenticate to ZiB.

Known gap: there is no non-S3 “list my files” endpoint for access-key credentials yet. It is coming. Until then, record each file_id in your own database at upload time. The S3 entries of 2026-08-25 and 2026-08-28 below are history, not a recommendation.

Action required

Encoding routes check who is calling and who owns the file

POST /v1/api/encoding/jobs (SDK startEncoding) and POST /v1/api/encoding/request now start an encode only for a file owned by the calling account. Any other file is 404, the same answer as a missing one. A customer_id in the request body must be your own, or the request is 403. GET /v1/api/encoding/jobs/{id} answers only for your own jobs (404 otherwise). Storage-node-only encoding routes refuse tenant credentials with 403, and the unused cleanup routes (delete-source, delete-encoded, delete-all, schedule-cleanup, cleanup/process) answer 410. Calls on your own files are unchanged. Deletes go through DELETE /v1/api/file/{file_id}.

Fixed

v1.9.2 — the AES-128 key is cached for its max-age, not fetched per segment

video.js/VHS requested the HLS key for every segment. installVhsMultiSource and installGlobalMultiSource now answer repeats from memory: only this title's key, keyed by the exact URI, and never longer than the server's Cache-Control max-age (30 s today; a missing header counts as 30 s), the token's exp, or keyCacheMaxMs (default 10 min). Failed or wrong-sized answers are never cached. Expect about one key request per 30 s of playback. hls.js caches keys itself.

Fixed

v1.9.1 — “Empty HLS response” on a replayed segment

The multi-source loaders handed every load of a segment the same buffer, and players transfer segment bytes to their workers, which empties it. A seek back, an ABR switch back or a second player then got 0 bytes and video.js logged “Empty HLS response”. It was never missing data. Each load now gets its own copy, and undelivered prefetches are bounded (prefetchCap, default parallel × 4).

New

v1.9.0 (2026-10-26) — rendition-level holders, BLAKE3-verified multi-source HLS

The multi-source loaders (createHlsLoader, installVhsMultiSource, installGlobalMultiSource) fetch the holder list once per quality, spread consecutive segments across holders, fail over on error or timeout, and verify every segment's BLAKE3 hash before the player sees it. Play counting now comes from the player clock (attachToVideo). These loaders are now documented under Video Encoding → Multi-source playback.

Clarification

Docs corrections

  • Unsigned object URLs: the docs said object-token enforcement “is currently off”. That is a server-side switch, set for the whole platform or per account, so the docs no longer state it. Build only on signed URLs from /v1/api/object-url.
  • uploadDirect's cdn_url is a bare, unsigned URL that the SDK builds itself. The Quick Start showed it with ?exp=&tok=. Persist file_id only.
  • GET /v1/api/pipeline/{file_id} → cdn_url is a signed URL for the uploaded source file, not the HLS stream. The polling example returned it as the manifest URL. Use stages.encoding.hls_manifest_url. The example also checked failed before supersede, so it could never detect a supersede. Fixed.
  • getEncodingStatus() has no srt_file_id / sidecar_file_id. Read them from /pipeline (stages.*.output_file_id), getComputeStatus(), or the transcription.complete / vision.complete webhooks.
  • Sidecars and subtitle files were fetched with bare getCdnUrl() URLs in the compute docs. They are now fetched through /v1/api/object-url.
  • encoding.progress was still shown in a handler example after being marked not emitted. The example now polls /pipeline.
  • Per-request webhooks never carry file.ready. This is now stated inside the Pattern 1 card and the PIN-upload section, not only under Deleting Files.
  • DASH: manifest.mpd was listed as safe to store. Encoding produces HLS only, and dash_manifest_url is null.
  • Telemetry: the constructor example put access and secret keys on a player page. Use ZiBStorage.startTelemetry() in the browser.
  • Python example used /s3/ listing and /encoding/jobs. It now uses /encoding/request, /pipeline, /object-url and DELETE /file.
Action required

Webhook routing and signing, stated precisely

  • Jobs created with POST /v1/api/encoding/jobs (SDK startEncoding) store no webhook URL and send no encoding.* webhooks. Use /v1/api/encoding/request if you rely on them.
  • A job's webhook URL and secret are fixed when it is created: the per-request values, else (on /encoding/request) the account's encoding_webhook_url. Pipeline encodes use the pipeline's webhook_url.
  • Account URLs: webhook_url → file.ready; encoding_webhook_url → file.deleted, upload.reassigned, file.failed (unless a URL was stored with the upload) and the job-event default. They can be read only in the dashboard; there is no access-key read API today.
  • Secret rotation: the old secret stays valid 24 h and the file events (including file.failed) carry X-ZiB-Signature-Previous. Job events are signed once, with the secret current when the job was requested, so keep the old secret until in-flight jobs finish. A verification snippet is in the docs.
  • Pattern A sent object_key: <path> + bucket, which the handler rejects with 400. The canonical form is object_key: <file_id>.
  • New guidance for when /v1/api/object-url fails at serve time: never fall back to an unsigned URL.

2026-10-01

network

Durability work on the storage network. Nothing changes on your side — no SDK release, no API change — but two guarantees are now stronger and worth knowing about.

New

Rolling integrity verification of every stored byte

Every storage node now re-reads each object it holds end to end and checks it against its content hash on a rolling schedule (paced so it never competes with serving). A copy that fails the check is quarantined, reported, and replaced from a good copy automatically — so silent corruption after write, which no prior check could see, is now detected and repaired without anyone noticing it first.

New

Video durability is tracked per quality variant, not per segment

Each HLS variant (360p, 720p, …) is now a single content-addressed unit — one root over its ordered segments — replicated to at least three nodes across at least two failure domains. A node holds a variant completely or not at all, so a variant can no longer degrade one segment at a time without the network noticing, and any holder can serve any segment. Existing titles are being migrated to this model automatically; playback is unchanged.

Clarification

No SDK change

Manifests, keys, /holders multi-source resolution and object reads are exactly as before. When per-variant placement is later refined into segment groups, the SDK's source list will gain a per-group shape — that will ship as a versioned SDK release and be announced here first.

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 fastest→fast, recommended→balanced). 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.