Uploads
Anchorify has two multi-file upload surfaces: the web upload page and the CLI upload-folder subcommand. Both let an admin publish a batch of files to a single project under one access setting. For a single file — text or binary — anchorify <file> and the upload API cover it.
Web — /<org>/<project>/upload#
Admin-only. The page is a file picker (<input type=file multiple>) plus a Who can access select and a List publicly checkbox that apply to the whole batch. Submit POSTs the multipart form to the same path.
Behavior per file:
- Size limit: 1 MiB per file on Free (
MAX_SHARE_BYTES), 10 MiB on Pro (MAX_SHARE_BYTES_PRO). Oversize files are rejected with a per-row error; the rest of the batch continues. - Slug: kebab-cased basename.
Q1 Report.md→q1-report. Slug collisions auto-suffix-2,-3, … up to 50 attempts. - Content type: filename extension (see content types).
- Binary detection: a NUL byte in the first 1KB routes the file to the binary path — bytes go to R2 storage under
shares/<share_id>/<filename>and the share's content column gets the sentinel"@@blob". The content type isimage,pdf, orbinarybased on MIME + extension.
The success page lists one row per file with a status (✓ created, ✓ created as <slug> for auto-renames, ✗ <error> for failures) and the new URL.
CLI — anchorify upload-folder <dir>#
Recursive: walks the directory tree, flattens nested paths into slugs (docs/intro.md → docs-intro), and publishes each file — text files via POST /, binaries via POST /api/v1/shares/upload.
anchorify upload-folder ./drafts
anchorify upload-folder ./drafts --project q1-reports
anchorify upload-folder ./drafts --access restricted
anchorify upload-folder ./drafts --dry-run # list slugs, no POST
Flags:
--project <slug>— required when the org has 2+ projects. Without it, the server returns a 400 with the list of projects + the exact retry command.--access <tier>—restricted,view,comment, orsuggest; applies to every file in the batch. Defaultcomment.--index/--no-index— public listing for the whole batch. Default--no-index.--dry-run— prints<slug>\t<rel-path>per file and exits without hitting the server.
Skipped at walk time: dotfiles (.git, .DS_Store) and node_modules.
Binaries publish alongside text ([F-CLI-BINARY]) — a folder of markdown, PDFs and screenshots goes up in one pass. Each file is read as bytes and probed: a NUL anywhere routes it to the blob tier, everything else publishes as text. Re-running upload-folder over the same directory is idempotent for both: every file, text or binary, updates the share already at its slug rather than erroring ([F-CLI-BINARY-UPDATE]).
Exit code is 1 if any file errored, 0 otherwise. The first error is printed; the loop continues so a single broken file doesn't sink the batch.
CLI — a single file#
anchorify <file> publishes one file and prints its URL. Binaries take the same route as the folder walk:
anchorify ./q1-deck.pdf --project q1-acme
anchorify ./architecture.png --slug system-diagram
Three things differ from a text publish:
--typeis ignored. The tier (image/pdf/binary) comes from the bytes and the filename, not from a flag. The CLI prints a note if you pass one.--passwordis refused. Publish first, thenanchorify password set <slug>.- Re-publishing replaces in place. The CLI remembers the file→share mapping, so a second
anchorify ./q1-deck.pdfswaps the bytes of the share you already published — same id, same slug, same URL, so the link in the client's inbox keeps working. Use--newwhen you want a separate copy instead. The same swap is available in the browser — see replacing a share's file.
Web — replacing a share's file#
Open the share's Share settings page (⋯ → Share settings, or the Replace file… entry in the share page's ⋮ menu) and use the File block. Pick a file, submit, and the bytes swap in place: same id, same slug, same URL, same comments, same access — the link you already sent keeps working.
The in-browser editor is text-only and always will be; there is nothing in a PDF for a textarea to edit. The file input is what replaces it.
Two things the page tells you, and they are both true:
- The old file is deleted once the new one is in place.
- Replacing is not versioned.
share_versionsstores content, and a file-backed share's content column holds a pointer, not content — so there is no previous version to roll back to. Keep your own copy.
Uploading a text file to a file-backed share converts it back to a text share (and the editor starts applying again); uploading a binary to a text share does the reverse.
API — POST /api/v1/shares/upload#
The multipart publish endpoint that both CLI paths call. Auth is a per-user token or a cookie session; the legacy admin bearer is rejected (it has no home org). Accepts file, plus optional filename, slug, access, indexed, type, org, project.
curl -sX POST https://anchorify.io/api/v1/shares/upload \
-H "Authorization: Bearer $ANCHORIFY_TOKEN" \
-F "file=@./q1-deck.pdf" \
-F "slug=q1-deck"
The stored MIME is sniffed from the bytes, never taken from the request — a PNG declared as text/html is still stored and served as image/png, and an HTML payload is served as an attachment. Full field list, response shape and error table: API reference.
MCP — anchorify_publish_file#
Agents get the same capability through the MCP server: anchorify_publish_file takes a local path, reads the bytes, and uploads them. Use it instead of anchorify_publish whenever the file isn't text — anchorify_publish carries content as a JSON string and can't represent a PDF.
Moving shares between projects#
A share can move to a different project in the same org without re-uploading. Old URLs 301 to the new canonical URL via the K4 redirect chain.
- Share page: the
⋮menu in its header → Move to… → pick a project. - Dashboard or project page: open the share's actions menu (
⋯) → Move to… → pick a project. - CLI:
anchorify move <slug-or-id> --to <project>. - API:
POST /api/v1/shares/<id>/move {project_id|project_slug}.
Moving is gated on share.update, which is wider than most per-share admin actions: org admins, project editors and share-level editors can all do it, on every one of those surfaces.
Cross-org moves are blocked — use invites to bring the recipient into your org first.