Security Model
What keeps a token from doing more than it should, and what keeps a test integration from touching real money.
Live and test modes
Both modes share one host, api.wefunder.com. The mode segment in your credentials decides which environment a request reaches.
| Live | Test | |
|---|---|---|
| Client id | pk_live_... | pk_test_... |
| Client secret | sk_live_... | sk_test_... |
| Access token | at_live_... | at_test_... |
| Refresh token | rt_live_... | rt_test_... |
| Data | Real investors, companies, and syndicates | An isolated sandbox with its own records |
| Money | Real | None. No payment is ever taken in test mode |
| Authorize URL | https://wefunder.com/oauth/authorize | https://oauth.wefunder-sandbox.com/oauth/authorize |
A pk_test_ request cannot read or write live data, and a pk_live_ request never lands in the sandbox. Test and live credential pairs are created together for every application; the live secret is shown once, the test secret can be re-read in the portal. Details in Sandbox.
Tokens
Every access token is an at_live_ / at_test_ bearer string, but the API issues five kinds, and they differ in who they act as, how long they live, and how they are revoked. The first three come from the OAuth token endpoint and expire on a timer. The last two are minted from an installation and live as long as the install does.
| Credential | How you get it | Acts as | Scope ceiling | Resource binding | Expires | Renew | Revoked by |
|---|---|---|---|---|---|---|---|
| User token | Authorization Code + PKCE at /oauth/authorize and /oauth/token | The user who consented | The scopes the user granted, from those ticked on your app. read:investors:pii is stripped at consent and capped on every request unless the user manages your organization or Wefunder has approved your app for it | Optional. resource_type, resource_id and resource_scope=required together on the authorize request confine it to one syndicate. The three travel together or the request is rejected, and resource_scope=optional only records a hint | 2 hours | grant_type=refresh_token. Refresh tokens have no timer of their own; they work until revoked | The user, from Settings → Connected apps or DELETE /connected-apps/{app_id} |
| Syndicate-owned OAuth token | The same flow with grant_to=syndicate&syndicate_id=syn_… | The syndicate | read:syndicates and write:syndicates only; other requested scopes are stripped | Always. Bound required to that syndicate | 2 hours | grant_type=refresh_token | A current manager, from the syndicate's Integrations settings; revoking an install of your app on that syndicate sweeps it too |
| Publishable key | Your app's client id (pk_live_ / pk_test_), sent as the bearer | Your app, with no person | read:public only | None | Never | Nothing to renew | Deleting the key or disabling the app |
| Client-credentials token | grant_type=client_credentials with your pk_ / sk_ pair | Your app, with no person | read:public only | None | 24 hours | Mint another. No refresh token is issued for this grant | Deleting the key or disabling the app. Rotating a secret does not revoke tokens already minted |
| Portal personal token | Developer portal → Installations → the You row → Mint token | You, the developer who minted it | Your app's declared scopes, capped the same way as a user token | None | Never | Mint another | You, from Settings → Connected apps or DELETE /connected-apps/{app_id} |
| Installation token | POST /installations, POST /installations/{id}/tokens, Mint token in the portal, or Generate API key in syndicate settings | The company or syndicate the app is installed on | The install's grant, which never exceeds the app's declared scopes; a mint may narrow it further | Always. Bound required to the installed-on company or syndicate | Never | Mint another from the same install | Anyone who could revoke the install: DELETE /installations/{id}, or Settings → Installed apps on the company or syndicate |
A token that has expired or been revoked fails with 401 on the next request. Refresh and re-mint on the first 401, not per request; see Rate limiting.
Whose authority a token carries
The two user-owned kinds act as a person, and the API re-reads that person's roles on every request. Losing a syndicate manager role means a user token loses that syndicate immediately, and a deleted or locked account's tokens stop working at once.
The target-owned kinds (the syndicate-owned OAuth token and installation tokens) do not act as the person who created them, and the API never re-checks that person's roles on the target, so a target-owned credential survives the installing person leaving the company or syndicate. That is the property to want for an integration that belongs to the business rather than to one employee, and the reason Resource Binding recommends grant_to=syndicate for tools a whole team relies on. Where the authority comes from differs between the two:
- An installation token carries the installation's grant: the tier and scopes recorded when the install was made, frozen at that moment, and evaluated against what that tier may do on the target today.
- A syndicate-owned OAuth token is owned by the syndicate, and the token exchange creates no installation for it. If your app has an active install on that syndicate, that install's tier and scopes govern the token. If it has none, the token is refused once Wefunder enforces installations for syndicate-owned tokens; while that enforcement is still rolling out, such a token acts with full access to its syndicate within its
read:syndicates/write:syndicatesscopes. Install your app on the syndicate rather than relying on that fallback.
Role changes and account changes are separate cases. Losing a manager role never disables a target-owned token. But every syndicate-owned token (the OAuth kind and installation tokens on a syndicate alike) still records the person who authorized it, and the API refuses it if that person's Wefunder account is deleted or locked, even though the install stays active. A company-owned token records no acting person and is unaffected.
Permission to mint is a separate question from the credential's authority, and it is checked live. POST /installations/{id}/tokens and the portal's Mint token succeed only for someone who could revoke the install right now (a current editor or manager of the target, or an admin of the organization that owns the installing app) or who currently manages the app. An installer who has since left can no longer mint, but the tokens minted while they had standing keep working until the install is revoked (or, for a syndicate-owned token, until the authorizing account is deleted or locked). The install, not the token, is the durable authorization: revoking it sweeps every token minted from it and every token of the installing app bound to that target. Tokens minted through POST /installations, POST /installations/{id}/tokens or the portal's Mint token, syndicate API keys generated from syndicate settings, and the tokens a refresh produces from any of them are linked to their install record, and a request carrying one is refused the moment the install is revoked, even if the sweep has not caught up; only a syndicate API key or refreshed token issued before that link existed is not linked to the install record, so it depends on the sweep.
A suspended or disabled application is refused on every request, whatever kind of token it presents, and cannot mint new ones.
Revocation
- A user can revoke an app from their Wefunder account settings, or through
DELETE /connected-apps/{app_id}(write:profile). Every token the app holds for that user, personal tokens included, is revoked at once and the revocation is written to the activity feed. - Anyone who can edit the company or manage the syndicate, or an admin of the installing app's organization, can revoke an installation with
DELETE /installations/{external_id}or from the target's Installed apps settings. Every installation token minted from it stops working. See Installations. - A syndicate manager can revoke an individual syndicate-owned token from the syndicate's Integrations settings.
- Revoked tokens fail with
401on the next request.
Authorization is server-side
The API decides what a token may do; nothing the caller sends changes that. Scopes gate the endpoint, the principal's authority gates the resource (a user's current roles, or an installation's grant), bindings narrow the resource set, and organization roles gate app-management surfaces such as webhook endpoints. The same rules apply whether the caller is a script, a partner integration, Wefunder's own MCP server, or a person in a browser.
Irreversible actions have one more gate: a human must approve an intent before they run.
Webhooks
Deliveries are signed with HMAC-SHA256 over a timestamp and the raw body, secrets are shown once and rotated with an overlap window, and an endpoint never receives data the same grant could not read from the API. See Handling Webhooks.
Audit
Every API action on a user's behalf is recorded with the application, the acting user, the action, the resource, the outcome, and the time, and is visible to that user. See Activity & Audit Trail.
Your responsibilities
- Keep
sk_secrets and refresh tokens server-side and encrypted at rest. Never ship them to a browser or a mobile binary. - Never log full tokens or webhook secrets.
- Request the minimum scopes and bind tokens to a single syndicate when the integration only needs one.
- Verify webhook signatures against the raw body and reject stale timestamps.