Skip to content

Writing

Making changes is where the API asks the most of a client. Read this page once before you write anything.

What has to allow a change. Every change needs its scope, a person who may make it, an app whose administrator has allowed it, and a share where it’s allowed. See what decides whether a change is allowed. An app that hasn’t been given the right gets 403 write-not-allowed; an app asking about a share outside its policy gets the same 404 not-found as a share that doesn’t exist.

Nothing is ever replaced silently. A create, a copy or a move that finds the name taken doesn’t overwrite: the default is to refuse (on_conflict: "fail", answering 409 conflict, or 412 for an upload), and on_conflict: "rename" picks the first free name (“Q3 copy.xlsx”, “2027 copy”), so always read the path in the answer to see what name the item got.

Read, then change. Changes to something that exists need the ETag you last read (If-Match). Read the item first (getItem, or the answer to your last change), and send its ETag with the change. If it’s stale you get 412: read again and decide again, rather than forcing it. See ETags.

Retry safely. The calls that create or remove things take an Idempotency-Key, so a retry after a timeout can’t do it twice. See Idempotency keys.

Things Foldr refuses whatever the app is allowed. An item someone else has checked out is 423 checked-out. An item under a legal hold, or a folder holding one, is 403 legal-hold for rename, overwrite, move, restore and delete. A published file (one published as an approved version) is 409 published and is never moved or deleted by an app. The document of a signing still out is 409 envelope-source. Show these to your user as “not now” or “not here” answers, not as faults.

The parent folder must exist. A create or a move into a folder that isn’t there is 404 not-found. Create the folders first.

What you wantOperationScopeNeedsAnswers
Make a foldercreateFolderfiles.writeIdempotency-Key201
Upload a new fileuploadFilefiles.writeIf-None-Match: *, Idempotency-Key, Content-Length201
Upload straight to the storagecreateUploadSession, completeUploadSessionfiles.writenone201, 201
RenamerenameItemfiles.writeIf-Match200
CopycopyItemfiles.writeIdempotency-Key201, or 202 with an operation
Replace a file’s contentsoverwriteFilefiles.overwriteIf-Match, Content-Length200
Put an earlier version backrestoreVersionfiles.overwriteIf-Match, Idempotency-Key200 (or 202, held)
Move (within a share or to another)moveItemfiles.moveIf-Match, Idempotency-Key200, or 202 with an operation
DeletedeleteItemfiles.deleteIf-Match, Idempotency-Key200 (or 202, held)
Set a custom fieldsetFieldValuefields.writeIf-Match200
Ask to sharecreateShareRequestsharingIdempotency-Key202

Every one of these can also answer the errors common to all calls (401, 403 insufficient-scope, 403 write-not-allowed, 429, 500, 503) and, when your app has a daily limit, 429 daily-write-limit (or destructive-budget). The sections below list the ones particular to each.

Terminal window
curl -s -X POST "$FOLDR/api/v1/shares/$SHARE/folders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"path": "/Reports/2027"}'

201 Created, with the folder as an item (its canonical path), its ETag, and a Location header that reads it back.

  • path is the new folder’s full path, its name last. The folder it goes in must exist (404 if not). on_conflict is fail (the default, 409 conflict) or rename.
  • Also: 409 not-a-folder if the parent is a file; 403 write-not-allowed; 413 too-large.

An upload is one PUT whose body is the file, at any size. The file streams to the share as it arrives.

Terminal window
curl -s -X PUT "$FOLDR/api/v1/shares/$SHARE/files?path=%2FReports%2F2027%2FQ1.xlsx" \
-H "Authorization: Bearer $TOKEN" \
-H "If-None-Match: *" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/octet-stream" \
--data-binary @Q1.xlsx
{
"share_id": "01a11dce-14f0-7f23-b857-c621356097a0",
"path": "/Reports/2027/Q1.xlsx",
"name": "Q1.xlsx",
"kind": "file",
"size": 48213,
"modified": "2027-04-02T10:15:09Z",
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"export": null,
"etag": "W/\"48213-1806660909\"",
"read_only": false,
"checked_out": null
}
  • If-None-Match: * is required. It’s HTTP’s way of saying “create this, never replace anything”. Without it: 428 precondition-required. If something already has the name: 412 precondition-failed. With on_conflict=rename Foldr takes the next free name instead, so read the answer’s path (and Location).
  • Content-Length is required, so that a share without room refuses (413 too-large) before the first byte. A chunked request without a length is 400 invalid-request. Most HTTP libraries set it when you give them a file or a byte string; if yours streams from a generator, give it the length.
  • If-Match is refused (400): this operation only creates. To replace a file use overwriteFile.
  • A person can have only a few uploads running at once through your app (4). More than that: 429 rate-limited with Retry-After: 10. Queue your uploads.
  • The Idempotency-Key also matches the bytes: the same key with other bytes is 422 idempotency-key-reused.
  • Foldr doesn’t limit the size of an upload. Quotas and the storage’s own limits do.
  • If an upload is cut off (your connection dropped, or the server restarted), check with getItem whether anything is at the name before you retry. On some storage a partial file can be left behind, and a retry with the default on_conflict then answers 412. Where the same Idempotency-Key is still held, the retry answers 409 conflict until it frees (about two minutes after the last progress); then it runs.

