Guide to Community.com Webhooks: Features and Best Practices
Community (community.com) webhooks notify your systems about conversations with your audience: a member texts you back, a campaign message goes out, or someone joins, updates their details, or leaves. If you're building on Community's SMS and conversational messaging platform, webhooks are how inbound replies and membership changes reach your own stack in near real time instead of waiting on an export.
This guide covers how Community webhooks work, the HMAC-SHA256 scheme behind the community-signature header, the five member and message event types, and the best practices for production.
What are Community webhooks?
Community webhooks are HTTP POST requests sent to an endpoint you own when something happens in your account, such as a member sending you a message or joining your audience. You subscribe per webhook to any combination of five event types, and each delivery arrives as a JSON envelope with the event data nested inside it.
Every payload is signed. Community generates an HMAC-SHA256 signature and delivers it in a community-signature header alongside the timestamp used to produce it, in a single header value of the form t=1711666033,v1=b777f6ae.... The scheme resembles Stripe's, and it is worth being explicit that it is not the Standard Webhooks specification: there are no webhook-id, webhook-timestamp, or webhook-signature headers to look for.
Webhooks are a plan-gated feature. Access depends on permissions on your account, so if the Webhooks option is not visible in your dashboard, that is a provisioning question for Community rather than a configuration mistake.
Community webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard only, under Settings, Integrations, Webhooks; there is no API for creating or managing webhooks |
| Verification | Lowercase hex HMAC-SHA256 over the timestamp, a literal ., and the raw request body, delivered in the community-signature header |
| Secret | A signature secret unique to each webhook, shown in the dashboard when the webhook is created or edited |
| Payload | JSON envelope with id, type, object, created, and api_version, with the event data under data |
| Events | Five documented types covering inbound and outbound messages plus member creation, update, and deletion |
| Transport | HTTPS endpoints only, with certificates validated as valid and for the correct host |
| Delivery | At-least-once, with a documented requirement to deduplicate on the event id |
| Response budget | A 2xx within 15 seconds; the response body is ignored |
| Retries | Five retries with increasing backoff, for up to an hour from the first attempt |
| SDK | No official verification helper; verify manually with your standard crypto library |
Common events
Community documents five event types, and a webhook subscribes to whichever subset you select when you configure it. Branch on the top-level type field:
| Event | Fires when |
|---|---|
message.inbound | A member sends a message to your account |
message.outbound | Your account sends a message to a member |
member.created | A new member joins your account |
member.updated | A member unsubscribes or changes any of the standard personal data collected |
member.deleted | A member deletes themselves |
Two of these carry qualifications worth knowing before you build on them.
message.outbound is not a complete record of everything you send. Community filters some outbound messages out before publishing, including messages handled by other Community features such as help, start, and stop responses, and tapbacks. If you need an exact ledger of sends, this event is not it.
member.deleted carries a much sparser payload than the other member events: id, active, timestamp, client_id, communication_channel, and an emptied communication_channel_id, with none of the personal data fields. Write your member handlers so that a missing email or given_name is an ordinary case rather than an error.
See Community webhook payloads in action. Inspect and replay sample Community webhook payloads in the Hookdeck Console, with no account or setup required.
Setting up Community webhooks
Webhooks are configured in the Community Dashboard under Settings, Integrations, Webhooks, or directly at https://dashboard.community.com/settings/integrations/webhooks. The option only appears if your account has the necessary permission. Existing webhooks are listed there and can be enabled or disabled in place.
Creating or editing a webhook opens a modal where you set the name, the endpoint URL, and the event types to publish. That same modal displays the signature secret for this webhook. Copy it into your application's environment, because your handler needs exactly this value to verify signatures:
# From the webhook modal in the Community Dashboard
export COMMUNITY_WEBHOOK_SECRET="the signature secret shown for this webhook"
Two details about the secret matter in practice. It is unique per webhook, so if you point several webhooks at the same service you will be holding several secrets and must select the right one for the endpoint being called. And it is not the same credential as the community_api prefixed token used for the Async REST API, which will never produce a matching signature no matter how correct the rest of your implementation is.
Your endpoint must be HTTPS, and its certificate must be valid and match the host. Community will not deliver to a plain HTTP URL.
Securing Community webhooks
Verification is a manual HMAC check, since Community publishes no SDK helper. The community-signature header carries two fields in one value:
community-signature: t=1711666033,v1=b777f6ae2497ae95e99811c88b28d8ba377c95d615905963c68fae4c800de48d
t is the Unix timestamp in seconds at which the request was generated, and v1 is the signature. The signed content is the timestamp, a literal . character, and the raw request body, in that order: compute HMAC-SHA256(secret, timestamp + "." + raw_body), hex-encode it, and compare it against v1.
The raw body matters. Compute the HMAC over the exact bytes received, because parsing and re-serializing JSON changes whitespace and key order and therefore changes the digest. In Express use express.raw(), in Next.js use await request.text(), and in FastAPI use await request.body(). Forgetting the timestamp and the separator is the single most common cause of a handler that rejects every legitimate delivery.
const crypto = require("crypto");
function verifyCommunityWebhook(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
// Parse "t=...,v1=..." by splitting on "," then the first "=".
// Field order is not guaranteed, so do not match the whole value with one regex.
const fields = {};
for (const part of signatureHeader.split(",")) {
const i = part.indexOf("=");
if (i === -1) continue;
fields[part.slice(0, i).trim()] = part.slice(i + 1).trim();
}
const timestamp = fields.t;
const signature = fields.v1; // only v1 is defined; treat anything else as unsupported
if (!timestamp || !signature) return false;
const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`, "utf8")
.digest("hex");
try {
// Hex is case-insensitive, so normalize before the timing-safe compare
return crypto.timingSafeEqual(
Buffer.from(signature.toLowerCase()),
Buffer.from(expected)
);
} catch {
return false; // different lengths, invalid
}
}
app.post(
"/webhooks/community",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.get("community-signature");
if (!verifyCommunityWebhook(req.body, signature, process.env.COMMUNITY_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString());
// Acknowledge first, then process off the response path
processQueue.add({ id: event.id, type: event.type, data: event.data });
res.sendStatus(200);
}
);
The same check in Python:
import hashlib
import hmac
def verify_community_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header or not secret:
return False
fields = {}
for part in signature_header.split(","):
key, sep, value = part.partition("=")
if sep:
fields[key.strip()] = value.strip()
timestamp = fields.get("t")
signature = fields.get("v1")
if not timestamp or not signature:
return False
signed_content = timestamp.encode("utf-8") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed_content, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature.lower())
Community's documentation specifies no tolerance window for t, so a staleness check is a hardening step. If you add one, keep the window comfortably longer than an hour: Community retries a failed delivery for up to an hour from the first attempt, and a tight window can reject legitimate retries. Deduplicating on the event id, which you need to do anyway, already provides most of the replay protection a tolerance window would.
Make Community webhooks production-ready. Hookdeck Event Gateway ingests every delivery, acknowledges it in milliseconds, and retries your handler on its own schedule.
Community webhook limitations and pain points
Webhooks are dashboard-only and plan-gated
The Problem: There is no API for creating, updating, or listing webhooks, so endpoints and event subscriptions cannot be managed as code, reproduced across environments, or rotated programmatically. Access to the feature depends on permissions on your account, and a teammate without them will not see the option at all.
Why It Happens: Webhook configuration lives entirely in the Community Dashboard, and the feature is provisioned per account rather than enabled by default.
Workarounds:
- Record each webhook's endpoint URL, subscribed events, and which secret it maps to in your own configuration, since the dashboard is the only place that state exists.
- Route staging and production through separate endpoints on your side, because you cannot script the split upstream.
- Arrange access with Community before planning work that depends on it.
How Hookdeck Can Help: Point one Community webhook at a Hookdeck source and manage fan-out to your environments in Hookdeck instead, where connections and filters are configurable and versioned without touching the vendor dashboard.
At-least-once delivery makes duplicates your problem
The Problem: Community states plainly that a webhook can be sent more than once for the same event. For membership changes a duplicate is untidy; for messages it can mean sending a member the same reply twice, which is the failure your audience actually notices.
Why It Happens: The delivery guarantee is at-least-once, so a retry after an ambiguous outcome can redeliver an event your handler already processed successfully.
Workarounds:
- Store each event
idfor at least an hour and skip any event you have already seen, which is what Community's own documentation recommends. - Treat message sending as at-most-once: for outbound actions, not sending is the safer failure.
- Do the deduplication check before any side effect, not after.
How Hookdeck Can Help: Every delivery is recorded with its full body and headers, so when a duplicate does slip through you can see both requests side by side and confirm whether the retry came from Community or from your own handler failing to acknowledge.
A 15 second budget, then automatic disabling
The Problem: Your endpoint must return a 2xx within 15 seconds. Connection errors, non-2xx responses, and timeouts are retried five times with increasing backoff for up to an hour, and a webhook that keeps failing may be disabled by Community, after which it has to be re-enabled before deliveries resume.
Why It Happens: Sustained failures are treated as a broken endpoint rather than a transient problem, and the automatic disabling protects the sender.
Workarounds:
- Verify the signature, enqueue the work, and return 2xx immediately; never do the processing inline.
- Alert on your own delivery failures rather than waiting for the notification email, since by the time the webhook is disabled you have already lost events.
- Check the webhook list in the dashboard after any incident, because a disabled webhook stays disabled until someone turns it back on.
How Hookdeck Can Help: Hookdeck acknowledges Community immediately and retries your handler independently, so a slow or briefly unavailable service never counts as a failed delivery upstream, and Issues alerts you when your own endpoint starts erroring.
Best practices
Verify against the raw body
Read the raw bytes before any parsing, sign the timestamp followed by a . and then the body, hex-encode, and compare timing-safe. A 401 on mismatch keeps forged payloads out, and since the endpoint is a public HTTPS URL, an unverified handler will eventually be found.
Deduplicate on the event id
Store each event id for at least an hour and check it before doing any work. This is the deduplication Community's documentation asks for, and it doubles as replay protection for a scheme that has no timestamp tolerance of its own. See our guide to webhook idempotency.
Acknowledge quickly, process asynchronously
Return a 2xx as soon as the signature checks out and move the work off the response path, so you stay inside the 15 second budget and never accumulate the failures that lead to a disabled webhook. See why to process webhooks asynchronously.
Keep one secret per webhook
Each webhook has its own signature secret. Map secrets to endpoints explicitly rather than reaching for a single shared value, and never reach for the Async REST API token, which is a different credential entirely.
Handle sparse and optional member fields
Personal data fields on a member are optional, and member.deleted omits nearly all of them. Treat every field other than id and communication_channel as potentially absent instead of asserting a full profile on each event.
Branch on type, and handle unknown values
Dispatch on the top-level type and log anything you do not recognize instead of throwing. A handler that returns a non-2xx on an unfamiliar event type turns a harmless addition into a retry loop and, eventually, a disabled webhook.
Conclusion
Community signs each delivery with a lowercase hex HMAC-SHA256 over the timestamp, a literal ., and the raw body, keyed with a signature secret unique to each webhook and delivered in a single community-signature header. Verify the raw body timing-safely, deduplicate on the event id because delivery is at-least-once, acknowledge inside 15 seconds, and write member handlers that tolerate absent fields.
Hookdeck Event Gateway ingests Community's deliveries, acknowledges them instantly, queues them durably, and retries your handler independently, so the 15 second budget and the automatic disabling stop being constraints your code has to satisfy.
Get started with Hookdeck for free and handle Community webhooks reliably in minutes.