Skip to content

Getting access

An app gets access to a Foldr site in three steps: an administrator turns connected apps on, your app is registered with the site, and a person signs in and agrees to what the app asks for. This page covers all three, then the tokens you end up with.

Foldr uses OAuth 2.1: the authorisation code flow with PKCE, and refresh tokens. There are no API keys, no app passwords and no passwords handled by your app; a person always signs in to Foldr itself, with whatever the organisation uses (a Foldr password, Microsoft Entra, Google, SAML, two-factor, a passkey), and your app only ever sees the result.

If you’re building an app for a customer, this is what you’ll ask their Foldr administrator to do. If you run Foldr yourself, it’s your own setup.

  1. Check the plan. Connected apps are part of some Foldr plans. A site whose plan lacks them shows “Connected apps are not included in your plan” under Settings > Connected apps.
  2. Turn connected apps on. In Settings > Connected apps, switch on Allow connected apps. It’s off by default. While it’s off, every address under /api/v1 answers a bare 404 with {"error":"not_found"}, on purpose, so the API looks as though it isn’t there. If you get that on a site that should have the API, this is the first thing to check.
  3. Say who may connect apps. Under Who may connect apps, give the permission to the people or groups who should have it. Nobody has it until it’s given. Taking it away from someone pauses every app they’ve connected (their tokens are refused) and giving it back resumes them.
  4. Register your app (below), either by adding it by hand or by letting it register itself.
  5. Decide what the app may change. A newly registered app can read and search, and change nothing. To let it make changes, the administrator opens Policy for the app and ticks what it may do (see Scopes).

Added by an administrator. On the Apps tab, Add an app: a name, one or more redirect URLs, and whether the app can keep a secret.

  • Redirect URLs must be exact: https://, or http:// only for the same computer (localhost, 127.0.0.1, [::1]). The URL your app sends in the sign-in request has to match one of them character for character. (For a loopback URL, as an app on someone’s own computer uses, the port may differ.)
  • The name and the redirect URLs can be changed afterwards: Edit on the Apps tab. Plenty of tools can’t tell you their callback until they’ve been given a client ID, so registering first and filling the URL in after is a perfectly normal order to work in. Nothing else about the app changes, and nobody already connected to it is disconnected.
  • Tick This app can keep a secret for an app that runs on a server, where the secret stays hidden. It makes the app confidential: it gets a client ID and a client secret, shown once, and must present the secret at the token endpoint. Leave it clear for an app on someone’s computer or phone, which can’t keep a secret; that app is public and gets only a client ID.
  • Either way PKCE is mandatory. A secret is an extra proof, never a substitute for it.
  • Make a note of the client ID and, for a confidential app, the secret as soon as they’re shown. The secret isn’t shown again.

Registered by the app itself. AI assistants and other tools that have to work on any site register themselves the first time someone connects them, using dynamic client registration (RFC 7591). If your app needs to do that:

