Skip to content

Share a folder of code

POST/api/v1/folders

Access
API key
Permission
content:write

Share a folder of code: a ZIP of up to 32 MB, of which the server keeps up to 2000 text files (20 MB of text, 512 KB each). The server decides what stays: folders of dependencies, build output and version control, secrets (they are never stored), the root .gitignore, names Windows cannot create, paths deeper than 20 or longer than 200 characters, case collisions and anything that is not UTF-8 text by its bytes are left out and listed in folder.skipped. The kept files are stored as a new ZIP (sorted, UTF-8 names, deflate) and counted in the quota by its bytes.

The archive must be a plain ZIP: one disk, no ZIP64, no encryption, stored or deflated entries, UTF-8 names and no absolute, drive or «..» paths (400 FOLDER_ARCHIVE_INVALID). Nothing left: 400 FOLDER_EMPTY; too many files or too much text: 413 FOLDER_TOO_MANY_FILES / FOLDER_TOO_LARGE; an archive over the size limit: 413 UPLOAD_TOO_LARGE with params.limitBytes. With an antivirus on the server a threat is 422 FOLDER_INFECTED. An archive whose files all sit in one top folder («my-app/…», a repository download) is that folder: the top folder is dropped from the paths and its .gitignore is the root one. Title defaults to the archive name without .zip. Uploads share limits.uploadsAtOnce with POST /uploads.

Example request

curl -X POST "https://lac.pics/api/v1/folders" \
  -H "Authorization: Bearer lac_api_YOUR_KEY" \
  -H "Idempotency-Key: folder-2026-09-28-my-app-1" \
  -F "file=@my-app.zip" \
  -F "visibility=unlisted"

Parameters

Headers

  • Idempotency-Keystring

    As for POST /uploads: retain it on retries with identical bytes and fields; a replay returns the folder made before (folder: null).

    pattern ^[A-Za-z0-9_-]{16,128}$

Request body

