Skip to main content

OAuth Scopes

A scope names what a token may do, in the form action:resource (for example read:syndicates). Request only the scopes you need on the authorization request; the user sees each one on the consent screen and can decline. When an app requests no scope at all it receives read:profile.

A write: scope always includes its read: counterpart. A token holding write:syndicates can call every read:syndicates endpoint; you do not need to request both. The same holds for every implication in the catalog (read:attribution:full includes the anonymized and aggregate tiers). A scope with no read counterpart, such as write:comments, is write-only by design.

Which grant gets which scopes​

The grant you use decides which scopes a token can hold and whether anyone has to consent. Two things about the portal follow from this: read:public is not a checkbox, because every app has it, and the scopes you do tick are what the Authorization Code flow may later ask a user for.

Client CredentialsAuthorization Code + PKCE
Who the token acts asYour app, with no userA Wefunder user who authorized your app
Scopes the token can holdread:public, plus read:referral_codes / write:referral_codes when ticked on the appAny scope ticked on the app except the referral-code scopes, plus read:public
How the scope gets on the appImplicit. Granted to every app at creation; not shown in the portal pickerSelectable. Tick it in the portal, then request it on the authorize URL
User consentNoneRequired. Each scope appears on the consent screen and the user can decline
Program-gated scopesNeverOnly after the access request is approved
Typical usePublic deal catalog on your own serverAnything that names a person: profile, portfolio, syndicate, company

Requesting any scope other than read:public and the referral-code scopes with the client-credentials grant fails at the token endpoint. Requesting a scope the app does not have ticked fails at the authorize step. See Authentication for both flows end to end.

Public scope​

Every app has it, a server-side (client_credentials) token may hold it, and the app's publishable key (pk_…) reaches it with no token at all, from a browser or a phone (Authentication). Apart from the referral-code scopes, which also belong to the app, everything else needs a user.

ScopeGrants
read:publicThe public deal catalog: GET /explore, GET /offerings/{id}, GET /companies/{id}, and company search

Self-serve scopes​

Selectable by any developer in the portal. What a token can actually reach is still bounded by what the authorizing user can see or manage on Wefunder.

ScopeGrants
read:exploreThe deal catalog as this user sees it, including accredited-only and invited rounds
read:profile / write:profileThe user's basic profile (GET /users/me; without this scope a user token gets only the user's id there), the companies they follow as their public profile lists them (GET /users/me/follows leaves out companies with promotion_opt_in false unless the token also holds write:follows or read:mcp), plus their activity feed and connected apps; the write side updates the profile and revokes connected apps
write:followsFollow and unfollow companies for the user. Includes listing every company they follow (GET /users/me/follows, the full list), but not their profile or email: with only write:follows, GET /users/me returns just the user's id, and the other read:profile endpoints return insufficient_scope
write:commentsDraft questions, answers, and comments for the user to approve and post
read:investmentsThe user's investments, reservations, and portfolio
read:companiesCompanies the user can access, with their rounds and SPVs. For a company the user helps run, this also covers its investor roster
read:webhooks / write:webhooksThe app's own webhook endpoints
read:referral_codes / write:referral_codesThe app's own referral codes: create, rename, pause, and read their counts. Held by the app's server key (client credentials) only: the codes belong to the app, so a user token is refused
read:installations / write:installationsWhere the app is installed, and installing it on companies or syndicates the user can edit
read:investors:piiInvestor names and email addresses on a company's investor records. Declared like any scope; granted per install, see below
read:syndicates / write:syndicatesSyndicates the user manages: members, deals, statistics, portfolio. Irreversible actions still require an intent
read:invite_links / write:invite_linksInvestment links to your offerings: member links, and the invite links they share their access with. Used with the token of an app installed on the syndicate or company. Includes per-person invites; an invite addressed by account (wefunder_user_id) never returns that account's email
read:investment_sessions / write:investment_sessionsHosted investment sessions: send a known investor straight into a single-page Wefunder checkout from your site or app, and read the outcome. Used with the token of an app installed on the syndicate or company that runs the offering (beta)
write:syndicate_membersMint join sessions that sign a person up to your syndicate from your own site. Already included in write:syndicates (beta)

The picker lists them in this order: the deal catalog first, then each write: directly under its read:.

Investor PII is granted per install​

Ticking read:investors:pii on your app does not by itself grant it anywhere. Each installation on a company carries the scope only when the person installing could grant it:

  • They manage your organization (developer, admin, or owner): a founder running their own app, or your staff installing on a company they already edit. They see those investors through their company role, so the app learns nothing its operator can't. No review.
  • Otherwise, Wefunder has approved your app for investor PII. This is the third-party case: a founder installing an agency's or a SaaS vendor's app hands investor identities to a team outside their company. Request the review from the app's page in the portal; until it is approved, installs by people outside your organization carry every other scope but not this one, and the response's scopes show it. Once approved, the founder's consent screen states that your organization will see investor names and emails.