Terminal window
curl -s -X POST https://files.example.com/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Reports",
"redirect_uris": ["https://reports.acme.example/foldr/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "foldr.read offline_access profile"
}'
{
"client_id": "e3a846ddd53b7e70896e7d63d023ffd5",
"client_name": "Acme Reports",
"redirect_uris": ["https://reports.acme.example/foldr/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "foldr.read offline_access",
"client_id_issued_at": 1791501531
}

Things to know about self-registration:

  • The answer is 201. Registration is anonymous, so it’s rate limited: expect 429 if a lot of registrations come from one address or for one site.
  • The app is public (token_endpoint_auth_method of none) unless it asks for client_secret_post or client_secret_basic, in which case it’s confidential and the response carries a client_secret once.
  • Foldr substitutes rather than refuses where it can: scopes, grant types and response types it doesn’t know are dropped, and the response says what was registered. It refuses bad or missing redirect URIs (invalid_redirect_uri, at most 10, https or loopback only), and a missing, unusable or impersonating name, or any other authentication method (invalid_client_metadata). A name can be 1 to 100 characters and can’t contain “Foldr” or the site’s own title.
  • Self-registration never grants the right to change anything. Write scopes are never registrable by an app; only an administrator can give them, in the app’s policy.
  • An administrator can require approval of self-registered apps. Then your app can register, but nobody can use it until an administrator approves it, and the person trying to connect it is told it’s waiting. An app nobody approves within 30 days of registering is removed; if it registers again, it waits again.
  • The consent page shows the name you registered with, marked as unverified. Choose a name people will recognise.

A scope is one kind of thing your app may do. You ask for scopes in the sign-in request; the person agrees to them on a consent page that describes each in plain words.

ScopeWhat it allowsGranted by default?
foldr.readRead: shares, listings, metadata, downloads, search, custom fields, versions.Yes, to any app.
offline_accessReceive a refresh token, so the app stays connected without the person signing in again.Yes, to any app.
profileGET /me: the person’s name, email address and sign-in name.Yes, to any app.
files.writeCreate folders, upload new files, rename, copy. Never replaces, moves away or deletes.No: an administrator allows it per app.
fields.writeSet a custom field value on a file.No: per app.
sharingAsk to share a file or folder. Every request waits for an approver; the app never gets a link.No: per app.
files.overwriteReplace a file’s contents, and put an earlier version back over it.No: per app, and a separate decision.
files.moveMove files and folders to another folder, in the same share or another.No: per app, and a separate decision.
files.deleteDelete files and folders.No: per app, and a separate decision.

The last three are changes that remove or replace. They are never implied by files.write, and a person must agree to each when they connect the app.

For any change, all of these have to allow it:

  1. The token’s scope. The call needs the scope for that operation (the table below).
  2. The person. The app acts as them, so they must be allowed to make that change in Foldr themselves.
  3. The app’s policy, set by an administrator under Settings > Connected apps > Policy:
    • which of the three write scopes (files, custom fields, sharing) the app may use;
    • where it may make changes: any share the person can write to, or only the shares named;
    • a daily limit on write calls for each person (counted in UTC days; only a change that happens counts: a call refused before it is attempted (no permission, a name that is taken, a share outside the policy), a call refused for being over the limit, and one that fails in the storage all count nothing);
    • for removing and replacing, the three scopes separately, only in shares named (there’s no “any share” for these), and a daily limit counted in items (a folder counts everything in it; the default is 100 items a day for each person).
  4. The share’s own switch for changes that can’t be undone: Allow unrecoverable changes from connected apps, in the share’s settings. It’s on by default. If an administrator switches it off, apps can’t replace, restore over or delete anything in that share if its storage couldn’t bring it back (moves are still allowed, because moving something back undoes it). People using Foldr itself aren’t affected. See Recoverability.
  5. Foldr’s own protections, which apply to every app whatever it’s allowed: a file someone else has checked out, an item under a legal hold, a published file (never moved or deleted by an app) and the document of a signing still out are refused.

An administrator can also require approval for an app’s deletes, moves and version restores. See Operations and approval.

A narrowed policy bites immediately, not when the token expires. If an administrator removes a scope from your app, your next call that needs it is refused (403 write-not-allowed), and a refresh hands out a token without it.

OperationScope
getMeprofile
listShares, getShare, listChildren, getItem, getContent, getDownloadUrl, search, listFields, getFieldValues, listVersionsfoldr.read
createFolder, uploadFile, renameItem, copyItem, createUploadSession, completeUploadSession, cancelUploadSessionfiles.write
setFieldValuefields.write
createShareRequest, getShareRequest, listShareRequestssharing
overwriteFile, restoreVersionfiles.overwrite
moveItemfiles.move
deleteItemfiles.delete
getOperation, listOperations, cancelOperationthe scope of the change that started the operation (files.write for a copy, files.move for a move, files.delete for a held delete, files.overwrite for a held restore); the OpenAPI document lists all four as alternatives

A token that lacks the scope gets 403 insufficient-scope, and its WWW-Authenticate header names the scope it needed:

WWW-Authenticate: Bearer error="insufficient_scope", scope="sharing"

Ask only for what the app needs, and ask for offline_access if it should stay connected. Foldr grants less than you ask for, rather than refusing: a scope the app isn’t allowed (an unknown one, a write scope its policy doesn’t give it) is dropped, and the consent page shows what’s actually granted. If you ask for a change scope, foldr.read comes with it. If nothing you asked for is available, the app gets its default read scopes.

So never assume you got what you asked for. Read the scope in the token response, and treat a later 403 insufficient-scope or 403 write-not-allowed as a normal answer.

The site describes itself (RFC 8414):

Terminal window
curl -s https://files.example.com/.well-known/oauth-authorization-server
{
"issuer": "https://files.example.com",
"authorization_endpoint": "https://files.example.com/oauth/authorize",
"token_endpoint": "https://files.example.com/oauth/token",
"registration_endpoint": "https://files.example.com/oauth/register",
"revocation_endpoint": "https://files.example.com/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
"revocation_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
"scopes_supported": ["foldr.read", "offline_access", "profile"],
"authorization_response_iss_parameter_supported": true
}

scopes_supported lists only the scopes any app can have. The change scopes are never advertised, because an app has them only when an administrator has given them to it.

The API is also described as a protected resource (RFC 9728) at /.well-known/oauth-protected-resource/api/v1, and every 401 from the API points to it in its WWW-Authenticate header. The resource is https://files.example.com/api/v1.

PKCE is required, with the S256 method only. Make a random verifier of 43 to 128 characters from A-Z a-z 0-9 - . _ ~, and the challenge is the unpadded base64url of its SHA-256:

Terminal window
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')

Keep the verifier with the sign-in attempt; you’ll need it in step 4.

Redirect the person’s browser (or open the system browser, for an app on their computer) to the authorisation endpoint:

https://files.example.com/oauth/authorize
?response_type=code
&client_id=e3a846ddd53b7e70896e7d63d023ffd5
&redirect_uri=https%3A%2F%2Freports.acme.example%2Ffoldr%2Fcallback
&scope=foldr.read%20offline_access%20profile%20files.write
&state=7c1f0e5a9b3d4e28
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&resource=https%3A%2F%2Ffiles.example.com%2Fapi%2Fv1
  • state is required. Make it unguessable, keep it, and check it when the person comes back.
  • redirect_uri must match a registered URL exactly.
  • resource is optional but recommended: it ties the token to the API, so it can’t be used anywhere else (RFC 8707). It must be your site’s own /api/v1 address. Leave it out and the token works on every API route; send it and the token works only under /api/v1.
  • Only response_type=code exists.

The person signs in if they aren’t already, sees the app’s name and what it asks for in plain words, and chooses Allow or Deny. Then Foldr sends their browser to your redirect_uri:

https://reports.acme.example/foldr/callback?code=fdr_ac_...&state=7c1f0e5a9b3d4e28&iss=https%3A%2F%2Ffiles.example.com

Check that state is the one you sent, and that iss is the site you meant to talk to. The code lives for 60 seconds and works once.

If something’s wrong with the request, an app an administrator registered gets ?error=...&state=...&iss=... on its redirect URL instead (invalid_request, unsupported_response_type, invalid_scope, invalid_target, access_denied). For a self-registered app the person sees an error page rather than being sent back, because anyone can register a redirect URL, unless this person has already connected that app and not disconnected it: then Foldr trusts the redirect URL and sends errors back as it does for an administrator’s app. A client or redirect URL that is not valid is an error page for every app. Either way, an error here tells you something in the request is wrong; fix it rather than retrying.

If the person isn’t allowed to connect apps, or the app is waiting for approval, the consent page says so to them. Your app only sees that no code arrives.

From your server (or, for a public app, from the app itself), POST form-encoded to the token endpoint. A public client sends its client_id; a confidential client authenticates with HTTP Basic (client_secret_basic) or sends client_id and client_secret in the form (client_secret_post), not both:

Terminal window
curl -s -X POST https://files.example.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://reports.acme.example/foldr/callback \
-d code_verifier="$VERIFIER"

(For a public app, leave out -u and add -d client_id="$CLIENT_ID".)

{
"access_token": "fdr_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "foldr.read offline_access profile files.write",
"refresh_token": "fdr_rt_..."
}
  • The body must be application/x-www-form-urlencoded. JSON is refused.
  • scope is what was granted.
  • refresh_token appears only if offline_access was granted.
  • Tokens begin fdr_at_ and fdr_rt_ so that secret scanners recognise them. Store them as you’d store a password.
  • If you send resource, it has to match the one in step 3.
Terminal window
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://files.example.com/api/v1/me
{"name":"Alex Morgan","email":"[email protected]","username":"[email protected]"}
TokenLifetime
Authorisation code60 seconds, single use.
Access token1 hour (expires_in says so). Opaque: don’t try to read it.
Refresh tokenUsed once and replaced. Each connection lasts up to 90 days from the person agreeing (an administrator can set 1 to 365 days), and ends sooner after 30 days unused (also the administrator’s setting). The lifetime is counted from the first sign-in and rotation doesn’t extend it.

When the connection’s lifetime or idle limit is reached, the refresh fails with invalid_grant and the person has to connect the app again.

Before the access token runs out (or when a call answers 401 invalid-token), exchange the refresh token:

Terminal window
curl -s -X POST https://files.example.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN"

The answer has the same shape as step 4, and it contains a new refresh token. Refresh tokens rotate:

  • Save the new refresh token and discard the old one, before you use the new access token.
  • A refresh token that’s been used already is a sign it leaked. If an already-used one is presented again, the whole chain it belongs to is revoked (the access tokens that came from the same sign-in too), and the person has to connect again. The exception is a deliberate allowance for clients that refresh from two places at once: within 30 seconds of the first use a second attempt is refused with invalid_grant but revokes nothing. Even so, serialise your refreshes: one at a time for each connection.
  • You may send scope with fewer scopes than the connection holds, to get a narrower access token. Asking for a scope the connection never held is invalid_scope. The new refresh token keeps the connection’s full scope.
  • If an administrator has since narrowed your app’s policy, the new access token carries only what the app may still do.

The token and revocation endpoints answer in OAuth’s own format, not problem documents: a JSON body with an error such as invalid_request, invalid_client (401), invalid_grant, invalid_scope, unsupported_grant_type, or server_error (500, a temporary fault: try again). There’s deliberately no description. Which check failed is never said, so invalid_grant covers an expired code, a wrong verifier, a used code, a revoked connection, a person who no longer exists and a person who’s lost the right to connect apps. When you get invalid_grant from a refresh, stop refreshing and send the person through the sign-in again.

When a person disconnects your app inside your app, tell Foldr (RFC 7009):

Terminal window
curl -s -X POST https://files.example.com/oauth/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$REFRESH_TOKEN" \
-d token_type_hint=refresh_token
  • The client authenticates just as at the token endpoint.
  • Revoking an access token ends that token only.
  • Revoking a refresh token ends the whole connection: every token the app holds for that person, and the refresh chain. The person has to agree again to reconnect.
  • The answer is always 200 with an empty body, including for a token that doesn’t exist or isn’t yours, so the endpoint can’t be used to test whether a token is real.

People see the apps they’ve connected in Foldr, in the Connected apps section of the Security tab of their profile, and can disconnect any of them. Administrators can disconnect any person’s connection, or revoke an app for everyone, in Settings > Connected apps. Disconnecting:

  • ends the connection at once: the next call with any of its tokens is 401 invalid-token, and the refresh token no longer works;
  • withdraws any of the app’s changes still held for approval, so nothing waiting is made later (see Operations and approval);
  • can be undone only by the person connecting the app again, through the sign-in flow.

A person is also disconnected when their account is deleted, disabled in the directory, or removed by directory sync, with the same result on the next call. And a person who loses the right to connect apps is paused rather than disconnected: their tokens are refused while the right is missing, and work again if it comes back.

A token carries no password. A share that opens only with the person’s own password (an SMB or WebDAV share set up that way, for instance) can’t be mounted on a token’s behalf, so it doesn’t appear in what the app sees. This is why an app can show fewer shares than the person sees in Foldr’s own web app.

Some shares live in the person’s own Google Drive, OneDrive, Dropbox or Box account. Foldr reaches those with a separate sign-in to that service, which the person gives once, in Foldr’s web app: by opening the share there (Foldr asks them to sign in to the service) or under Me › Services. Until they have, opening such a share through the API answers 409 storage-account-not-connected. Your app can’t make that connection for them, and the sign-in to Foldr above doesn’t make it either.