{"openapi":"3.0.3","info":{"title":"Vylo Storage","version":"1.0.0","description":"Object storage, video hosting and encoding. Upload, fetch, list, rename, delete and encode from your own code. Encoding is free; you are billed for the storage the result occupies."},"servers":[{"url":"https://storage.vylo-tech.com"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Your zone key (vz_...), a short-lived upload token (vzt_...), or your account API key (vsk_...) depending on the endpoint."}}},"security":[{"bearerAuth":[]}],"tags":[{"name":"Files","description":"Upload, fetch, rename and delete. A key may contain slashes, which is what makes folders: there are no folder records to create, and one exists exactly while something is in it."},{"name":"Resumable uploads","description":"The tus 1.0 protocol — core, creation, creation-with-upload and termination. Use it for anything big enough that the connection itself is the risk. Existing tus clients (Uppy, tus-js-client, tus-py) work against this unchanged."},{"name":"Encoding","description":"Turn what somebody uploaded into something a browser will play. Encoding itself is free — you are billed only for the storage the result occupies, at the same rate as anything else. Jobs run one at a time and the original is never replaced."},{"name":"Links","description":"A zone is private by default, so a delivery link carries its own permission: a signature over that one object and the moment it stops working. Nothing about it works on a different file, a different zone, or a second past its expiry. A link can also be bound to one person, which is what stops a student pasting theirs into a group chat."},{"name":"Embedding the player","description":"Put a video on your own site with an iframe, or in an app with a WebView. The player reads the renditions, poster and caption tracks for itself, so a quality menu appears as soon as you have encoded more than one size."},{"name":"The client library","description":"One file, no build step. It uses fetch and nothing else, so the same code runs in a browser, on your server under Node 18+, and in React Native."},{"name":"Keeping a video private","description":"What a private zone actually guarantees, and what it does not. Worth reading once before you hand a link to anybody."},{"name":"Your account","description":"Authenticated with your API key (vsk_...) rather than your zone key. These live at the root rather than under /v1, because they are this site rather than the storage service."}],"paths":{"/v1/{zone}/{key}":{"put":{"tags":["Files"],"summary":"Upload a file, or replace one","description":"Answers as soon as the bytes are safely on this server, not when they reach the storage box — that second hop is ours to worry about. Send Content-Type if you know it; it is what delivery will send back.","security":[{"bearerAuth":[]}],"parameters":[{"name":"zone","in":"path","required":true,"description":"your zone name","schema":{"type":"string"}},{"name":"key","in":"path","required":true,"description":"the path within it, e.g. w1/intro.mp4","schema":{"type":"string"}},{"name":"Content-Type","in":"header","required":false,"description":"the file’s type","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}},"get":{"tags":["Files"],"summary":"Fetch a file","description":"Supports range requests, ETag and 304, which is what lets a video player seek instead of downloading everything first. For a private zone the request needs a signature — see Links. Delivery is normally done straight from https://cdn.vylo-tech.com instead.","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}},"head":{"tags":["Files"],"summary":"Size, type and ETag without the body","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}},"delete":{"tags":["Files"],"summary":"Delete one file","description":"Also removes any directory it was the last thing in.","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/zones/{zone}/objects":{"get":{"tags":["Files"],"summary":"List files, or one folder at a time","description":"With delimiter=/ this answers the way object stores have since S3: the keys directly under prefix, plus the distinct next segments as folders, each carrying the count and bytes of everything beneath it.","security":[{"bearerAuth":[]}],"parameters":[{"name":"prefix","in":"query","required":false,"description":"the folder to look in, e.g. w1/","schema":{"type":"string"}},{"name":"delimiter","in":"query","required":false,"description":"set to / to group into folders","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"1-1000, default 200","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"for paging","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/zones/{zone}/move":{"post":{"tags":["Files"],"summary":"Rename a file, or move it between folders","description":"A rename on the storage box, so moving a 2 GB lecture does not move 2 GB. It will not overwrite something already there.","security":[{"bearerAuth":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string","description":"the current key"},"destination":{"type":"string","description":"the new key"}}}}}},"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/zones/{zone}/folder":{"delete":{"tags":["Files"],"summary":"Delete a folder and everything under it","description":"prefix is required and cannot be empty: deleting a folder and emptying an account must not be one keystroke apart.","security":[{"bearerAuth":[]}],"parameters":[{"name":"prefix","in":"query","required":false,"description":"the folder, e.g. w1/","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/uploads":{"post":{"tags":["Resumable uploads"],"summary":"Start a resumable upload","description":"Upload-Metadata carries zone and key, each base64-encoded. The Location header is what you PATCH to.","security":[{"bearerAuth":[]}],"parameters":[{"name":"Upload-Length","in":"header","required":false,"description":"total size in bytes","schema":{"type":"string"}},{"name":"Upload-Metadata","in":"header","required":false,"description":"zone <b64>,key <b64>,filetype <b64>","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/uploads/{id}":{"head":{"tags":["Resumable uploads"],"summary":"Ask where an upload got to","description":"The one question resuming depends on. Answers Upload-Offset and Upload-Length.","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}},"patch":{"tags":["Resumable uploads"],"summary":"Send the next stretch of bytes","description":"A wrong offset answers 409 carrying the right one, so a confused client can always recover. When the last byte lands, the response carries X-Vylo-Object with the delivery URL.","security":[{"bearerAuth":[]}],"parameters":[{"name":"Upload-Offset","in":"header","required":false,"description":"where this chunk starts","schema":{"type":"string"}},{"name":"Content-Type","in":"header","required":false,"description":"application/offset+octet-stream","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}},"delete":{"tags":["Resumable uploads"],"summary":"Give up on an upload","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/zones/{zone}/probe":{"get":{"tags":["Encoding"],"summary":"What is inside a file, and whether a browser will play it","description":"Worth asking before queueing anything. suggested_preset is null when the file is already fine, \"remux\" when the codecs are good but the container is not, and a size preset when it genuinely needs re-encoding.","security":[{"bearerAuth":[]}],"parameters":[{"name":"key","in":"query","required":false,"description":"the object to inspect","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/encodes/presets":{"get":{"tags":["Encoding"],"summary":"What can be produced","description":"remux repackages without touching a pixel and runs at disk speed — it is the fix for an MKV or MOV that already holds H.264. The size presets re-encode and take real time.","security":[],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/encodes":{"post":{"tags":["Encoding"],"summary":"Queue an encode","description":"Answers 202 straight away: encoding a lecture takes minutes and holding a request open for it is how integrations time out. The result lands beside the source — w1/lecture.mkv becomes w1/lecture.720p.mp4 — and the original is left alone.","security":[{"bearerAuth":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"zone":{"type":"string","description":"your zone"},"key":{"type":"string","description":"the source object"},"preset":{"type":"string","description":"a name from /v1/encodes/presets"}}}}}},"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/encodes/{id}":{"get":{"tags":["Encoding"],"summary":"How an encode is going","description":"state is queued, running, done or failed. progress is 0 to 1 while running.","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}},"delete":{"tags":["Encoding"],"summary":"Cancel an encode","description":"A running job has its encoder stopped; a queued one is simply dropped.","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/zones/{zone}/encodes":{"get":{"tags":["Encoding"],"summary":"Every encode for a zone, newest first","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/sign":{"post":{"tags":["Links"],"summary":"Mint a link that expires","description":"Unbound, a link is a bearer token: whoever holds it can watch until it expires. `viewer` makes it attributable — the id rides in the URL, is covered by the signature and cannot be edited, so a link that turns up in a group chat says who it was given to. `ip` goes further and stops it working anywhere else at all.","security":[{"bearerAuth":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"zone":{"type":"string","description":"your zone"},"key":{"type":"string","description":"the object"},"ttl":{"type":"string","description":"seconds; 0 uses the default"},"viewer":{"type":"string","description":"an id of your own, e.g. a student number"},"ip":{"type":"string","description":"the address you saw the viewer on"},"bind_ip":{"type":"string","description":"bind to the address calling this endpoint instead"}}}}}},"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/play/{zone}/{key}":{"get":{"tags":["Embedding the player"],"summary":"The player page — this is what goes in the iframe","description":"Takes the exp and sig from a signed link. Public by design, because the signature IS the permission, and the one page that allows being framed on another site.","security":[{"bearerAuth":[]}],"parameters":[{"name":"autoplay","in":"query","required":false,"description":"true to start on load (implies muted; browsers require it)","schema":{"type":"string"}},{"name":"muted","in":"query","required":false,"description":"true to start silent","schema":{"type":"string"}},{"name":"loop","in":"query","required":false,"description":"true to repeat","schema":{"type":"string"}},{"name":"controls","in":"query","required":false,"description":"false to hide them entirely","schema":{"type":"string"}},{"name":"t","in":"query","required":false,"description":"start here: 90, 1m30s or 01:30","schema":{"type":"string"}},{"name":"color","in":"query","required":false,"description":"accent colour, e.g. 4c8dff","schema":{"type":"string"}},{"name":"preload","in":"query","required":false,"description":"none, metadata (default) or auto","schema":{"type":"string"}},{"name":"poster","in":"query","required":false,"description":"override the poster image","schema":{"type":"string"}},{"name":"ar","in":"query","required":false,"description":"16:9, 9:16, 4:3 or a number. Default auto — the video's own","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/v1/zones/{zone}/playable":{"get":{"tags":["Embedding the player"],"summary":"Everything a player needs, already signed","description":"The renditions to offer as qualities, the poster and any caption tracks. Authenticated EITHER by a zone key or by the signature the viewer already holds — which is how the player page, holding no secret, builds a quality menu. Signatures it returns never outlive the one that asked for them.","security":[{"bearerAuth":[]}],"parameters":[{"name":"key","in":"query","required":false,"description":"the video","schema":{"type":"string"}},{"name":"exp","in":"query","required":false,"description":"from your signed link","schema":{"type":"string"}},{"name":"sig","in":"query","required":false,"description":"from your signed link","schema":{"type":"string"}},{"name":"ttl","in":"query","required":false,"description":"seconds; capped at what your link has left","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/me":{"get":{"tags":["Your account"],"summary":"Usage, quota and balance","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/account/upload-token":{"post":{"tags":["Your account"],"summary":"A short-lived credential scoped to your zone","description":"What a browser should hold instead of your zone key: it expires, and it works on nothing but your own zone. Anywhere these docs say $ZONE_KEY, one of these works too.","security":[{"bearerAuth":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ttl":{"type":"string","description":"seconds, 60 to 86400; default 900"}}}}}},"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/account/zone-key/rotate":{"post":{"tags":["Your account"],"summary":"Replace your zone key","description":"Shown once. Anything still using the old one stops immediately.","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/account/key/rotate":{"post":{"tags":["Your account"],"summary":"Replace your API key","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/account/charges":{"get":{"tags":["Your account"],"summary":"What you have been billed, by month","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}},"/account/payment-claims":{"post":{"tags":["Your account"],"summary":"Tell us you have paid","description":"A claim never credits anything by itself; it is applied once it is confirmed.","security":[{"bearerAuth":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"string","description":"USD"},"method":{"type":"string","description":"bank_transfer, fib, fastpay, cash, other"},"reference":{"type":"string","description":"a transfer id or receipt number"}}}}}},"responses":{"200":{"description":"OK"},"401":{"description":"the key is not valid for that zone"}}}}}}