Operations
Some work doesn’t fit inside one request: copying a folder, moving a big file between shares, or a change that has to wait for a person to approve it. For these Foldr answers 202 Accepted with an operation, and does the work afterwards. Your app polls the operation until it’s finished.
When you get an operation
Section titled “When you get an operation”| You called | You get an operation when |
|---|---|
copyItem | The item is a folder, or anything with Microsoft 365 at either end, or a file over 64 MiB going between shares (1 GiB within one share; deployment defaults), or you sent Prefer: respond-async. |
moveItem | The move is to another share and the item is a folder, or has Microsoft 365 at either end, or is over 64 MiB, or you sent Prefer: respond-async. Or the app is in approval mode (every move). |
deleteItem, restoreVersion | Only in approval mode: the change waits for an approver. |
A move within one share, and every change in the other tables, never produce an operation (except in approval mode for moves, deletes and restores).
Prefer: respond-async (RFC 7240) asks for an operation whatever the size, for a copy, or for a move to another share. When the header is why you got one, the 202 carries Preference-Applied: respond-async. Without the header a small file is done in the request, so a client that always wants one code path can always send it.
The 202
Section titled “The 202”HTTP/2 202Location: https://files.example.com/api/v1/operations/2f7776a7-2d0a-4b90-b939-c111fadc9491Retry-After: 5Preference-Applied: respond-asyncContent-Type: application/json{ "id": "2f7776a7-2d0a-4b90-b939-c111fadc9491", "kind": "copy", "state": "queued", "phase": null, "created_at": "2027-04-02T10:17:29Z", "started_at": null, "finished_at": null, "source": { "share_id": "01a11dce-...", "path": "/Invoices", "kind": "folder" }, "target": { "share_id": "01a11dce-152f-...", "path": "/Invoices" }, "version_id": null, "progress": { "items_done": 0, "items_total": 4, "bytes_done": 0, "bytes_total": 56 }, "result": null, "deletion": null, "approval": null, "failures": [], "failures_truncated": false}Location is the address to poll, and Retry-After is how long to wait before the first poll (5 seconds; 60 for an operation waiting for approval, which takes hours).
Polling: getOperation
Section titled “Polling: getOperation”GET the Location until it answers something other than 202. That is the whole protocol, and the one Power Automate and Logic Apps follow on their own.
curl -s -i -H "Authorization: Bearer $TOKEN" "$LOCATION"| The operation is | getOperation answers |
|---|---|
| queued, running or cancelling | 202 with the operation, Location and Retry-After. Ask again after that many seconds. |
held for an approver (pending_approval) | 202, with a longer Retry-After. |
| succeeded | 200 with the operation, whose result is what it made. Content-Location names the item (absent for a delete). |
| failed | The problem it ended with, at that problem’s own status, carrying the operation in the problem’s operation member. For example 409 copy-incomplete, 409 source-remains, 403 download-blocked, 503 copy-interrupted. |
| cancelled | 409 operation-cancelled, carrying the operation. |
| declined, expired | 403 change-declined, 409 change-expired (see below). |
So: a 2xx is a state of play, and anything else is the end, with the reason in the problem. A well-behaved loop:
loop: r = GET location if r.status == 202: sleep(Retry-After); continue if r.status == 200: done, r.json.result is the item else: it ended as r.json.type saysPoll no faster than Retry-After. Each poll costs 1.
Fields of an operation
Section titled “Fields of an operation”| Field | |
|---|---|
id | UUID. |
kind | copy, move, delete or restore. (Open list.) A delete or restore is only ever an operation held for approval. |
state | pending_approval, queued, running, cancelling, succeeded, failed, cancelled, declined or expired. |
phase | For a move: copying (everything is copied first; nothing has been removed, and a cancel removes the partial copy) then deleting (the copy is whole and proven, and the originals are being removed: too late to cancel). null for anything else. |
created_at, started_at, finished_at | RFC 3339 UTC; started_at is null while queued, finished_at until it ends. |
source | What’s being copied, moved, deleted or restored: share id, path, kind. |
target | Where it was asked to go: the path as you sent it. result.path is where it landed (on_conflict: "rename" may have given it another name). null for a delete or a restore. |
version_id | For a restore, the version asked for; otherwise null. |
progress | items_done and bytes_done, and items_total and bytes_total, which are null until Foldr has counted what there is to do. Nothing is guessed. |
result | Once it has succeeded, what it made (the copy, the moved item, the restored file), as an item. null until then and for a delete. A move that ended source-remains also has it: the whole copy it made. |
deletion | For a delete held for approval, once it has been made: the same answer deleteItem gives. |
approval | For an operation held for an approver: how its approval stands (below). |
failures | At most 100 items (see failures_truncated): for a copy that ended copy-incomplete, the items it couldn’t copy; for a move that ended source-remains, the items still at the source. Each is { "path", "code" } where code is a problem slug (download-blocked, invalid-path, not-found, …). |
failures_truncated | true when more items failed than failures lists. |
Finding an operation again: listOperations
Section titled “Finding an operation again: listOperations”If your app loses an operation’s id (a restart, a crash), list them:
curl -s -H "Authorization: Bearer $TOKEN" "$FOLDR/api/v1/operations?limit=20"The person’s operations through your app, newest first, as a page, including finished ones for 7 days after they finish. After that an operation is gone (404).
An operation is yours alone: the person, through your app. Another person’s or another app’s operation is the same 404 as one that doesn’t exist. And it’s read under the scope of the change that started it: a copy with files.write, a move with files.move, a held delete with files.delete, a held restore with files.overwrite. A token that has one of the four scopes but not that one sees 404, not 403. In practice an app that makes the change has the scope to watch it.
Cancelling: cancelOperation
Section titled “Cancelling: cancelOperation”curl -s -X POST -H "Authorization: Bearer $TOKEN" "$FOLDR/api/v1/operations/$ID/cancel"204 either way. What happens next depends on where it was:
- Queued: cancelled at once.
- Running (or a move still
copying): it stops at the next item, and the copy it had made so far is removed. A move’s original is untouched. - A move already
deletingits originals: stopping would leave the item split between two places, so it finishes. A copy whose last item is done as the cancel arrives also succeeds. - Held for an approver: withdrawn at once, nothing changed. Its
approval.statusthen sayswithdrawn.
Poll afterwards to see how it ended (409 operation-cancelled). Cancelling something that has already finished is 409 operation-finished; read it for how it ended.
Moves and source-remains
Section titled “Moves and source-remains”A move to another share is made safe by order: every item is copied first, the copy is checked whole, and only then are the originals removed. So the worst case is that an item is in both places, never in neither.
If the copy is whole but an original couldn’t be removed afterwards, the move ends 409 source-remains:
- the item is in both places, and nothing is lost;
- the operation’s
resultis the whole copy, and itsfailureslist what is still at the original place; - what to do: check the copy, then delete what’s left at the original place yourself (with
deleteItem). Moving it again would make a second copy.
A synchronous move (a small file in the request) can end the same way: the 409 answer says where the item is, and is kept for its Idempotency-Key, so a retry is told the same and doesn’t copy again.
Idempotent retries
Section titled “Idempotent retries”Send an Idempotency-Key with copyItem and moveItem and a retry after a lost 202 gives you the same operation back (with Idempotent-Replayed: true) and starts nothing new. After the 24 hours the key is kept, a retry still finds the operation: one still under way answers 202 again, and one that has finished answers as the original request would have if it had made the change at once, or with the problem it ended with, carrying the operation. If you lose the id altogether, listOperations finds it.
What can stop an operation
Section titled “What can stop an operation”Everything that can refuse a request can also end an operation, because Foldr checks again when it does the work. The common ones: 409 conflict (the name was taken in the meantime), 403 download-blocked (a label keeps the file in its share), 409 copy-incomplete, 503 copy-interrupted (the server stopped the work too often, or it waited too long to start, a day), 503 write-failed, 503 share-unreachable. Operations that were never started, or whose worker was lost, are restarted or failed by Foldr; they’re never left waiting for ever.
Changes held for approval
Section titled “Changes held for approval”An organisation can require approval for an app’s changes that remove or replace. It is set per app, in Settings > Connected apps > Policy, as Approve each change first, and needs Reviews & approvals on the site. An app in approval mode works like this.
What a client sees
Section titled “What a client sees”A delete, a move or a version restore passes every check it would pass to be made at once (scope, permissions, policy, If-Match, holds, the daily allowance) and is not made. Instead it answers:
HTTP/2 202Location: https://files.example.com/api/v1/operations/9d3c...Retry-After: 60with an operation in state pending_approval:
{ "id": "9d3c...", "kind": "delete", "state": "pending_approval", "phase": null, "created_at": "2027-04-02T10:20:00Z", "started_at": null, "finished_at": null, "source": { "share_id": "01a11dce-...", "path": "/Reports/2027/Q1.xlsx", "kind": "file" }, "target": null, "version_id": null, "progress": { "items_done": 0, "items_total": null, "bytes_done": 0, "bytes_total": null }, "result": null, "deletion": null, "approval": { "status": "pending", "expires_at": "2027-04-16T10:20:00Z", "decided_at": null }, "failures": [], "failures_truncated": false}(An illustration of the shape: your values will differ.) Someone other than the person who asked is asked in Foldr to approve it. They are shown the app, the person, what will happen and where the item is going. Who approves, and where, is the organisation’s business and isn’t visible to the app.
How the app learns the outcome
Section titled “How the app learns the outcome”Poll the operation (getOperation), at the slow pace its Retry-After gives, a minute at a time, because approvals take hours or days. It ends in one of these ways:
| Outcome | getOperation answers | approval.status | State |
|---|---|---|---|
| Approved, and made | 200; for a delete the deletion says what was deleted and where it is now; for a move or restore result is the item | approved | succeeded |
| Declined by an approver | 403 change-declined | declined | declined |
| Nobody decided in time (14 days, unless the organisation says otherwise) | 409 change-expired | expired | expired |
| The app, or the person, withdrew it | 409 operation-cancelled | withdrawn | cancelled |
| Approved, but the item changed after you asked | 412 changed-since-requested | approved | failed |
| Approved, but the share can’t be opened as the person now | 409 requester-not-connected | approved | failed |
| Closed before anyone decided (for instance, the app was disconnected) | 409 operation-cancelled | closed or withdrawn | cancelled |
In every failing case in this table, nothing was changed. An approved change that then fails while it is being made is different: a move to another share can copy the item and then fail to remove the original, which ends as 409 source-remains with the copy as the result (see Moves and source-remains).
- Declined or expired: show it to your user as such. Ask again only if they want to.
changed-since-requested: an approved change is made as things stand then. YourIf-Matchis checked again at that moment; if the item has changed since, nothing is touched. Read the item again, and ask again if the change is still wanted.requester-not-connected: an approved change is only ever made as the person who asked, never with anyone else’s access. If their own sign-in to the storage isn’t available any more, or they can no longer reach the share, it can’t be made. Ask again.- Approval statuses are an open list:
pending,approved,declined,expired,withdrawn,closed.
Other things to know
Section titled “Other things to know”- The daily allowance is charged when you ask, not when it’s made, and isn’t given back if the change is declined, expires or is withdrawn. It bounds how much an app can ask approvers for. A request refused for another reason costs nothing.
- No approver, no request. If nobody other than the person could approve it (or the organisation has no approvals), the request is refused up front:
409 no-approver. Nothing is charged. - Replacing a file’s contents is not offered to such an app:
403 needs-approval-not-available. New contents can’t be held for approval. The person can replace the file in Foldr. - Rename, copy, upload and the rest are not held. Approval mode covers deletes, moves and version restores only, because those are the ones that remove or replace.
- Cancelling a held operation withdraws it at once (
cancelOperation,204). - If the person disconnects the app, or an administrator revokes it, everything of its still waiting is withdrawn and never made. The app’s tokens stop working at the same moment, so it can’t poll; if the person connects it again,
listOperationsshows those operations ended. - Operations are kept 7 days after they end, so a late poll still finds the answer.