Skip to main content

Authentication

The Wefunder API uses OAuth 2.0. Most integrations use the Authorization Code flow with PKCE: the token acts on behalf of a Wefunder user (an investor, a syndicate lead, or a founder) with exactly the scopes that user granted your app.

A Client Credentials flow is also available for server-to-server access with no user present. Client-credentials tokens can only hold public scopes (read:public), which cover the public deal catalog. They cannot read anyone's profile, investments, syndicate, or company data. Use the Authorization Code flow for anything tied to a user.

Get your credentials​

Create an application in the developer portal: choose a name, the scopes you need, the client type, and a redirect URI if users will authorize through OAuth (required for a public client; a confidential client may leave it blank when its installs and tokens come from the portal, and an app with no scope ticked is public-catalog-only: read:public through client credentials, no callback, no user consent). The scopes you tick are the ones a user can later grant through the Authorization Code flow. read:public is not in the picker because every app gets it automatically; it is the only scope the Client Credentials grant can use. See which grant gets which scopes.

Creating an app mints two OAuth clients at once, a test client and a live client, and shows both secrets on the next screen:

  • The live secret (sk_live_) is shown once. Only its hash is stored; if you lose it, rotate the key from the app page.
  • The test secret (sk_test_) stays retrievable from the app page, so you can paste it into integration tests on another machine later.

Store the live secret before leaving that screen.

Who can enter the portal:

  • Anyone who manages a company or a syndicate on Wefunder can create apps right away.
  • Everyone else joins the waitlist and gets portal access on approval.

Your live key works immediately on the companies and syndicates your organization manages, and the people who manage your organization (developer, admin, or owner) can authorize your app for their own accounts right away. Until the app passes review, that is also the limit: anyone else who opens your authorize link sees a page saying the app hasn't been reviewed yet, and no token is issued — the code exchange and refresh are refused for them too. Installing your app on someone else's company or syndicate likewise requires app review. Submit for review from the app's page in the portal; once approved, anyone can connect. Test (pk_test_) clients are not gated: sandbox accounts belong to your own realm.

Endpoints​

PurposeURL
Authorize, live (pk_live_ client)https://wefunder.com/oauth/authorize
Authorize, test (pk_test_ client)https://oauth.wefunder-sandbox.com/oauth/authorize
Token exchange and refreshhttps://api.wefunder.com/oauth/token
API basehttps://api.wefunder.com

There is one token endpoint for both environments. The credential prefix decides where the request goes. Resource URLs carry no version segment; see Versioning.

Authorization Code with PKCE​

PKCE is required for every authorization request. Generate a code_verifier and its S256 code_challenge per request.

Step 1: Redirect the user to authorize​

https://wefunder.com/oauth/authorize?
response_type=code&
client_id=pk_live_YOUR_CLIENT_ID&
redirect_uri=YOUR_REDIRECT_URI&
scope=read:profile read:investments&
state=RANDOM_STATE&
code_challenge=BASE64URL_SHA256_OF_VERIFIER&
code_challenge_method=S256

After the user approves, Wefunder redirects to your redirect_uri with ?code=...&state=.... Check state before continuing. While your app is unreviewed, a user outside your organization never reaches the consent screen or your redirect_uri (see above), and a code or refresh token exchanged for such a user is refused with invalid_grant.

Step 2: Exchange the code for tokens​

curl -X POST "https://api.wefunder.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=YOUR_REDIRECT_URI" \
-d "client_id=pk_live_YOUR_CLIENT_ID" \
-d "client_secret=sk_live_YOUR_CLIENT_SECRET" \
-d "code_verifier=YOUR_CODE_VERIFIER"

Public clients (browser or mobile apps) omit client_secret; PKCE is what protects them.

Response:

{
"access_token": "at_live_abc123...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "rt_live_def456...",
"scope": "read:profile read:investments",
"created_at": 1705420800
}