multipart/form-datarequired

  • filestring · binaryrequired

    The ZIP; its file name («my-app.zip») names the folder when no title is given.

  • tagsstring

    JSON-encoded string array, as for POST /uploads.

    up to 2,048 characters

  • titlestring

    Title, up to 180 characters.

    up to 180 characters

  • visibilitystring

    private 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"

  • ttlstring

    Relative 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 only

    8–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})?$

  • oneTimeboolean

    Only 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

  • 201SuccessCreatedFolder
    • idstringrequired

      The file id.

    • titlestringrequired

      Title, up to 180 characters.

    • tagsarray of stringrequired

      NFKC, 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

    • originalNamestring

      The original file name.

    • typestringrequired

      IMAGE, VIDEO, PASTE or FOLDER (a folder of code).

      values: "IMAGE" "VIDEO" "PASTE" "FOLDER"

    • mimestringrequired

      The MIME type of the stored file.

    • pasteFormatstringor null

      values: "plain" "markdown" "code"

    • pasteLanguagestringor null
    • folderFilesintegeror null

      A 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

    • revisionintegerrequired

      Optimistic revision for editing saved paste text; distinct from share access version.

      at least 1

    • accessVersionintegerrequired

      Share access version: 1 for a new file, raised by every change of visibility, password or one-time. From 2 on, url carries it as ?v= so messengers build a fresh link card instead of the one they kept for the previous access.

      at least 1

    • sizeinteger · int64required

      Size in bytes.

    • visibilitystringrequired

      private 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"

    • allowDownloadbooleanrequired
    • passwordProtectedbooleanrequired
    • oneTimebooleanrequired
    • consumedAtstring · date-timeor null
    • readExpiresAtstring · date-timeor null
    • readMaxExpiresAtstring · date-timeor null
    • hasVideoTextboolean
    • videoTextRevisioninteger

      0–2,147,483,647

    • viewsintegerrequired

      Explicit guest opens, deduplicated during the viewer grant (normally 24 hours). Owner previews are excluded.

      at least 0

    • viewLimitintegeror null

      Link 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 null

      When the view limit was used up; null while views remain or without a limit.

    • mediaStatusstring

      New 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 null

      The reason when processing failed.

    • widthintegeror null

      Width in pixels.

    • heightintegeror null

      Height in pixels.

    • durationMsintegeror null

      Video duration in milliseconds.

    • videoCodecstringor null
    • hasAudioboolean
    • previewUrlstringor null

      Encrypted derived preview served with the same access checks as the source.

    • expiresAtstring · date-timeor null
    • createdAtstring · date-timerequired

      When it was created.

    • urlstring · urirequired

      The file page to share: {WEB_ORIGIN}/s/{id}, with ?v={accessVersion} once access changed (the page ignores the parameter; older links keep working).

    • mediaUrlstringrequired

      The address of the file itself.

    • moderationHoldstringor null

      Automatic 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"

    • favoriteboolean

      Present in list responses

    • ownedboolean

      Present in list responses

    • authorobject
      3 fields
      • idstring

        The author’s id.

      • usernamestring
      • namestring
    • contentContent

      same as Content above

    • folderobjectrequiredor null

      What the server kept and left out. Null when an Idempotency-Key replay returns the folder made before.

      5 fields
      • filesintegerrequired

        at least 1

      • textBytesintegerrequired

        at least 0

      • skippedarray of FolderSkiprequired

        Sorted by path; the first 200.

        up to 200 items

        FolderSkip 4 fields
        • pathstringrequired

          A file, or a whole folder when it ends with «/».

        • reasonstringrequired

          dependencies (node_modules, vendor of a package manager, .venv…), build (dist, build, out, target, .next, bin/obj of .NET, coverage, caches…), service (.git, .svn, .hg, .idea, .vs), secret (.env* except .env.example, *.pem, *.key, id_rsa*, credentials*, token files, or a private key inside), gitignore (the root .gitignore), name (a name Windows cannot create), path (too deep or long), large (a file over the size limit), duplicate (a name that differs only by case), binary (not UTF-8 text by its bytes).

          values: "dependencies" "build" "service" "secret" "gitignore" "name" "path" "large" "duplicate" "binary"

        • filesintegerrequired

          at least 1

        • sizeintegerrequired

          Uncompressed bytes.

          at least 0

      • skippedFilesintegerrequired

        at least 0

      • gitignorestringrequired

        Whether the root .gitignore was applied: none without a readable one, complex when its patterns needed too much work (then it is not applied at all).

        values: "applied" "none" "complex"

  • 401Authentication required
  • 403Insufficient scope
  • 404Content unavailable
  • 408The request body stopped arriving
  • 409Media processing, upload idempotency or paste revision conflict
  • 410Previously uploaded content was deleted or expired
  • 422FOLDER_INFECTEDFOLDER_INFECTED
  • 429Rate limit, or the account already uses all its upload slots
  • 503Upload or password verification capacity reached

Example response

201 · application/json
{
  "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"
  },
  "folder": {
    "files": 1,
    "textBytes": 0,
    "skipped": [
      {
        "path": "string",
        "reason": "dependencies",
        "files": 1,
        "size": 482133
      }
    ],
    "skippedFiles": 0,
    "gitignore": "applied"
  }
}

Error codes

CodeStatusMessage
FOLDER_ARCHIVE_INVALID400The folder archive could not be read: send a plain ZIP without a password, with UTF-8 names and no paths like “../”.
FOLDER_EMPTY400No files are left to share: dependencies, build output, secrets and non-text files are not uploaded.
ONE_TIME_NOT_PUBLIC400A one-time file cannot be published in the feed and profile. Choose link access or turn off the one-time link.
FOLDER_TOO_LARGE413The folder has more than 20 MB of text. Choose a smaller folder or exclude files in .gitignore.
FOLDER_TOO_MANY_FILES413Up to 2,000 files can be shared. Choose a smaller folder or exclude files in .gitignore.
UPLOAD_TOO_LARGE413The file exceeds the size limit.
FOLDER_INFECTED422The antivirus found a threat in the folder, so it was not uploaded.

Answers of any method with a key (an invalid or expired key, a missing permission, too many requests) are in Errors.

Share a folder of code: POST /api/v1/folders · lacuna API