Skip to content
Fotolane

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.

HostCarries
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.

MethodWhereNotes
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.

ParameterTypeMeaning
authstringSession token returned when the upload session is opened.
chunk_idintegerZero-based index of the part being sent. Parts may arrive out of order.
totalintegerOptional. 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.

HeaderValue
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.

ParameterExampleMeaning
w, hw_640Target width or height in pixels.
fitfit_coverOne of cover, contain, pad.
arar_16:9Aspect ratio; combined with fit_cover it crops.
qq_78Encoder quality, 1–100, on the JPEG scale; WebP and AVIF are matched to look the same. Defaults to 82.
extension.avifjpg, 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.

PathMeaning
/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

CodeWhen
200Part accepted, or session state returned.
201Upload finalised; body carries the object descriptor.
400Malformed request: missing part index, unreadable body, unsupported media type.
401Token absent, expired, or not valid for this project.
413Part exceeds the per-request limit for your plan.
429Too many open sessions. Retry after the interval in Retry-After.