Conventions
The rules that hold across the whole API. Most of the surprises people meet are on this page.
Requests and responses
Section titled “Requests and responses”- Send the token on every call:
Authorization: Bearer <access token>. There’s no other way to authenticate. A browser’s Foldr cookie is not accepted and gets the same401as no token. - JSON bodies are sent as
Content-Type: application/jsonand are limited to 1 MB. File contents are the exception: an upload or overwrite body is the file’s raw bytes (application/octet-stream), whatever its size. - JSON answers come as
application/json. Every error isapplication/problem+json(see Errors). - Shared caches never store a response. An answer carrying an
ETagisCache-Control: private, no-cache: the client’s own cache may keep it but must ask again withIf-None-Matchbefore using it. Every other answer isno-store. The OpenAPI document and the problem-type pages are the exception: public, and cached for a short while. - Ignore what you don’t know. New fields and new header values can appear within v1. Don’t fail on them.
- Shares, custom fields, share requests and operations have UUIDs (for example
01a11dce-14f0-7f23-b857-c621356097a0). They’re always UUIDs, on Foldr Cloud and on a Foldr server alike. Treat them as opaque strings anyway; compare them as text, in lower case. - Files and folders have no id. An item is its share’s id plus its path inside the share. If a file is renamed or moved, its path changes, and the response tells you the new one. (If Foldr ever adds stable item ids, an
idfield will be added to items; that’s an additive change.) - People are identified by
idin a few responses (the person who has a file checked out, the person who last set a field value). That’s their directory id, an opaque string. - Version ids and upload session ids are opaque strings. Never build one.
Files and folders are addressed by a share id and a path from the top of the share.
Where the path goes. The path never goes in the URL path. It’s a query parameter, ?path=, on reads, on the raw-bytes upload (uploadFile), on overwriteFile and on deleteItem, and a JSON "path" field on everything else. The reason is that proxies and frameworks quietly rewrite %2F, // and .. in URL paths, and file names can legally contain #, ? and %.
Encoding. Percent-encode the value of path as you would any query value, with your HTTP library’s own encoder: a name such as /Invoices/Q3 & Q4 #1 100%.txt has to travel as %2FInvoices%2FQ3%20%26%20Q4%20%231%20100%25.txt (a / may also be left as it is). With curl, let it do the work:
curl -s -G -H "Authorization: Bearer $TOKEN" \ "$FOLDR/api/v1/shares/$SHARE/item" \ --data-urlencode "path=/Invoices/Q3 & Q4 #1 100%.txt"Give a parameter only once. ?path=/a&path=/b is refused with 400 invalid-request, as is a list.
The forms you may send.
- The leading
/is optional in a request:Reports/Q3.xlsxand/Reports/Q3.xlsxmean the same. /, an emptypathor nopathat all is the top of the share, where the operation takes a folder. A path made only of spaces is refused (400 invalid-path); it’s never quietly read as the top.- Nothing is trimmed. A name that begins or ends with a space is addressed, and returned, with it.
- At most 4,096 bytes.
- Refused with
400 invalid-pathbefore anything is looked up: a..segment, a control character, a backslash, an absolute-URL or drive form (smb://x,C:\x,//server/x), a path of only spaces. A new name (a folder, a rename, a copy’s or upload’s name) is a single name: no/, no control characters, and no space at either end.
The form you get back. Every response gives the item’s canonical path: exactly as the storage holds it (its own upper and lower case), in Unicode NFC, always beginning with /. / alone is the top of the share. Keep that form and use it in later calls, in If-Match bookkeeping and in your own records. Two storage details:
- Case. Storage that ignores case (file servers, for one) finds an item whatever case you send and answers with the stored case. Storage that is case sensitive (S3, for one) matches exactly.
- Unicode. macOS tends to produce names in decomposed form (NFD). You can send either; Foldr converts the request to NFC. If a folder holds two names that differ only in how an accented letter is encoded, a path that could mean either is
409 ambiguous-path.
Names may be tidied. A name you supply for something new is made safe for the storage: characters the storage forbids are replaced, runs of spaces are collapsed, trailing dots and dashes are removed and a very long name is shortened. Reserved device names and format characters are refused. So always use the path in the answer, never the name you sent, for every create, copy, move and rename. A name that already exists is addressed as stored, even if a new name like it would have been changed.
After a change. A rename or move answers with the item at its new canonical path. The old path names nothing afterwards, except where the storage ignores case and the old spelling is just another way to write the new one (a rename that only changes case). The case in a canonical path comes from cached folder listings, so after a case-only change made outside Foldr it can be a minute out of date.
Conditional requests: ETags
Section titled “Conditional requests: ETags”Every item has an etag, and the same value comes in an ETag header when you read an item on its own, download it, or change it.
- An ETag is an opaque string. Send it back exactly as you received it, including any
W/and the quotation marks. It may be a weak tag (a hash of size and modified time) or the storage’s own tag; which depends on the storage. A weak tag is a best-effort change detector: two writes of the same size within the same second can share one, and a folder’s tag follows the folder’s own timestamp, not its contents. Where you need to be sure, read the item on its own withgetItemfirst and send the tag it gives. A listing may carry one form and a read of the item on its own another: either is accepted when you send it back asIf-Match. - Reading with
If-None-Match. Send an item’s ETag asIf-None-MatchtogetItem,getContentorgetFieldValues. If nothing has changed, you get304with no body, and the call costs 1 on your rate budget whatever the read would have cost. (If-None-Matchcompares weakly, as HTTP says it should. A list of tags, or*, is accepted.) - Changing with
If-Match. A call that changes something that already exists says which version it means withIf-Match: <etag>. The operations that need it arerenameItem,overwriteFile,setFieldValue,moveItem,deleteItemandrestoreVersion.- Without it:
428 precondition-required. - With a tag that no longer matches:
412 precondition-failed. Someone changed the item since you read it. Read it again, decide again, and don’t just retry. - For overwrite, move, delete and restore,
If-Match: *is refused (400 invalid-request): name the version you mean. (setFieldValueaccepts*, meaning “whatever the values are now”.) - A list of tags matches if any member does.
- Without it:
- Creating with
If-None-Match: *.uploadFilecreates a file and never replaces one, and says so in HTTP’s own words:If-None-Match: *is required (428without it). If the name is taken:412 precondition-failed. - A write’s answer carries the item’s new ETag. Use it for the next change instead of reading again.
- A search hit comes from the search index, so its
etagandchecked_outarenull.getItemgives the live ones. - The ETag of a file’s field values is a different tag, over the whole set of values; it’s used with
getFieldValuesandsetFieldValueonly.
Idempotency keys
Section titled “Idempotency keys”Networks drop answers. If you create something and never see the reply, you can’t tell whether it happened. An Idempotency-Key lets you send the same request again safely.
Which calls take one. All of these require it (without one: 400 invalid-request, and nothing runs): createFolder, uploadFile, copyItem, moveItem, deleteItem, restoreVersion and createShareRequest. The others don’t take one, because If-Match already protects them (renameItem, overwriteFile, setFieldValue) or they’re safe to repeat.
The key. A value of your own, 1 to 255 visible ASCII characters (a UUID is ideal), new for every new request. Keys belong to your app and the person it acts for, so nobody else’s keys can collide with yours.
What the server does.
| You send | You get |
|---|---|
| A key it hasn’t seen | The request runs, once. A successful (2xx) answer is kept for 24 hours. |
| The same key and the same request again, within 24 hours | The first answer again, with the header Idempotent-Replayed: true. Nothing is done a second time, and a repeat doesn’t use up your daily limit. |
| The same key with a different request (another path, another query, other bytes, another body) | 422 idempotency-key-reused. |
| The same key while the first request is still being handled | 409 conflict. Wait a moment and send it again. |
| The same key after the first request was cut off part way (a server restarting, say) | The key, and any name it was writing, are free about two minutes after the request last made progress; send the same request again and it runs. |
A key whose first request was refused or failed (any answer that isn’t 2xx) | It runs again: a refused request changed nothing, so the key is free. |
The one refusal that is kept like a success is 409 source-remains for a move to another share, because that move did change things (the copy was made); sending it again tells you the same and doesn’t copy a second time.
An upload is matched on its bytes too: Foldr reads the repeated body to be sure it’s the same file. Other bytes of the same length under the same key are 422.
After 24 hours. The kept answer is forgotten.
- For a request that can become an operation (
copyItem,moveItem,deleteItemandrestoreVersion) the key still finds the operation: one still under way answers202again, and one that has finished answers what the request would have answered if it had made the change at once (201with the copy,200with the item or the deletion) or the problem it ended with, carrying the operation. - For
createFolder,uploadFileandcreateShareRequestthe key is simply forgotten and the request runs as a new one. Nothing is ever replaced, so the usual result is409 conflictor412 precondition-failedbecause it already exists; but withon_conflict=renameyou would get a second copy under a new name. So retry soon, within the day, and use the defaulton_conflict(fail) when you rely on a key.
Pagination
Section titled “Pagination”Collections come as a page:
{ "items": [ ... ], "next_cursor": "opaque-string-or-null", "truncated": false }- Ask for the next page by sending
next_cursorback as?cursor=. Whennext_cursorisnullthere are no more. - A cursor is opaque. Never build one or change one, and don’t keep one for later; its form can change within v1. Send it back with the same other parameters (the same
path, the same search). limitsets the page size, from 1. Alimitover the largest allowed is read as the largest;0, or something that isn’t a whole number, is refused (400 invalid-request).
| Collection | Default limit | Largest |
|---|---|---|
listShares | 100 | 200 |
listChildren | 100 | 200 |
search | 20 | 50 |
listOperations | 50 | 200 |
listShareRequests | 50 | 200 |
listVersionsputs all of a file’s versions on one page.listFieldsandgetFieldValuesreturn a plain array with no paging.- A cursor that isn’t one Foldr issued for that collection is
400 invalid-cursor. A listing cursor whose snapshot has gone is410 cursor-expired: start again from the first page. - Folder listings are a snapshot. The first page takes a snapshot of the folder and keeps it for a minute while you page through it. A folder that changes in the meantime shows the snapshot, so don’t use a listing to decide whether something exists now; use
getItem. - Very big folders. One listing reads at most 25,000 entries. A folder with more is paged as its first 25,000, and every page of it says
"truncated": true. Anullnext_cursoron a truncated listing is therefore not “the whole folder”.truncatedis always present andfalseotherwise. - Search pages walk the person’s search indexes one after another, so a page can hold fewer hits than
limitand still not be the last.total_estimateis only an estimate.
Timestamps
Section titled “Timestamps”RFC 3339, in UTC, to the second: 2026-10-08T23:17:11Z. A timestamp can be null where the storage doesn’t report one (modified on some storage). Daily limits reset at 00:00 UTC.
Rate limits
Section titled “Rate limits”Every call takes some tokens out of a bucket that refills steadily. There are two buckets, and a call is charged to both or to neither:
- your app’s budget for each person, by default 120 tokens refilling at 2 a second;
- your app’s budget across the organisation, by default 1,200 tokens refilling at 20 a second.
So you can burst, and then sustain a rate. These numbers are defaults, and a Foldr server’s administrator can change them.
What a call costs. Each operation’s x-foldr-rate-cost in the OpenAPI document says:
| Cost | Operations |
|---|---|
| 1 | getMe, listShares, getShare, listChildren, getItem, listFields, getFieldValues, listOperations, getOperation, listShareRequests, getShareRequest |
| 2 | getDownloadUrl, listVersions, createFolder, renameItem, copyItem, setFieldValue, createShareRequest, cancelOperation, createUploadSession, completeUploadSession, cancelUploadSession |
| 3 | search |
| 5 | getContent, uploadFile, overwriteFile, moveItem, deleteItem, restoreVersion |
A HEAD costs 1 whatever its GET costs, and so does a 304.
Headers. Every answer, a success or a refusal, carries X-RateLimit-Limit (the size of the bucket that would run out first for you) and X-RateLimit-Remaining (what’s left in it). Use them to slow down background work before you’re refused.
When you’re over budget: 429 with a problem document of type rate-limited and a Retry-After header, in seconds.
HTTP/2 429Retry-After: 3X-RateLimit-Limit: 120X-RateLimit-Remaining: 0Content-Type: application/problem+jsonWait at least that long. If several requests are waiting, don’t send them all at once afterwards; space them out, and back off further on repeated 429s. Treat 429 as flow control, not as a failure to show a person.
Other limits that answer 429:
- By default at most 4 uploads in flight at once for each person through your app (
uploadFilestreams the file through a server process for as long as it takes):rate-limited, withRetry-After: 10. - At most 20 copies waiting for each person through your app, with two running at a time:
rate-limited. Two copies also run at a time across the whole site, so a copy can wait behind other people’s. - The daily limits an administrator sets for your app:
daily-write-limitfor writes,destructive-budgetfor removing and replacing.Retry-Aftersays how many seconds until the limit resets at midnight UTC. - A very heavy flood from one network address is stopped before it reaches the API, with an HTML
429and noRetry-After. A client that parses every429as JSON should check the content type first.
Every read (GET) also answers HEAD: the same status and headers as the GET, with no body, for a cost of 1. HEAD on getContent answers from the file’s details and doesn’t read the file. Writes don’t answer HEAD.
405 and 404
Section titled “405 and 404”- An address that exists but doesn’t answer that method is
405 method-not-allowedwith anAllowheader naming the methods it does answer. For example,PATCHon a file’s content answersAllow: GET, HEAD, PUT.OPTIONSis answered405everywhere: the API sends no CORS headers, because it’s meant to be called from servers. - An address that isn’t the API’s is
404 not-found. - Without a valid token, both of these are the
401first. You find out nothing about the API’s shape before you’ve signed in. 404 not-foundis deliberately blunt. It answers a share or path that doesn’t exist, one that exists but isn’t the person’s, one that a share’s path rules hide, and a share outside the shares your app’s policy names. They look the same on purpose, so that an app can’t use the API to find out what it isn’t allowed to see. Don’t build logic that tries to tell them apart.- The exceptions are two true answers about the person’s own shares:
503 share-unreachable(a file server that’s down) and409 storage-account-not-connected(a cloud storage account of theirs that they haven’t yet connected in Foldr’s web app).