Errors particular to it: 404 (the folder isn’t there), 409 not-a-folder, 409 conflict (a request with this key, or the same name, is being handled right now), 412, 413, 422, 428.

Upload straight to the storage: upload sessions

Section titled “Upload straight to the storage: upload sessions”

On a share whose storage supports it (Amazon S3 and compatible storage, Microsoft 365, and Dropbox for files up to 150 MB), the bytes can go from your app straight to the storage, which spares the double trip through Foldr. Use it for large files and for slow or flaky connections where the storage can resume. Where the share doesn’t offer it, createUploadSession answers 409 no-direct-upload: use uploadFile instead.

1. Open a session.

Terminal window
curl -s -X POST "$FOLDR/api/v1/uploads" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"share_id": "01a11dce-...", "path": "/Reports/2027/Big.mov", "size": 734003200,
"content_type": "video/quicktime"}'

size is the file’s exact size in bytes; completing the session checks it. As with every create, nothing is replaced: on_conflict is fail or rename.

{
"id": "u_8f2c...",
"mode": "direct",
"share_id": "01a11dce-...",
"path": "/Reports/2027/Big.mov",
"size": 734003200,
"url": "https://storage.example.net/...",
"method": "PUT",
"headers": { "Content-Type": "video/quicktime" },
"chunk_size": null,
"expires_at": "2027-04-02T10:30:09Z"
}

2. Send the file to the storage, to url, with the method and exactly the headers the session gave you. The url is a credential for this one upload: send it no Foldr token, and don’t log or share it.

  • chunk_size null: send the whole file in one request.
  • chunk_size set: send the file in pieces of exactly that many bytes (the last may be shorter), each with a Content-Range header (Content-Range: bytes 0-3276799/734003200). Microsoft 365 uses multiples of 320 KiB. Every piece but the last is answered 202; the last is answered 200 or 201.
  • Do it before expires_at; after that the URL stops working.

3. Complete the session, so Foldr checks the file is there and announces it:

Terminal window
curl -s -X POST "$FOLDR/api/v1/uploads/$SESSION_ID/complete" -H "Authorization: Bearer $TOKEN"

201, with the new item. The storage must hold exactly size bytes at the session’s path, or you get 409 upload-incomplete; the session stays open, so finish sending and complete again. Completing an already completed session answers the same file again, so it’s safe to retry.

Or cancel it: POST /uploads/{id}/cancel answers 204. It removes the empty placeholder some storage keeps for the upload, never a file with bytes in it. A completed session can’t be cancelled (409).

What happens to a session you forget. It’s kept until about an hour after its expires_at; one neither completed nor cancelled by then is gone (404), and a file sent to the storage for it is removed and never announced. The removal is a scheduled clean-up of files Foldr can show were written for that session; it leaves a file alone if it cannot, so a leftover is possible.

Sessions aren’t limited in number (no server process is held), but opening one costs 2 on your rate budget, as does completing it.

Terminal window
curl -s -X POST "$FOLDR/api/v1/shares/$SHARE/rename" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H 'If-Match: W/"48213-1806660909"' \
-d '{"path": "/Reports/2027/Q1.xlsx", "new_name": "Q1 final.xlsx"}'

200, with the item at its new canonical path (the old path names nothing afterwards, except as another spelling of a case-only rename on storage that ignores case). new_name is one name: no /, no control characters, no space at either end (400 invalid-path if not). It stays in the same folder; to put something in another folder use moveItem.

Errors: 428, 412, 409 conflict (the name is taken; nothing is replaced), 423 checked-out, 403 legal-hold. A rename needs only files.write.

Terminal window
curl -s -X POST "$FOLDR/api/v1/shares/$SHARE/copy" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"path": "/Reports/2027/Q1.xlsx",
"to_share": "01a11dce-...", "to_path": "/Archive/Q1.xlsx", "on_conflict": "rename"}'

