Webhooks

Audience

This is the point of Lenit webhooks. For idea.status_changed and announcement.published only, the payload carries the people connected to that idea, so your server knows who to notify right away.

The four audience types

TypeWho it isNotes
authorThe person who posted the ideaA single object, or absent
commentersEveryone who left a public comment on the ideaAn array; authors of private admin notes are excluded
votersIdentified end users who voted for the ideaAn array; anonymous visitors (fingerprint) are excluded
reactorsIdentified end users who reacted with an emoji on the announcement linked to the ideaAn array; fingerprint visitors are excluded

The audience object

json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "audience": {
    "author": {
      "externalId": "user_123",
      "traits": { "plan": "pro", "userType": "heavy", "signupDays": 120 }
    },
    "commenters": [
      { "externalId": "user_456", "traits": { "plan": "free" } },
      { "externalId": "user_789", "traits": { "plan": "pro" } }
    ],
    "voters":   [ /* same shape */ ],
    "reactors": [ /* same shape */ ]
  }
}
  • externalId — the same userId you passed when initializing the widget
  • traits — the object you passed when initializing the widget

Audience targets — choose who you receive

When you register a webhook you tick the groups you want. Anything left unticked isn't included in the payload at all.

A different audience per endpoint
text
1
2
3
4
5
6
7
8
[Webhook A]  author + voters
             → the author and voters only

[Webhook B]  commenters
             → people who commented only

[Webhook C]  author + commenters + voters + reactors
             → everyone

Each endpoint is configured on its own, so webhook A and webhook B can receive different audiences for the same event.

Who is left out (on purpose)

CautionAlways excluded from the audience
  • Votes and reactions from anonymous visitors (fingerprint) — with no externalId there's no way to notify them, so they're left out of the payload.
  • Authors of private admin comments — a comment with is_private = true is an internal note, so its author isn't treated as a commenter.
  • The author of an admin-posted idea — when idea.author isn't an end user, audience.author is empty.

A 500-person cap

Each audience group carries at most 500 people; anything beyond that is truncated before sending. This:

  • Keeps the webhook payload from ballooning
  • Keeps your server from timing out and returning a 504

If you need to reach more than 500 people, take the event into a queue and paginate on your side (Recipe 6 — retry queue).

Using it on your server — the core pattern

Merge the audience and dedupe
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const event = JSON.parse(req.body);

if (event.event === "idea.status_changed" && event.data.audience) {
  const a = event.data.audience;
  const recipients = [
    a.author,
    ...(a.commenters || []),
    ...(a.voters     || []),
    ...(a.reactors   || []),
  ].filter(Boolean);

  // The same person can appear in several groups - dedupe by externalId
  const seen = new Set();
  const unique = recipients.filter(u => {
    if (seen.has(u.externalId)) return false;
    seen.add(u.externalId);
    return true;
  });

  for (const user of unique) {
    // Look up email/phone in your own DB by externalId, then send on your channel
    await notifyUser(user.externalId, {
      title: event.data.title,
      newStatus: event.data.newStatus,
    });
  }
}

Using traits — routing by segment

Use traits to decide how each person gets notified.

Different channels for paid and free users
js
1
2
3
4
5
6
7
for (const user of unique) {
  const channel = user.traits?.plan === "pro"
    ? "kakao-alimtalk"     // Paid users get a KakaoTalk notification
    : "email";             // Free users get an email

  await sendByChannel(channel, user.externalId, ...);
}

Next steps

←Event typesReceiver recipes→