Skip to content

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.

You calledYou get an operation when
copyItemThe 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.
moveItemThe 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, restoreVersionOnly 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.

HTTP/2 202
Location: https://files.example.com/api/v1/operations/2f7776a7-2d0a-4b90-b939-c111fadc9491
Retry-After: 5
Preference-Applied: respond-async
Content-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).

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.

Terminal window
curl -s -i -H "Authorization: Bearer $TOKEN" "$LOCATION"
The operation isgetOperation answers
queued, running or cancelling202 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.
succeeded200 with the operation, whose result is what it made. Content-Location names the item (absent for a delete).
failedThe 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.
cancelled409 operation-cancelled, carrying the operation.
declined, expired403 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 says

Poll no faster than Retry-After. Each poll costs 1.

Field
idUUID.
kindcopy, move, delete or restore. (Open list.) A delete or restore is only ever an operation held for approval.
statepending_approval, queued, running, cancelling, succeeded, failed, cancelled, declined or expired.
phaseFor 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_atRFC 3339 UTC; started_at is null while queued, finished_at until it ends.
sourceWhat’s being copied, moved, deleted or restored: share id, path, kind.
targetWhere 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_idFor a restore, the version asked for; otherwise null.
progressitems_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.
resultOnce 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.
deletionFor a delete held for approval, once it has been made: the same answer deleteItem gives.
approvalFor an operation held for an approver: how its approval stands (below).
failuresAt 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_truncatedtrue 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:

Terminal window
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.

Terminal window
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 deleting its 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.status then says withdrawn.

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.

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 result is the whole copy, and its failures list 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.

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.

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.


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.

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 202
Location: https://files.example.com/api/v1/operations/9d3c...
Retry-After: 60

with 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.

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:

OutcomegetOperation answersapproval.statusState
Approved, and made200; for a delete the deletion says what was deleted and where it is now; for a move or restore result is the itemapprovedsucceeded
Declined by an approver403 change-declineddeclineddeclined
Nobody decided in time (14 days, unless the organisation says otherwise)409 change-expiredexpiredexpired
The app, or the person, withdrew it409 operation-cancelledwithdrawncancelled
Approved, but the item changed after you asked412 changed-since-requestedapprovedfailed
Approved, but the share can’t be opened as the person now409 requester-not-connectedapprovedfailed
Closed before anyone decided (for instance, the app was disconnected)409 operation-cancelledclosed or withdrawncancelled

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. Your If-Match is 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.
  • 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, listOperations shows those operations ended.
  • Operations are kept 7 days after they end, so a late poll still finds the answer.