Upload and delivery API
Everything enters Fotolane through a single endpoint: /api/uploadFile/. It accepts whole files and resumable chunks over the same path. Stored objects come back through transform URLs or, for large ones, as a stream of segments.
Hosts
Fotolane answers on two hosts. Each one is a separate edge network in front of the same origin in Tallinn.
| Host | Carries |
|---|---|
img.vikmen.run |
Uploads, transform URLs, and segments addressed by query. |
cdn.vikmen.run |
The delivery host: transform URLs, delivery sessions addressed by path, and segments addressed by query. |
Our upload and gallery components keep a session on both hosts and move to the other one when the first stops answering.
Authentication
Two equivalent ways to present a project token.
| Method | Where | Notes |
|---|---|---|
Authorization |
Request header | Preferred for server-to-server calls. Format: Bearer <token>. |
auth |
Query parameter | For browser uploads and any client that cannot set headers freely, pass ?auth=<token>. Tokens presented this way are short-lived, scoped to a single session, and 8 to 12 characters long. |
A request without a recognised token is answered with 401 and an empty body. The endpoint deliberately does not describe why a token was rejected.
Whole-file upload
The simplest form. Suitable up to roughly 64 MB per request.
POST /api/uploadFile/ HTTP/1.1
Host: img.vikmen.run
Authorization: Bearer <token>
Content-Type: multipart/form-data; boundary=----fl
------fl
Content-Disposition: form-data; name="file"; filename="porch-light.jpg"
Content-Type: image/jpeg
<binary>
------fl--
Chunked upload
For large originals, flaky mobile links, or anything you want to resume. Open a session, then send parts in any order.
| Parameter | Type | Meaning |
|---|---|---|
auth | string | Session token returned when the upload session is opened. |
chunk_id | integer | Zero-based index of the part being sent. Parts may arrive out of order. |
total | integer | Optional. Total number of parts, so the server can finalise without a separate call. |
# send part 7 of an open session curl -X POST "https://img.vikmen.run/api/uploadFile/?auth=$SID&chunk_id=7" \ --data-binary @part-07.bin # poll session state (GET on the same path) curl "https://img.vikmen.run/api/uploadFile/?auth=$SID"
Sessions stay open for 30 minutes after the last accepted part. Parts are held in edge storage and are never cached: the endpoint always answers with Cache-Control: no-store.
Part integrity
Every chunked request carries a signature of its own payload.
| Header | Value |
|---|---|
X-Part-Checksum |
Checksum of the part body, salted with the session token. Length varies with the negotiated digest; clients that cannot compute it may omit the header, in which case the part is accepted but not verified until the session is finalised. |
The signature is recomputed per request, so it differs for every part even when the same bytes are re-sent after a retry. Delivery sessions send the same header in a longer form: the address of the segment, with the signature in its _dc parameter.
Browser clients (CORS)
The endpoint answers preflight requests, so uploads can run straight from a page.
OPTIONS /api/uploadFile/ HTTP/1.1
<- 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Part-Checksum
Access-Control-Max-Age: 86400
Because a browser cannot attach an Authorization header to a form post without dropping into fetch(), browser uploads normally pass the session token as ?auth= instead. Both paths are equivalent server-side.
Delivery and transforms
Once stored, an object is addressed as /<id>/<transform>.<ext>. Parameters are joined with commas, in any order.
| Parameter | Example | Meaning |
|---|---|---|
w, h | w_640 | Target width or height in pixels. |
fit | fit_cover | One of cover, contain, pad. |
ar | ar_16:9 | Aspect ratio; combined with fit_cover it crops. |
q | q_78 | Encoder quality, 1–100, on the JPEG scale; WebP and AVIF are matched to look the same. Defaults to 82. |
| extension | .avif | jpg, png, webp, avif. Omit to keep the original. |
# 640 pixels wide, cropped to 16:9, as AVIF
curl -O "https://cdn.vikmen.run/<id>/w_640,ar_16:9,fit_cover,q_78.avif"
Unknown parameters are ignored rather than served as an error, so a typo never blanks a page.
Progressive delivery
Large originals and gallery prefetches are streamed in ordered segments instead of one long response, so a slow client can start rendering early and resume after a dropped connection.
| Path | Meaning |
|---|---|
/api/v2/streams/<session>/<seq> |
Segment seq of an open delivery session. Sessions are opened by the player or gallery component and identified by a UUID; segment numbering starts at zero. |
/api/v2/streams/<session> |
Session manifest: segment count, byte ranges and the content type being served. |
/api/v2/segment.bin?auth=<session>&chunk_id=<seq> |
The same segment addressed by query instead of path, for players and gateways that only accept a fixed file URL. Without chunk_id the session is returned as one continuous response. A request without auth gets 401. |
# fetch the third segment of an open session
curl "https://cdn.vikmen.run/api/v2/streams/$SID/2"
Path-addressed segments are served from the delivery host; the query form is answered on both hosts. Either way segments carry Cache-Control: no-store: the session is per-viewer and must not be shared between them. Unlike transform URLs, a stream session expires once the object has been fully delivered.
Receiver reports
While a session is open the player tells the origin what it has already decoded, so that segments can be skipped, or sent again after a drop. A report rides on the segment request itself: in the request headers X-Data-0 … X-Data-n on the path form, in the request body on the query form. Reports are opaque and can run to several kilobytes, so a gateway in front of the player should allow at least 32 KB of request headers.
Status codes
| Code | When |
|---|---|
200 | Part accepted, or session state returned. |
201 | Upload finalised; body carries the object descriptor. |
400 | Malformed request: missing part index, unreadable body, unsupported media type. |
401 | Token absent, expired, or not valid for this project. |
413 | Part exceeds the per-request limit for your plan. |
429 | Too many open sessions. Retry after the interval in Retry-After. |