The grant is frozen at install time. An install made before your app was approved does not gain the scope; re-install where you need it. Syndicate installs are unaffected. Without the scope, investor records are served with the identity fields omitted (see Investment changes).

Declared, not yet selectable​

These scopes are registered in the catalog but no API operation accepts them yet, so the portal does not offer them. Requesting one on an authorize URL fails at the authorize step unless your app already carries it. Apps and tokens created while they were selectable keep them; they simply unlock nothing until the operations ship, at which point the scope returns to the self-serve list above.

ScopePlanned to grant
write:investmentsInvestment metadata changes and TTW reservation submit/cancel. Investing itself stays human-only
read:contracts / write:contractsContract packages and templates. Signing stays human-only
read:perks / write:perksPerks attached to offerings
read:tax_documents1099s and K-1s. Flagged as sensitive on the consent screen

Available, not selectable​

These scopes have live operations behind them, but the portal does not offer them as ticks, because a self-serve app could not use them as ticked. Wefunder adds them to an app when the matching program applies.

ScopeWhy it is not a tickHow you get it
write:companiesUnlocks only Partner API operations, which are an invite-only previewJoin the preview; Wefunder enables it per application
read:files / write:filesThe file endpoints are not yet in the published referenceAsk Wefunder if your integration needs API-side file uploads
read:investors:contactsGranted by a founder's own consent to Wefunder's MCP server for one tool, not a partner-integration surfaceNot available to partner applications
read:profile:followsThe list of companies the user follows, on its own. Both read:profile and write:follows include it, so a separate tick would only offer a subset of a scope you already pickTick read:profile or write:follows

Program-gated scopes​

Granted through an access program, not the picker, and not listed in the portal.

ScopeProgramGrants
read:attribution:aggregateMarketing partner, tier 0Implied by the two scopes below. The endpoints that used it alone were removed on 2026-10-14
read:attribution:anonymizedMarketing partner, tier 1On an installation on a company, turns that company's referral-code ranges into anonymized rows and delivers investment.referred
read:attribution:fullFounderOn a founder-tier installation that lists it, turns that company's referral-code ranges into full rows, including investor identity
write:attributionMarketing partnerAuthorizes nothing today: the partner registration, invite, access-request and attribution-webhook endpoints it guarded were removed

Retired scopes​

read:offerings and write:offerings were folded into read:companies and write:companies. A company is never wanted without its rounds, so the two scopes gave you a choice with no right answer. Every endpoint that required an offerings scope now requires the matching companies scope, and the lifecycle investment.* and offering.* webhook events are delivered to installs that carry read:companies.

Nothing you already have breaks. A token, install, or app that holds read:offerings is treated as holding read:companies (and write:offerings as write:companies) on every request, so it reaches everything it reached before. New apps cannot select the retired names; request the companies scopes instead. There is no date by which existing grants must be replaced.

read:follows was folded into read:profile. The companies a user follows are already shown on their public Wefunder profile, so listing them is part of reading the profile, and GET /users/me/follows accepts read:profile. A token, install, or app that holds read:follows keeps exactly what it had: it is treated as holding read:profile:follows, so it lists the follows and does not gain the rest of the profile or the user's email. write:follows stays a separate scope: following companies for a user is not the same as editing their profile, and it lists the follows without reading the profile. New apps cannot select read:follows; request read:profile, or write:follows if the app only needs the follows.

Reserved scopes​

read:mcp is the umbrella Wefunder's own MCP server requests; it implies the read scopes its tools need and is not offered to other applications. admin:read, admin:write, and admin:attribution power Wefunder's internal tooling and are never granted to partner applications.

Requesting scopes​

Space-separate them on the authorization request:

GET https://wefunder.com/oauth/authorize?
client_id=...&
response_type=code&
scope=read:profile read:syndicates write:syndicates&
...

The token response echoes what was granted:

{
"access_token": "at_live_...",
"scope": "read:profile read:syndicates write:syndicates"
}

When a scope is missing​

A call that needs a scope the token does not hold returns 403:

{
"error": {
"type": "insufficient_scope",
"message": "...",
"request_id": "..."
}
}

A 403 with type: "forbidden" is different: the scope is present but the user lacks permission on that particular resource, or the token is bound to another one.

How you recover from insufficient_scope depends on one question: does your app declare the scope? Check the app's page in the portal. Re-consent and re-installation can only hand out scopes the app already holds; they cannot add one it was created without.

Changing scopes​

An app's declared scopes are set when it is created and can be changed afterwards from the app's page in the portal: any self-serve scope can be added or removed while the app is active, whether or not it has been through third-party review. Approval is never affected by a scope change. A key's redirect URI and client type can also be changed from the app page (see Onboarding). Adding and removing have very different effects on what already exists, so read the matching section before you save.

Adding a scope from the portal​

Open the app, find Scopes, and choose Edit scopes. The editor lists the self-serve scopes with the app's current ones ticked; tick the ones to add. Saving writes the new scopes to the app and to every one of its keys, test and live, and records the change in the app's audit history. The request is refused, and nothing changes, when:

  • the app is suspended or disabled (scope_change_blocked); re-enable it first, or write to support for a suspension;
  • a scope is not self-serve (invalid_scopes). Program-tier scopes are added by Wefunder as part of the program.

