HunchwireDocs hunchwire.com Sign in

API reference

Webhooks

Get an event in your product when someone joins, moves up, gets in or leaves.

View as Markdown

Use your agent. Paste this into it:

Read hunchwire.com/docs/webhooks and set up webhooks so my app hears when people join.
On this page Webhooks

Webhooks

Get an event in the founder's own product when someone joins, moves up, gets in or leaves. Hunchwire sends each event to one URL, signed and retried. Webhooks work with every way, on every plan.

GET /projects/:id/setup/site/webhooks?page=1 Needs analytics

Get the endpoint and its delivery log, newest first, 20 a page.

Show 6 details
  • url: where events go, or null while webhooks are off.
  • events: the events it gets. all_events: every event there is.
  • failing_since: when it started failing, or null. It starts failing when a delivery runs out of retries, and stops when the next one is delivered.
  • secret_set: whether the signing secret exists. Let people in uses the same secret. Only the founder can copy it, from Setup › Connect › Webhooks.
  • total, page and per_page: ask for the next page while page × per_page < total.
  • deliveries: each with id, event, event_id, person_id, state (pending, delivered or failed), attempts, response_status (your endpoint's answer, or null), error (why it failed, or null), next_attempt_at, delivered_at and created_at. With emails, each also has body: what Hunchwire sent.
GET /projects/:id/setup/site/webhooks
curl https://app.hunchwire.com/projects/:id/setup/site/webhooks?page=1 \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

PATCH /projects/:id/setup/site/webhooks Needs manage

Set the URL and the events.

Details
  • The URL must be https. http://localhost works for testing.
  • Leave out events to keep them. Send all of them, or none, to get every event.
  • Send "url": "" to turn webhooks off.
PATCH /projects/:id/setup/site/webhooks
curl -X PATCH https://app.hunchwire.com/projects/:id/setup/site/webhooks \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhooks": {"url": "https://yourapp.com/hooks/hunchwire", "events": ["person.joined"]}}'

POST /projects/:id/setup/site/webhooks/test Needs manage

Send a test event now, with a sample person. Returns the delivery with its body.

POST /projects/:id/setup/site/webhooks/test
curl -X POST https://app.hunchwire.com/projects/:id/setup/site/webhooks/test \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

POST /projects/:id/setup/site/webhooks/deliveries/:webhook_delivery_id/retry Needs manage

Send one delivery again, now. Returns the delivery.

POST /projects/:id/setup/site/webhooks/deliveries/:webhook_delivery_id/retry
curl -X POST https://app.hunchwire.com/projects/:id/setup/site/webhooks/deliveries/:webhook_delivery_id/retry \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

Receive an event

Hunchwire posts each event to the URL:

  • person is the person, exactly as the API returns it (see The person).
  • Answer with a 2xx within 10 seconds.
  • Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 15 hours. Then the delivery fails.
  • An event can arrive twice. Skip an id you've seen.
  • Behind a firewall? Let User-Agent: Hunchwire-Webhooks/1.0 through on this path.
Request
POST https://yourapp.com/hooks/hunchwire
Content-Type: application/json
User-Agent: Hunchwire-Webhooks/1.0
Hunchwire-Event: person.joined
Hunchwire-Delivery: evt_8Gq2…
Hunchwire-Signature: t=1791751809,v1=5f2c…

{"id": "evt_8Gq2…", "type": "person.joined", "created_at": "2026-10-11T14:02:11Z", "project": {"key": "…", "name": "Loop"}, "person": {…}, "data": {"via": "api"}}

Check the signature

Check Hunchwire-Signature before you trust the body. v1 is the HMAC-SHA256 of t, a dot and the raw body, keyed with the signing secret, in hex. Refuse a t more than 5 minutes old. Node is shown here. Python and Ruby are on the Webhooks screen.

Check the signature
import crypto from &quot;node:crypto&quot;;
// req.body must be the raw text Hunchwire sent (express.text({ type: &quot;application/json&quot; })).
const [t, v1] = req.get(&quot;Hunchwire-Signature&quot;).split(&quot;,&quot;).map((part) =&gt; part.split(&quot;=&quot;)[1]);
const expected = crypto.createHmac(&quot;sha256&quot;, process.env.HUNCHWIRE_SECRET).update(`${t}.${req.body}`).digest(&quot;hex&quot;);
const fresh = Math.abs(Date.now() / 1000 - Number(t)) &lt; 300;
if (!fresh || v1.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) return res.sendStatus(401);
const event = JSON.parse(req.body); // skip it if you&#39;ve seen event.id before

Events

type When data
person.joined Someone new is on the list via: embed or api
person.friend_joined A friend now counts for them: both have confirmed, a held friend was approved, or their link let in someone waiting. When they confirm, each pending friend sends one friend_id, and rewards this friend reached for them
person.moved The founder or an agent moved them in line from_place, to_place, by
person.let_in They got access via: invite, referral or friends. With invite or referral, also invite: id, url, expires_at, wave_id
person.joined_product Your product reported they joined it invite_id, or null
person.removed The founder or an agent took them off the list by
person.restored They're back on the list by
person.deleted Their data was deleted Nothing. person has only id: delete what you hold for it
test You sent a test event message
  • by says who made the change: person, agent, signup (they signed up again and confirmed), or null when it happened on its own.
  • With invite, url is always set. Send it yourself, by email or text, if your product does.
  • A friend's link sends person.let_in with the same invite as the sign-up's answer. If you already welcomed them, skip it.
  • Someone already on the list let in through a friend's link gets person.let_in (via referral) when they confirm. The link's owner gets person.friend_joined then, or when they confirm, if later.
  • A place that shifts because someone else joined, left or moved sends nothing. Look the person up for their current place.
The person.joined event
{ "id": "evt_8Gq2…", "type": "person.joined", "created_at": "2026-10-11T14:02:11Z",
  "project": { "key": "…", "name": "Loop" },
  "person": { "id": 4812, "email": "maya@fernwood.studio", "phone": null, "first_name": "Maya", "list": null,
    "state": "waiting", "place": 148, "code": "k7q2m9xa", "link": "https://tryloop.app/?ref=k7q2m9xa",
    "friends": 2, "pending_friends": 0, "referred_by": 4790, "access": { "granted": false, "via": null, "at": null },
    "rewards": [], "joined_product_at": null, "signed_up_at": "2026-10-11T14:02:11Z", "confirmed": false },
  "data": { "via": "api" } }

Made from skill.md, the file agents read. Add .md to any page's address for its Markdown.