Show Your Raise on Your Own Site
Put the amount raised, the goal, and the investor count from your Wefunder round on your own landing page. One publishable key, one endpoint, a dozen lines on the page. No server, no token, nothing to rotate.
Everything this reads is already public on your Wefunder page, so the key that reads it is public too.
What you need
- Your publishable key:
pk_live_…, shown on your app's page in the developer portal. Company editors enter the portal directly; nobody has to approve you. If you have no app yet, create one and pick Something else with nothing ticked:read:publicis on every app and is all this needs. - Your company id:
co_…, listed on the same app page under Show your raise, next to a copy of the snippet below already filled in.
The publishable key is the client id. It is not a secret and may sit in your page source;
the client secret (sk_live_…) is a different string and never goes in a browser.
The snippet
Paste this where the numbers should appear, with your key and id filled in.
<p id="wf-raise">Loading…</p>
<script>
const WF_KEY = "pk_live_YOUR_PUBLISHABLE_KEY";
const WF_COMPANY = "co_YOUR_COMPANY_ID";
fetch(`https://api.wefunder.com/companies/${WF_COMPANY}`, {
headers: { Authorization: `Bearer ${WF_KEY}` },
})
.then((r) => r.json())
.then(({ data }) => {
const a = data.attributes;
const r = a.current_raise;
const usd = (cents) => (cents / 100).toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
const el = document.getElementById("wf-raise");
if (!r) {
el.textContent = `${usd(a.totals.raised_on_wefunder_all_time_cents)} raised on Wefunder`;
return;
}
const verb = r.testing_the_waters ? "reserved" : "raised";
const people = r.testing_the_waters ? "reservations" : "investors";
el.textContent = `${usd(r.amount_raised_cents)} ${verb} of ${usd(r.funding_target_cents)} · ${r.investor_count} ${people}`;
})
.catch(() => { document.getElementById("wf-raise").textContent = "See our raise on Wefunder"; });
</script>
That is the whole integration. The browser calls Wefunder directly from whatever domain your
site is on: GET requests to the API answer cross-origin for any origin, so there is no
domain to register. The key is rate-limited per key and per IP, and a static page at any
traffic stays far inside the limits (Rate limits).
What comes back
Trimmed to the fields the snippet reads:
{
"data": {
"id": "co_8Kd0aB3xQ9k2vF8mNp1zT5wY",
"type": "company",
"attributes": {
"name": "Acme Robotics",
"url": "https://wefunder.com/acme",
"raising": true,
"current_raise": {
"amount_raised_cents": 64230000,
"funding_target_cents": 100000000,
"investor_count": 1203,
"oversubscribed": false,
"closes_at": "2026-12-31T23:59:59Z",
"testing_the_waters": false,
"offering_id": "ofr_yw3OhusvpfJP4Pk3wRYV1F2R"
},
"totals": {
"profile_total_raised_cents": 64230000,
"raised_on_wefunder_all_time_cents": 64230000
}
}
}
}
current_raisedescribes your current Wefunder round, computed by the same policy your Wefunder page uses, and isnullwhen no round is live (raising: false). The snippet falls back to the lifetime total intotalsthen.- Money is integer cents in every
*_centsfield, the same convention asGET /investments. The older dollar-string twins (amount_raised,funding_target, …) are still present on this API version and go away in the next dated one; don't build on them. totals.profile_total_raised_centsis the number on your page's ticker bar; it can be larger than the current round when the page folds in past rounds. Pick the figure you mean.
Wording
If your round is a Testing the Waters round, people on Wefunder reserve; they do not
invest yet. Your copy must say "reserved" and "reservations", never "invested" or
"investors", while current_raise.testing_the_waters is true. The snippet switches on
that flag; keep the switch if you restyle it. When the round converts to a live Reg CF
offering the flag flips to false on its own and the copy follows.
From a server instead
If you already have a backend and would rather keep Wefunder calls off the page (to cache, to combine with other data, or because you also read investor data), the same key works from a server, and the SDKs take it as the bearer. No token exchange is involved for public reads.
- Node
- Python
- Ruby
import { Wefunder } from "@wefunder/sdk";
const wf = new Wefunder({ accessToken: process.env.WF_PUBLISHABLE_KEY });
// raw.* returns the transport result; unwrap gives you the response body ({ data: company }).
const { data } = await wf.unwrap(wf.raw.getCompany({ path: { id: process.env.WF_COMPANY_ID } }));
const raise = data.attributes.current_raise;
console.log(raise ? raise.amount_raised_cents : data.attributes.totals.raised_on_wefunder_all_time_cents);
import os, requests
company_id = os.environ["WF_COMPANY_ID"]
r = requests.get(
f"https://api.wefunder.com/companies/{company_id}",
headers={"Authorization": f"Bearer {os.environ['WF_PUBLISHABLE_KEY']}"},
timeout=10,
)
r.raise_for_status()
attrs = r.json()["data"]["attributes"]
raise_ = attrs["current_raise"]
print(raise_["amount_raised_cents"] if raise_ else attrs["totals"]["raised_on_wefunder_all_time_cents"])
require "net/http"
require "json"
uri = URI("https://api.wefunder.com/companies/#{ENV.fetch("WF_COMPANY_ID")}")
req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer #{ENV.fetch("WF_PUBLISHABLE_KEY")}")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
attrs = JSON.parse(res.body).dig("data", "attributes")
current = attrs["current_raise"]
puts(current ? current["amount_raised_cents"] : attrs.dig("totals", "raised_on_wefunder_all_time_cents"))
Cache the answer for a few minutes if your page is busy; the numbers change at most every few minutes on Wefunder's side too. If you later want investor rows rather than totals, that is where a server key and the investor sync guide come in.
Rate limits that apply
| Limit | Value | Does a widget get near it? |
|---|---|---|
| Per publishable key | 300 requests / 5 minutes | Only on a very busy page. Cache in sessionStorage or on your server if you do. |
| Per credential | 1,000 requests / hour | Same. |
| Per IP | 5,000 requests / hour | No: each visitor is their own IP. |
GET /companies/{id} is not in the 60-per-minute Explore bucket, which covers
/explore, /offerings/{id} and /companies/search only.
Troubleshooting
| Symptom | Cause |
|---|---|
401 | The key is not a pk_ publishable key, belongs to a deleted app, or is a pk_test_ key used against live. Copy the live key from the app page. |
403 insufficient_scope | The key reached an endpoint that needs a user or an installation. A publishable key reads public data only. |
404 | Not a co_ id, or the company's profile is not publicly listed. |
| A CORS error in the browser console | The request was not a plain GET, or it sent cookies (credentials: "include"). The snippet above sends neither. |
current_raise is null | Your round is not live right now. The snippet shows the lifetime total; hide the element if you prefer. |
| Numbers are 100× too big or too small | A dollar formatter applied to cents, or the reverse. *_cents fields are integers; divide by 100 to display. |
Next
raise-stats-widgetin wefunder-examples is this page as a runnable file with a test, if you would rather copy a directory than a snippet.- Build a Deal Aggregator with the Explore API reads every public offering with the same key.
- Sync Investments to Your CRM is the next step once you want investor rows, not just totals.