Copies a file or a folder with everything in it, within its share or into another of the person’s shares (to_share is this share’s id for a copy within it). The original isn’t touched. to_path is the copy’s full path, its name last, and the folder it goes in must exist. A folder copy is a new folder, never merged into one that’s already there.

  • A small copy happens in the request: 201 with the copy, its ETag and a Location.
  • A big or slow one happens afterwards: 202 with an operation to poll. That’s a folder, anything with Microsoft 365 at either end, a file over 64 MiB going between shares (or over 1 GiB within one share, deployment defaults an administrator can change), and any copy you send with Prefer: respond-async. If the Prefer header is why you got an operation, the answer carries Preference-Applied: respond-async.
  • Limits: a folder copy takes at most 10,000 files and folders, nested at most 64 deep (413 copy-too-large); a person may have at most 20 copies waiting through one app (429 rate-limited), and two run at a time (two at a time across the whole site as well).
  • A copy into another share is refused (403 download-blocked) when a label on the file stops it being downloaded.
  • Both shares have to be ones your app may change.
  • Errors: 409 conflict, 409 not-a-file/not-a-folder, 400 copy-into-itself, 409 copy-incomplete and 503 copy-interrupted (from an operation), 422, 413.

Replace a file’s contents: overwriteFile

Section titled “Replace a file’s contents: overwriteFile”

Needs the separate scope files.overwrite. The body is the new contents, streamed to the share, at any size:

Terminal window
curl -s -X PUT "$FOLDR/api/v1/shares/$SHARE/content?path=%2FReports%2F2027%2FQ1.xlsx" \
-H "Authorization: Bearer $TOKEN" \
-H 'If-Match: W/"48213-1806660909"' \
-H "Content-Type: application/octet-stream" \
--data-binary @Q1-revised.xlsx

200, with the file and its new ETag.

  • If-Match is required and must name the version you mean: 428 without it, 412 if it’s stale, 400 for *.
  • Content-Length is required. A body that ends before its Content-Length leaves the file exactly as it was.
  • Refused for a file someone else has checked out (423), one under a legal hold (403 legal-hold), the document of a signing still out (409 envelope-source), and, for a person who may only read the approved version of a published file, that file (403 write-not-allowed).
  • Where the share’s storage keeps no earlier versions, the old contents can’t be got back, and an administrator may have refused apps this change on that share: 409 not-recoverable-here. See Recoverability.
  • If the organisation requires approval for the app’s changes, overwrite isn’t offered at all: 403 needs-approval-not-available. New contents can’t be held for approval.
  • Counts against the app’s daily allowance for removing and replacing.

Put an earlier version back: restoreVersion

Section titled “Put an earlier version back: restoreVersion”

Also files.overwrite. Take a version id from listVersions:

Terminal window
curl -s -X POST "$FOLDR/api/v1/shares/$SHARE/versions/restore" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H 'If-Match: W/"48213-1806660909"' -H "Idempotency-Key: $(uuidgen)" \
-d '{"path": "/Reports/2027/Q1.xlsx", "version_id": "mBLNn8a.dZS0QpWuxkmW3J_FT."}'

200, with the file and its new ETag. On Foldr’s own hosted storage the version is copied over the file inside the storage; on the other storage that keeps versions (S3, Azure, SharePoint, an SMB server’s shadow copies) Foldr is given that version’s bytes and proves them whole before they replace the file, so a restore that fails leaves the file as it was. Refused as replacing a file is. In approval mode it’s held for an approver: 202 with an operation.

Needs files.move. Moves a file or a folder (with everything in it) to another folder, keeping its name or giving it a new one.

Terminal window
curl -s -X POST "$FOLDR/api/v1/shares/$SHARE/move" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H 'If-Match: W/"48213-1806660909"' -H "Idempotency-Key: $(uuidgen)" \
-d '{"path": "/Reports/2027/Q1.xlsx", "to_path": "/Archive/2027/Q1.xlsx"}'

200, with the item at its new canonical path. to_path is the full new path, its name last (the same name or a new one); the folder it goes in must exist. Nothing already there is replaced (on_conflict fail or rename).

  • Within a share, the storage moves it in one step, always in the request.
  • To another share (add "to_share": "<other share id>"; both shares must be ones the app may remove from and change), the item is copied, the copy proven whole, and only then is the original removed, so nothing can be lost. A small file is moved in the request (200). A folder, anything with Microsoft 365 at either end, a file over 64 MiB, or any such move sent with Prefer: respond-async is moved afterwards: 202 with an operation of kind move. A file whose label keeps it in its share can’t be moved out: 403 download-blocked.
  • 409 source-remains: the copy is whole but an original couldn’t be removed. The item is in both places, nothing is lost, and the answer says where. Check the copy, then delete what’s left; don’t move it again. See Operations.
  • A folder of more than 1,000 items is 409 folder-too-large; a folder moved into itself is 400 move-into-itself; a published file is 409 published.
  • Not asked of a move: whether the storage could undo it. Moves are allowed even where an administrator has refused unrecoverable changes, because moving something back undoes a move.
  • A folder counts every item in it against the daily allowance.