Access tokens live for two hours. When one expires, or a request returns 401, exchange the refresh token for a new pair:

curl -X POST "https://api.wefunder.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=rt_live_def456..." \
-d "client_id=pk_live_YOUR_CLIENT_ID" \
-d "client_secret=sk_live_YOUR_CLIENT_SECRET"

Public clients omit client_secret here too, exactly as in the code exchange.

Response:

{
"access_token": "at_live_ghi789...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "rt_live_jkl012...",
"scope": "read:profile read:investments",
"created_at": 1705428000
}

Every refresh rotates the refresh token. The response carries a new refresh_token, and the one you sent stops working the first time the new access token is used. Store the new access token and the new refresh token together, in one write, before you make another call. A client that keeps presenting the old refresh token is turned away with invalid_grant, and the user has to authorize again.

Step 3: Call the API​

curl "https://api.wefunder.com/users/me" \
-H "Authorization: Bearer at_live_abc123..."

Client Credentials (public catalog only)​

curl -X POST "https://api.wefunder.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=pk_live_YOUR_CLIENT_ID" \
-d "client_secret=sk_live_YOUR_CLIENT_SECRET" \
-d "scope=read:public"

Requesting any non-public scope with this grant fails. The token can call GET /explore, GET /offerings/{id}, GET /companies/{id} and the other public-catalog endpoints. A client-credentials token lives 24 hours; cache it and re-mint on expiry or on the first 401.

Publishable key (public catalog, no token)​

For public reads you don't need a token at all. Send the client id itself as the bearer:

curl "https://api.wefunder.com/companies/co_8Kd0aB3xQ9k2vF8mNp1zT5wY" \
-H "Authorization: Bearer pk_live_YOUR_CLIENT_ID"

The client id is not a secret, so this works from a browser, a phone, or a static page (Show Your Raise on Your Own Site is one fetch). It reaches exactly what a read:public token reaches: GET /explore, GET /offerings/{id}, GET /companies/{id}, GET /companies/search and the other public-catalog reads. Anything else answers 403 insufficient_scope with a note that a publishable key reads public data only.

Cross-origin, GET requests to the API answer for any origin, so a page on any domain can make these reads directly; there is no domain to register. Writes and every other method answer cross-origin only from the origins of your app's registered redirect URIs.

Everything that applies to a token applies to the key: a pk_test_ key reaches your sandbox realm and is refused on live, a deleted app's key answers 401, and a disabled app's key answers 403. It is rate-limited per key (300 requests per 5 minutes) on top of the per-IP limits; see Rate limiting. The client secret is a different string and never belongs in a browser.

Credential and token prefixes​

Every identifier tells you what it is and which environment it belongs to:

PrefixWhat it is
pk_live_ / pk_test_Client ID. Also the publishable key: usable as a bearer for public reads
sk_live_ / sk_test_Client secret
at_live_ / at_test_Access token (Bearer)
rt_live_ / rt_test_Refresh token

_test_ identifiers belong to your sandbox realm. _live_ identifiers act on real Wefunder accounts.

Scopes​

Apps request scopes at authorization time. The default scope is read:profile. See Scopes for the full catalog and which scopes are self-serve versus approval-gated.

Common failures​

Statuserror.typeCause
401unauthorizedToken missing, invalid, or expired; or a publishable key that is unknown, archived, or the wrong mode for this cluster
403insufficient_scopeToken valid, but it was not granted the scope this endpoint needs; or a publishable key sent to a non-public endpoint
403forbiddenToken has the scope, but the user or app lacks access to that resource

Every error body carries a request_id. See Error handling.

Security practices​

  1. Keep client_secret server-side. Never ship it in a browser or mobile app; use PKCE for public clients.
  2. Store secrets in a secrets manager. Rotate on staff changes and incidents.
  3. Log the request_id from error bodies. It is the fastest path to a support answer.
  4. Build against _test_ credentials first and switch to live only when ready.
Was this page helpful?