Webhooks
Real-time event notifications for your integration. Instead of polling, register an endpoint and we POST events to it as they happen — an offering opening, an investment executing, and more over time.
The flow
Quick start
- Create an HTTPS endpoint on your server.
- Register it with the
/webhook_endpointsAPI and save the returnedsecret. - Verify the signature on every request (
Wefunder-Signatureheader). - Store, then return
2xxfast — persist the delivery, acknowledge, and process from the stored row.
Events & payloads
You subscribe to specific events; each delivery is a signed JSON envelope:
{
"id": "evt_0KZq8xExampleOpened01",
"event": "offering.opened",
"created_at": "2026-01-15T12:00:00Z",
"mode": "live",
"installation": { "id": "inst_7hQExampleInstall01", "target": { "type": "company", "id": "co_3kExampleCompany001" } },
"data": { "offering": "ofr_9m2ExampleRound00", "status": "open", "company": { "name": "Example Company", "url": "example-company" } }
}
installation names the installation the delivery was produced for — the company, syndicate, or user your app is installed on — so one endpoint serving several installations can tell them apart and join each delivery to the token it holds for that installation.
See Events for the full catalog and per-event payloads.
Security
Every request carries a Wefunder-Signature: t=…,v1=… header — an HMAC-SHA256 over the timestamp and raw body, keyed with your endpoint secret. Always verify it and reject requests older than 5 minutes. Full recipe + copy-paste code in Verification.
At-least-once delivery
We deliver each event at least once — during retries you may receive the same event more than once. Deduplicate on id plus installation.id (process each pair once; ignore repeats).
Why the pair: if your app is installed on more than one company, syndicate, or user, and one event concerns two of them (an investor who connected your app invests in a company that installed it), an endpoint registered for the whole app receives one delivery per installation for that event. They share the event id but carry different payloads, each limited to what that installation may see. Deduplicating on id alone would drop all but the first. An endpoint created for a single installation only ever receives that installation's deliveries, so for it id alone is still exact.
Today an app-wide endpoint still receives a single delivery per event (per-installation delivery is a separate rollout step, see Setup); the pair key is correct either way, so use it from the start.
Return 2xx quickly, but only after the delivery is durably stored. A 2xx marks the delivery done on our side and it is never sent again, so the work must be on disk before you answer; a 5xx (or no answer) is retried. Do the real work from the stored row, off the request:
// Good: store first, acknowledge second, process off the request
app.post("/webhooks", async (req, res) => {
if (!verify(req)) return res.sendStatus(400);
const body = JSON.parse(req.body);
// Every event in today's catalog names its installation, so the pair key is complete.
// INSERT ... ON CONFLICT (event_id, installation_id) DO NOTHING — the unique index is the dedupe.
// A throw here is a 500, and we retry the delivery.
await inbox.insert({ eventId: body.id, installationId: body.installation.id, payload: req.body });
res.sendStatus(200); // committed; a worker drains the inbox
});
Failures, retries, and auto-disable
- Retries. A delivery that doesn't get a
2xxis retried with exponential backoff (30 seconds after the first failure, doubling each time, capped at 6 hours between attempts) over a 72-hour horizon. After that it's dead-lettered (kept, not dropped) and can be redriven once your endpoint is healthy. - Canceled. Pending and dead-lettered deliveries are canceled, not retried, when the endpoint is removed or the installation they belong to is uninstalled. A canceled delivery cannot be redriven.
- Warning. If an endpoint fails continuously for ~24 hours, we email your org's developers.
- Auto-disable. After 3 consecutive days in which every delivery failed, the endpoint is automatically disabled and we email your developers. Disabling is time-based — a low-traffic endpoint is treated the same as a busy one. While it is disabled no new deliveries are queued for it; deliveries that were already pending keep their own retry schedule and either land or dead-letter at their own horizon. Fix your server and re-enable it. Re-enabling does not resend dead-lettered deliveries or recreate the events from the disabled window — the Setup page walks through the recovery order (test, re-enable, resend, reconcile from your cursor).
Testing
Use the test action to send a real, signed example event and confirm your endpoint works end-to-end — before any real event fires.
Next steps
- Setup — register and manage endpoints
- Events — the event catalog and payloads
- Verification — validate signatures (with a CI-verified test vector)