read:investors:pii can be added like any other scope. It is granted per install, so adding it changes what an install may carry, not what any install does carry; see Investor PII is granted per install and request the review from the app's page if founders outside your organization install your app.

Adding a scope widens nothing that already exists. Every user token, refresh token, and installation keeps exactly the scopes it was granted; a refresh re-issues at most the original token's scopes. The new scope is requestable: your next authorize URL may include it and the user sees a fresh consent screen (the new consent replaces their earlier connection), and a company or syndicate install carries it only once that target is installed again (revoke, re-install, re-mint, as described next). That holds for installs on your own organization's companies and syndicates too. Plan the rollout the same way you would for a scope the app already declared.

Removing a scope from the portal​

Untick a scope in the same editor and save. A removal takes effect immediately and narrows everything beneath the app, in one transaction:

  • the scope is removed from the app and from every key;
  • every active installation of the app, user, company or syndicate, loses the scope. An install left with no scopes is disconnected (revoked), and its tokens with it;
  • every access token whose scopes are not a subset of the app's new set is revoked, including tokens that never expire (installation tokens and syndicate API keys) and the refresh tokens attached to them, so a refresh cannot re-mint the removed scope. A token holding only scopes the app still declares keeps working.

The response (and the portal, after saving) tells you how many installs were narrowed, how many were disconnected, and how many tokens were revoked. Expect insufficient_scope from any integration that still relies on the scope, and invalid_token from the revoked tokens; switch those integrations to a token that holds the remaining scopes (re-consent, or mint a new installation token).

A scope that another declared scope still implies cannot be removed on its own. read:mcp implies read:explore, read:profile, read:syndicates and read:companies, and every write: scope implies its read side (write:profile implies read:profile), so unticking read:profile while the app keeps write:profile removes nothing. The scope stays, nothing beneath it is touched, and the response names the implier under still_implied; remove the implier to drop it. (write:follows implies only the follows list, not read:profile, so unticking read:profile beside it does remove read:profile.)

Removing a scope that is not self-serve (one Wefunder added) is allowed, but only Wefunder can add it back. Removals never change the app's approval state or its investor-PII approval.

The app declares the scope, but this token does not hold it​

The grant on the credential is what is short, and each kind of credential is re-granted differently.

  • User token. Send the user through the Authorization Code flow again with the full scope list, including the one that was missing. They see the consent screen again; the new consent replaces the earlier one, and the token you receive carries the new set. Tokens from the earlier consent keep their old scopes until they expire or are revoked.
  • Installation token (a company or syndicate install). A token can be narrower than its install: POST /installations/{id}/tokens takes an optional scopes list and grants its intersection with the install, so first look at the install's own scopes (GET /installations/{id}, or the Installed on list in the portal). If the install already grants the scope, mint a replacement token that requests it, or omit scopes to get the install's full grant, and switch your integration to it; no reinstall is needed, and earlier tokens keep working until you revoke the install. Reinstall only when the install's own grant lacks the scope: the grant is frozen when the install is made and no token can exceed it. An app has at most one active install per company or syndicate (POST /installations answers 409 already_installed while one exists), so the order is: revoke the old install, install again with the scopes you need (the portal Install button, the install link, or POST /installations), mint a token from the new install, and switch your integration to it. Revoking invalidates every token on that install at once, so expect a gap between the revoke and the new mint, and do not revoke when a new token would do. Each install carries only what its installer could grant, so read the new install's scopes rather than assuming; see Installations.
  • Client-credentials token. This grant holds read:public only. Anything else needs a user or an install; there is nothing to re-request.

The app does not declare the scope​

Requesting the scope on an authorize URL fails at the authorize step with invalid_scope, and an install can carry only scopes the app holds, so neither re-consent nor re-installation can add it. Add the scope and then re-consent or re-install as above. A replacement app is only needed for a scope the editor cannot offer (one that is not self-serve), or for a shape the portal cannot give you:

  1. Create a new app with the full scope set. It gets its own client ids and secrets.
  2. Install it where the old one is installed, and send users through consent for it. Each company or syndicate needs a new install, and each user a new authorization; nothing transfers between apps.
  3. Move your integration to the new credentials, endpoint by endpoint, while the old app keeps working. Webhook endpoints belong to an app, so register them on the new app too.
  4. Once nothing depends on the old app, disable it from its page in the portal (owner or admin). Disabling revokes every token it holds at once, which is why it is the last step and not the first; a disabled app can be re-enabled, but its tokens are not restored.

An app that has been used cannot be deleted. Once it was approved, issued a live token, or installed anywhere, the portal offers Disable instead of Delete, so its audit trail survives. Only a never-used app can be deleted outright. If an old app is in a state you cannot resolve from the portal, write to support with the app id.

Was this page helpful?