Measure the Investors You Refer
Send people to Wefunder from your newsletter, community, or ad campaign, and find out how many of them invested. You create referral codes with your app's server key, put them on your links, and read back weekly counts.
Nothing here needs a founder to approve you, and nothing here changes what your visitors see on Wefunder. A code records that someone arrived through your link. It grants no access and selects no round.
Wefunder does not pay partners per referral. You may not be paid per referral, or by any amount that depends on investments made in a securities offering, unless you are a registered broker-dealer. Your organization acknowledges this once before its first code (Step 3).
What you'll build
- An application whose server key holds the referral-code scopes.
- A referral code, minted once per campaign, channel, or newsletter issue.
- Links that carry the code as
?wfr=ref_…. - A weekly job that reads each code's counts.
How it works
- The link. Add
wfr=<code id>to any wefunder.com link: a company page, Explore, the home page, a direct invest link. The parameter name is exactlywfr, lowercase, with no aliases. Unknown and paused codes are ignored without an error. - The cookie. On arrival, Wefunder stores the code in a cookie for 30 days. If the visitor arrives through another code later, the newest code wins (last touch among referral codes).
- The credit. When that visitor invests within 30 days, the investment is credited to the code, in any company, wherever they first landed. A code created for one company or offering credits only investments in that company.
- One credit per commitment. A reservation that later converts into an investment, or an investment that is amended, keeps its original credit and is not counted again.
Step 1: Give your app the scopes
In the developer portal, open your application and tick
read:referral_codes and write:referral_codes. These are the only scopes besides
read:public that a server key can hold, because the codes belong to your app, not to a user.
A token issued to a user, or an installation token, is refused on every referral-code endpoint
with 403 app_credentials_required. Use the Client Credentials grant.
Step 2: Mint a token
curl -X POST https://api.wefunder.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=$WF_CLIENT_ID \
-d client_secret=$WF_CLIENT_SECRET \
-d "scope=read:referral_codes write:referral_codes"
The token lives for two hours. Cache it and re-mint when it expires; the token endpoint allows 20 mints per hour per client.
Step 3: Accept the referral terms
The first time your organization creates a code, the API answers:
{
"error": {
"type": "attribution_terms_required",
"message": "Your organization must accept the referral attribution terms before creating or reactivating referral codes.",
"details": { "acceptance_url": "https://wefunder.com/developers/orgs/123#attribution-terms" },
"remediation": "An owner or admin of the organization accepts the terms once in the developer portal."
}
}
An owner or admin of your organization opens acceptance_url, reads the terms, and accepts them.
This happens once per organization, not per app or per code. Then retry the request.
Step 4: Create a code
curl -X POST https://api.wefunder.com/referral_codes \
-H "Authorization: Bearer $WF_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "October newsletter",
"utm": { "source": "acme_newsletter", "medium": "email", "campaign": "october" }
}'
{
"data": {
"id": "ref_8f3ExampleReferral0000",
"type": "referral_code",
"attributes": {
"label": "October newsletter",
"active": true,
"target": null,
"utm": { "source": "acme_newsletter", "medium": "email", "campaign": "october" },
"metadata": {},
"link_parameter": "wfr=ref_8f3ExampleReferral0000",
"created_at": "2026-10-13T17:30:00Z",
"updated_at": "2026-10-13T17:30:00Z"
}
}
}
Fields you can send:
| Field | Notes |
|---|---|
label | Required. Your name for the code, up to 200 characters. |
company_id or offering_id | Optional, at most one. Credits only investments in that company (co_…) or that offering's company (ofr_…). The offering must be public. Fixed once the code exists. Leave both out for a code that credits any company. |
utm | Optional presets: source, medium, campaign, content, term. See UTM presets. |
metadata | Optional flat object for your own use, up to 4 KB. |
Your app may hold 500 active codes. Past that, creating or resuming a code returns
422 quota_exceeded; pause codes you no longer use to free a slot. There is no
Idempotency-Key on this endpoint, so a retried request can create a second code. Pause the
extra one.
Step 5: Put the code on your links
Append link_parameter to any wefunder.com URL:
https://wefunder.com/acme?wfr=ref_8f3ExampleReferral0000
https://wefunder.com/explore?wfr=ref_8f3ExampleReferral0000
Mint one code per thing you want to measure separately: a campaign, a channel, a newsletter issue. Counts are never combined across codes, so a code is the smallest unit you can report on.
UTM presets
If you also tag your links with UTM parameters, presets save you from building every URL by hand.
On a visit that carries the code, each preset fills the matching utm_* field the URL leaves
blank. A UTM value on the link itself wins over the preset, field by field. Presets never touch a
visit without the code.
This makes your own breakdown and the founder's UTM report agree. Presets are labels: nothing is
counted or authorized by them. Each value is at most 100 printable ASCII characters. A source
or campaign that starts with wf_ is reserved for Wefunder's own channels and returns
422 reserved_utm_prefix.
Step 6: Read the counts
curl https://api.wefunder.com/referral_codes/ref_8f3ExampleReferral0000/stats \
-H "Authorization: Bearer $WF_ACCESS_TOKEN"
{
"data": {
"id": "ref_8f3ExampleReferral0000",
"type": "referral_code_stats",
"attributes": {
"label": "October newsletter",
"visits": 412,
"unique_visitors": 388,
"visits_as_of": "2026-10-12",
"companies": [
{
"company": "co_7hQExampleCompany0001",
"level": "counts",
"verification_url": "https://wefunder.com/acme/marketing/reports#partners",
"in_scope_since": "2026-W42",
"weeks": [
{ "week": "2026-W42", "investors": { "min": 3, "max": 5 }, "policy_version": 1, "published_at": "2026-10-19T06:05:00Z" },
{ "week": "2026-W43", "investors": { "min": 0, "max": 2 }, "policy_version": 1, "published_at": "2026-10-26T06:05:00Z" }
]
}
]
}
},
"meta": { "has_more": false, "page_count": 1 }
}
How to read the ranges
- One cell per company per week. A week is an ISO week, Monday to Sunday, UTC. A cell counts the unique investors your code referred into that company in that week.
- Every count is a range, never an exact number. Ranges are currently 3 wide:
{0, 2},{3, 5},{6, 8}, and so on. Seven investors read{ "min": 6, "max": 8 }. The bottom range includes zero, so{0, 2}can mean none. - Published once, never revised. A week's cell appears early on the following Monday (UTC) and does not change afterwards, even if an investment is later canceled. The current week has no cell yet.
- Every week appears. A company's series runs from
in_scope_sinceto the latest published week, with no gaps. A week with no investors reads{0, 2}like any other low week. - There are no totals. Not per code, per company, or across weeks, and no amounts. You can add cells up yourself; the sum is a range, and summing weeks counts investor-weeks, since someone who invests in two weeks appears in both.
policy_versiontells you which range rule produced a cell. The width may change in the future, only from a future week onward; a published cell is never re-released.
Ranges protect investors. If a code went to one person, an exact count would tell you whether that person invested. A range of 3 that includes zero does not.
Which companies appear
A code made for a company covers that company from the week the code was created. A code with no
target covers every company whose page its visitors landed on, from the week of the first such
visit. A company never enters the list because someone invested there; only a visit adds it.
The response is paginated over companies (cursor, per_page); each company's weekly series is
always complete.
visits and unique_visitors count arrivals up to the end of visits_as_of (UTC) and refresh
daily. GET /referral_codes and GET /referral_codes/{id} carry the same visit numbers under
stats, but never investor counts.
Polling
Cells change once a week. Read your codes' stats once on Monday after 06:05 UTC and store the result; reading more often returns the same data.
Rows instead of ranges: when a founder installs your app
The ranges above are what any app gets for its own codes. If a company's founder also installs
your app, you see that company's referred investments as rows instead. The level field on each
company tells you which you are getting:
level | What decides it | Each row carries |
|---|---|---|
counts | Your app is not installed on the company, or the install has neither scope below | Weekly ranges, as above |
anonymized | Your app's installation on the company grants read:attribution:anonymized | Opaque investment and investor tokens, week, amount_tier (under_1k, 1k_to_5k, 5k_to_25k, 25k_and_up), status (active or inactive), time_to_invest (same_day, within_week, within_30_days) |
full | The installation is at founder tier and lists read:attribution:full itself | The investment id, the investor's name and email, the exact amount, status, clicked_at, applied_at, and the UTM source and campaign when one was recorded |
The opaque tokens are stable for your app and that company, and differ across apps and across companies, so they cannot be used to follow one investor between companies.
Each installed company returns up to 100 rows. To page further, request that company alone with
company_id=co_… and pass investments_cursor from the previous investments_next_cursor.
The read:attribution:* scopes are granted through Wefunder's attribution program, not the
portal picker; contact us to enable them on your app. They take effect only
on companies where a founder has installed your app. Installed access applies to that company
alone: every other company in the same response keeps its ranges.
Let the founder check your numbers
A founder can see every partner's referrals into their company, with exact visits, investment
counts, and amounts, in the Partners section of their company's reports (Fundraising →
Analytics → Reports). Each company in your stats response carries a verification_url that
opens that section. The founder sees your organization's name and each code's ref_ id and label,
so when you quote a code, they can find it.
The founder's numbers are exact and current, and count only investments that are still active. Your ranges are fixed when each week is published, so the two will not always match exactly.
Managing codes
| Task | Request |
|---|---|
| List your codes | GET /referral_codes (filter with active=true or active=false; meta.active_limit is your cap) |
| Look one up | GET /referral_codes/{id} |
| Rename | PATCH /referral_codes/{id} with label |
| Pause or resume | PATCH /referral_codes/{id} with active: false or active: true |
| Change UTM presets | PATCH /referral_codes/{id} with utm (replaces all presets) |
A paused code is ignored on new arrivals, but cookies it already set keep crediting investments for up to 30 days. Resuming a code counts against the 500-code cap and needs your organization's accepted terms. The target company or offering cannot be changed; mint a new code instead.
Errors you may see
Status and type | Meaning |
|---|---|
403 app_credentials_required | You used a user or installation token. Use a client_credentials token. |
403 insufficient_scope | The token lacks read:referral_codes or write:referral_codes. Tick them on the app and mint a new token. |
404 not_found | The code is not one of your app's, the company_id is not in this code's stats, or the target you sent at creation is not a public company or offering. |
422 attribution_terms_required | Your organization has not accepted the terms. Open details.acceptance_url. |
422 quota_exceeded | Your app already has 500 active codes. |
422 reserved_utm_prefix | A preset source or campaign starts with wf_. |
422 invalid_params | Both company_id and offering_id were sent. |
422 validation_error | Another field is invalid; details.errors says which. |
See Error Handling for the error envelope.
Webhook events
Two webhook events cover referrals.
referral_cohorts.published arrives every Monday once the new week's cells are ready.
investment.referred is sent only through an installation on the investment's company that
grants an attribution scope. Without an endpoint, poll the stats endpoint on Mondays.
The legacy Attribution API
The older Attribution API, which reported UTM matches on a single campaign, was removed on
2026-10-14. Its endpoints answer 410 Gone. Use referral codes instead.