Upload a file
POST/api/v1/uploads
- Access
- API key
- Permission
content:write
Upload a still image (limits.maxImageBytes: 10 MB on the free plan by default) or an MP4/WebM video (limits.maxVideoBytes: 100 MB on the free plan). A plan without a per-file limit (Pro) is bounded by its storage quota (413 QUOTA_EXCEEDED) and the server’s ceiling for one file — 64 MB per image, 4 GB per video (params.ceiling = server); a body above 100 MB is better sent with /v1/uploads/resumable. The first bytes decide the type, so another format (400 UPLOAD_FORMAT_UNSUPPORTED) or an oversized file (413 IMAGE_TOO_LARGE / VIDEO_TOO_LARGE with params.limitBytes) is refused without storing it. A video is kept as one version: it must already be what limits.video describes — H.264/AAC or VP9/AV1 with Opus, within its frame, rate, duration and bitrate — or it is refused with 415 VIDEO_NOT_SUITABLE and params.problems (the site and the Windows app compress it first; the server never transcodes). Videos are streamed, never held in memory, and return PROCESSING until the worker has stored the version without container metadata (MP4 or WebM) and its cover. An account runs limits.uploadsAtOnce uploads at a time (429 UPLOAD_USER_BUSY); a busy server answers 503 UPLOAD_SERVER_BUSY; both carry Retry-After and changed nothing. A body idle for 30 seconds is dropped (408 UPLOAD_STALLED).
Example request
curl -X POST "https://lac.pics/api/v1/uploads" \
-H "Authorization: Bearer lac_api_YOUR_KEY" \
-H "Idempotency-Key: capture-2026-09-27-a7f3c1" \
-F "file=@capture.png" \
-F "visibility=unlisted"Parameters
Headers
Idempotency-KeystringUse a new random key for each logical file and retain it on retries with identical bytes, filename and fields. Account-scoped receipts persist at least 24 hours. Replays return the existing content with 201 and never extend TTL. An in-flight attempt returns 409 with Retry-After; a key reused with different input returns 409; deleted or expired content returns 410. A crashed handler lease expires after 5 minutes. No header means a new upload on every request.
pattern
^[A-Za-z0-9_-]{16,128}$
Request body
multipart/form-datarequired
filestring · binaryrequiredAn image or a video.
tagsstringJSON-encoded string array, e.g. ["design","release"]. Same max10/32-character normalization rules as JSON tags.
up to 2,048 characters
titlestringTitle, up to 180 characters.
up to 180 characters
visibilitystringprivate is owner-only. unlisted is accessible by link. public also appears in the authenticated feed and profile. public cannot be combined with oneTime (400 ONE_TIME_NOT_PUBLIC). Omitted on creation: use the account default (private or unlisted). Omitted on update: retain current access.
values:
"private""unlisted""public"ttlstringRelative to this request; forever clears expiry. Access stops at expiry; encrypted objects are removed asynchronously through a persistent retry queue.
values:
"forever""day""week""month"passwordstringrequest only8–128 characters. Empty string removes the password; omission retains it on update. Stored as Argon2id, never returned. Changing it revokes previous viewer access. Other accounts see protected content in the feed, favorites, profiles and albums only as a locked card (id, type, author, lock flags): title, tags, file name, size, dimensions, dates and previews stay hidden.
up to 128 characters · pattern
^(?:[\s\S]{8,128})?$oneTimebooleanOnly the first guest browser to explicitly open the share page receives access, for at most 10 minutes (including video Range requests). Owner previews and GET/HEAD do not consume the link. Omission retains the setting on update. Cannot be combined with visibility public: the resulting state is checked on creation and update (400 ONE_TIME_NOT_PUBLIC). Other accounts never see one-time content in the feed, profiles or albums.
default false
Responses
201Success
CreatedidstringrequiredThe file id.
titlestringrequiredTitle, up to 180 characters.
tagsarray of stringrequiredNFKC, trim, collapse spaces and lowercase; deduplicated after normalization. Raw controls are invalid. Omission preserves tags on update; an empty array clears them.
up to 10 items · unique
originalNamestringThe original file name.
typestringrequiredIMAGE,VIDEO,PASTEorFOLDER(a folder of code).values:
"IMAGE""VIDEO""PASTE""FOLDER"mimestringrequiredThe MIME type of the stored file.
pasteFormatstringor nullvalues:
"plain""markdown""code"pasteLanguagestringor nullfolderFilesintegeror nullA code folder (type FOLDER): files in its archive. Null for other kinds. The archive itself is the media: GET /api/media/{id} serves it (Range requests read one file by the ZIP central directory), ?download=1 as an attachment.
at least 1
revisionintegerrequiredOptimistic revision for editing saved paste text; distinct from share access version.
at least 1
accessVersionintegerrequiredShare access version: 1 for a new file, raised by every change of visibility, password or one-time. From 2 on,
urlcarries it as ?v= so messengers build a fresh link card instead of the one they kept for the previous access.at least 1
sizeinteger · int64requiredSize in bytes.
visibilitystringrequiredprivate is owner-only. unlisted is accessible by link. public also appears in the authenticated feed and profile. public cannot be combined with oneTime (400 ONE_TIME_NOT_PUBLIC). Omitted on creation: use the account default (private or unlisted). Omitted on update: retain current access.
values:
"private""unlisted""public"allowDownloadbooleanrequiredpasswordProtectedbooleanrequiredoneTimebooleanrequiredconsumedAtstring · date-timeor nullreadExpiresAtstring · date-timeor nullreadMaxExpiresAtstring · date-timeor nullhasVideoTextbooleanvideoTextRevisioninteger0–2,147,483,647
viewsintegerrequiredExplicit guest opens, deduplicated during the viewer grant (normally 24 hours). Owner previews are excluded.
at least 0
viewLimitintegeror nullLink control (Pro, plan feature view_limit): after this many counted views the link answers like an expired one (404 SHARE_UNAVAILABLE) to everyone but the owner and viewers still holding the grant of their counted open. Media of a limited file is served only after an explicit open (POST /api/share/{id}/open), so direct media links and link cards cannot bypass the count. Null: no limit.
1–1,000,000
viewLimitReachedAtstring · date-timeor nullWhen the view limit was used up; null while views remain or without a limit.
mediaStatusstringNew videos are processed asynchronously. Playback and previews require READY. The owner may explicitly download the original with ?download=1 while PROCESSING or FAILED; it is returned as an application/octet-stream attachment.
values:
"PROCESSING""READY""FAILED"mediaErrorstringor nullThe reason when processing failed.
widthintegeror nullWidth in pixels.
heightintegeror nullHeight in pixels.
durationMsintegeror nullVideo duration in milliseconds.
videoCodecstringor nullhasAudiobooleanpreviewUrlstringor nullEncrypted derived preview served with the same access checks as the source.
expiresAtstring · date-timeor nullcreatedAtstring · date-timerequiredWhen it was created.
urlstring · urirequiredThe file page to share: {WEB_ORIGIN}/s/{id}, with ?v={accessVersion} once access changed (the page ignores the parameter; older links keep working).
mediaUrlstringrequiredThe address of the file itself.
moderationHoldstringor nullAutomatic check:
review— hidden from everyone but the owner until a moderator decides;blocked— kept hidden by a moderator. Others get such a file exactly like a private one, so only the owner ever sees a non-null value.values:
"review""blocked"favoritebooleanPresent in list responses
ownedbooleanPresent in list responses
authorobject3 fields
idstringThe author’s id.
usernamestringnamestring
contentContentsame as Content above
- 401Authentication required
- 403Insufficient scope
- 404Content unavailable
- 409Media processing, upload idempotency or paste revision conflict
- 410Previously uploaded content was deleted or expired
- 415VIDEO_NOT_SUITABLE: the video is not stored as it is; params.problems names what to fix (compress it on the client).VIDEO_NOT_SUITABLE
Example response
{
"id": "Xq3u9RkT0bLmA7cV2pWz1eFy",
"title": "capture",
"tags": [
"release"
],
"originalName": "capture.png",
"type": "IMAGE",
"mime": "image/png",
"pasteFormat": null,
"pasteLanguage": null,
"folderFiles": 1,
"revision": 1,
"accessVersion": 1,
"size": 482133,
"visibility": "unlisted",
"allowDownload": true,
"passwordProtected": false,
"oneTime": false,
"consumedAt": null,
"readExpiresAt": null,
"readMaxExpiresAt": null,
"hasVideoText": false,
"videoTextRevision": 0,
"views": 3,
"viewLimit": null,
"viewLimitReachedAt": null,
"mediaStatus": "READY",
"mediaError": null,
"width": 1920,
"height": 1080,
"durationMs": null,
"videoCodec": null,
"hasAudio": false,
"previewUrl": null,
"expiresAt": null,
"createdAt": "2026-09-27T09:30:00.000Z",
"url": "https://lac.pics/s/Xq3u9RkT0bLmA7cV2pWz1eFy",
"mediaUrl": "/api/media/Xq3u9RkT0bLmA7cV2pWz1eFy",
"moderationHold": null,
"favorite": false,
"owned": true,
"author": {
"id": "cmf8a1x2k0000q7lh3v9w2e4d",
"username": "alex",
"name": "Alex"
}
}Error codes
| Code | Status | Message |
|---|---|---|
ONE_TIME_NOT_PUBLIC | 400 | A one-time file cannot be published in the feed and profile. Choose link access or turn off the one-time link. |
UPLOAD_FORMAT_UNSUPPORTED | 400 | Supported formats are still PNG, JPEG, WebP and AVIF images, plus MP4 and WebM video. Images can contain up to 40 megapixels. |
UPLOAD_STALLED | 408 | The file stopped arriving. Check your connection and upload it again. |
IMAGE_TOO_LARGE | 413 | The image exceeds the size limit. Compress it or reduce its resolution. |
QUOTA_EXCEEDED | 413 | There is not enough storage in the account. |
VIDEO_TOO_LARGE | 413 | The video exceeds the size limit. Make it smaller with Compress in your lacuna console or the Windows app. |
VIDEO_NOT_SUITABLE | 415 | The video must be compressed before upload: lacuna keeps H.264 with AAC (MP4) or VP9/AV1 with Opus (WebM) up to 3840 × 2160. Compress it in the lacuna app for Windows or in Compress on the site. |
UPLOAD_USER_BUSY | 429 | Your account already has uploads in progress. The next one starts when one of them finishes. |
UPLOAD_SERVER_BUSY | 503 | The server is receiving many files right now. Try again in a few seconds. |
Answers of any method with a key (an invalid or expired key, a missing permission, too many requests) are in Errors.