Needs files.delete.

Terminal window
curl -s -X DELETE "$FOLDR/api/v1/shares/$SHARE/items?path=%2FReports%2F2027%2FQ1.xlsx" \
-H "Authorization: Bearer $TOKEN" \
-H 'If-Match: W/"48213-1806660909"' -H "Idempotency-Key: $(uuidgen)"
{
"deleted": { "share_id": "01a11dce-...", "path": "/Reports/2027/Q1.xlsx", "name": "Q1.xlsx", "kind": "file",
"size": 48213, "modified": "2027-04-02T10:15:09Z", "mime_type": "...", "export": null,
"etag": "W/\"48213-1806660909\"", "read_only": false, "checked_out": null },
"items": 1,
"recoverable": "snapshot",
"destination": "shadow-copies-if-enabled",
"where": "the server's shadow copies, if it keeps them"
}

200, saying what was deleted and where its storage keeps it now, so you can tell your user whether it can be brought back (see Recoverability): recoverable is bin, snapshot or none; destination is a stable name; where is the same in words for a person.

  • A folder goes with everything in it, up to 1,000 items (409 folder-too-large beyond that). items says how many files and folders went. The top of a share can’t be deleted.
  • Refused for anything someone else has checked out (423), under a legal hold (403), published (409 published), or the document of a signing still out (409 envelope-source).
  • Where the storage has no recycle bin, an administrator may have refused apps deletions there: 409 not-recoverable-here.
  • Counts items against the daily allowance (429 destructive-budget).
  • In approval mode, a delete isn’t made at once: 202 with an operation held for an approver (see Operations and approval).
  • Repeating a delete with the same Idempotency-Key returns the first answer. Repeating it with a new key after the item has gone is 404.

Setting a custom field value: setFieldValue

Section titled “Setting a custom field value: setFieldValue”

Needs fields.write. The person must be allowed to set that field, a field with allowed values takes one of them, and a value must suit the field’s type.

Terminal window
curl -s -X PUT "$FOLDR/api/v1/shares/$SHARE/field-values/$FIELD_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H 'If-Match: "fv-4d1c..."' \
-d '{"path": "/Reports/2027/Q1.xlsx", "value": "Approved"}'

200, with every value on the file that the person can see (as getFieldValues does) and the new ETag. If-Match is the ETag of the file’s field values as you last read them from getFieldValues: 428 without one, 412 if anything changed since. Send * for “whatever they are now”.

value is a string, a number, or true or false, as the field’s type takes it (a date as 2027-01-31). 422 validation-failed if it doesn’t suit. Also 423 checked-out, 409 not-a-file. A field whose change the organisation holds for an approver is held as it is in Foldr; read the values again to see where it stands.

Needs sharing. An app can’t share anything by itself. It can ask, and every request waits for an approver in Foldr. The answer never carries a link.

Terminal window
curl -s -X POST "$FOLDR/api/v1/shares/$SHARE/share-requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"path": "/Reports/2027/Q1.xlsx", "kind": "secure_link",
"recipient_emails": ["[email protected]"], "expiry_days": 14,
"message": "The Q1 figures"}'
{
"id": "6f1a...",
"share_id": "01a11dce-...",
"path": "/Reports/2027/Q1.xlsx",
"kind": "secure_link",
"status": "pending_approval",
"created_at": "2027-04-02T10:20:00Z",
"decided_at": null
}

202. The kind says what is asked for:

kindAsks forAlso send
peopleAccess for people in the organisationpeople (email addresses, usernames or exact group names, at most 20), permission (read, the default, or write)
public_linkA link anyone who has it can open
secure_linkA link for outside email addressesrecipient_emails (at most 20)
inboxAn email address files can be sent to (a folder)

expiry_days is the days until access ends once approved (0 or absent: never; for a link, -1 ends it after its first download); message is a note shown with the link, up to 1,000 characters.

  • Poll the outcome with getShareRequest (GET /share-requests/{id}) or list your own with listShareRequests (GET /share-requests, paged as in Conventions). status is pending_approval, then approved, declined (by an approver, or left undecided until it lapsed) or withdrawn. Once approved, the sharing exists in Foldr, where the person finds the link; the API never gives it to the app.
  • 409 no-approver if nobody other than the person could approve it. 409 already-shared if the person already shares the item or has a request for it waiting. 422 validation-failed for a recipient who can’t be found or a term that isn’t allowed.
  • You can read only the person’s own requests, made through any app; someone else’s is the same 404 as one that doesn’t exist.