# Community Webhooks

Webhooks from Community (community.com) — the SMS / conversational-messaging
platform for brands and creators. This is not a generic community-forum
product, not Circle/Discourse/Bettermode, and not Salesforce Experience Cloud.
The developer hub is [https://developer.community.com](https://developer.community.com).

## When to Use This Skill

* How do I receive Community webhooks?
* How do I verify the `community-signature` header?
* Why is my Community webhook signature verification failing?
* How do I handle `message.inbound` / `message.outbound` events?
* How do I handle `member.created`, `member.updated`, and `member.deleted` events?
* How do I deduplicate Community webhooks (at-least-once delivery)?

## Verification (core)

Community signs every request with HMAC-SHA256 over `{timestamp}.{raw_body}`
and sends the result in a Stripe-style `community-signature` header:

```http
community-signature: t=1711666033,v1=b777f6ae2497ae95e99811c88b28d8ba377c95d615905963c68fae4c800de48d

```

The HMAC key is the signature secret, which is unique to each webhook
(Dashboard → Settings → Integrations → Webhooks). It is not your
`community_api`-prefixed Async REST API token. Always HMAC the raw request
body — re-serializing the JSON changes the bytes and breaks the signature.

```javascript
const crypto = require('crypto');

function verifyCommunitySignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;

  // Parse "t=...,v1=..." without assuming field order
  const fields = Object.fromEntries(
    signatureHeader.split(',').map((p) => {
      const i = p.indexOf('=');
      return i === -1 ? ['', ''] : [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    })
  );
  const timestamp = fields.t;
  const signature = fields.v1; // only the v1 scheme is supported
  if (!timestamp || !signature) return false;

  // Signed content is: timestamp + "." + raw body
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`, 'utf8')
    .digest('hex');

  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch {
    return false; // length mismatch
  }
}

```

> For complete handlers with tests, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/community-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/community-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/community-webhooks/examples/fastapi/).

There is no official Community SDK for webhook verification in any language —
implement the HMAC directly, as above.

## Event Types

Community documents exactly five event types. Subscriptions are selected
per webhook in the Dashboard.

| Event | Triggered When | `object` |
| --- | --- | --- |
| `message.inbound` | A member sends a message to your account | `message` |
| `message.outbound` | Your account sends a message to a member (some are filtered out) | `message` |
| `member.created` | A new member joins | `member` |
| `member.updated` | A member changes any standard personal data collected | `member` |
| `member.deleted` | A member unsubscribes or deletes themselves | `member` |

## Payload Envelope

```json
{
  "id": "4fab65b1-c98f-466e-b52f-c170768a6d89",
  "type": "member.created",
  "object": "member",
  "created": "2025-01-05T23:59:45.643131Z",
  "api_version": "2024-02-12",
  "data": { "object": { "id": "7a3e02ec-...", "active": true } }
}

```

Event data lives at `data.object` in every documented sample payload. The
prose in the same doc page says member events "include a `member` object in the
event's `data` field", so handlers should read `data.object` first and fall back
to `data.member` / `data.message` defensively.

`member.deleted` payloads are sparse — only `id`, `active: false`,
`timestamp`, `client_id`, `communication_channel`, and an emptied
`communication_channel_id`. Never assume `email`, `given_name`, `geolocation`,
or any other personal-data field is present on a member event.

## Delivery Semantics

* At-least-once. The same event can be delivered more than once. Deduplicate
  by storing the webhook `id` (or object `id`) for at least an hour and
  checking it before processing. This matters most for messages — prefer
  at-most-once handling there.
* Respond 2xx (200–299) within 15 seconds. The response body is ignored.
* Retries: on a connection error, non-2xx, or timeout, Community retries up
  to 5 times with increasing backoff, for up to an hour from the first
  attempt. Continued failures trigger a notification email and the webhook may
  be disabled (and must then be re-enabled).
* So: verify, enqueue, return 2xx fast — never do the work inline.

## Important Headers

| Header | Description |
| --- | --- |
| `community-signature` | `t=<unix_seconds>,v1=<hex>` — HMAC-SHA256 of `{t}.{raw_body}`, lowercase hex |

There is no documented source-IP allowlist, no handshake, and no
subscription-confirmation request. Community's other transport safeguards are
that only HTTPS endpoints are accepted and SSL certificates must be valid for
the correct host.

## Environment Variables

```bash
COMMUNITY_WEBHOOK_SECRET=your_signature_secret   # Dashboard → Settings → Integrations → Webhooks (per webhook)

# Optional hardening (NOT a documented Community requirement — see below)
COMMUNITY_WEBHOOK_TOLERANCE_SECONDS=0            # 0 = disabled (default)

```

The docs specify no tolerance window for `t`. A replay/staleness check is
therefore the implementer's own hardening choice, and it is disabled by default
in the examples. If you enable one, make the window comfortably larger than an
hour — retries of a failed delivery can arrive up to an hour after the first
attempt.

## Local Development

```bash
# Forward Community webhooks to your local server (no account required)
npx hookdeck-cli listen 3000 community --path /webhooks/community

```

Paste the resulting HTTPS URL into the endpoint URL field of your webhook in
Dashboard → Settings → Integrations → Webhooks.

## Access

Webhooks are a plan/permission-gated Community feature and can only be
configured in the Community Dashboard — there is no API to create them. Contact
[yourfriends@community.com](mailto:yourfriends@community.com) to arrange access.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/community-webhooks/references/overview.md) - Community webhook concepts, event types, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/community-webhooks/references/setup.md) - Configure a webhook in the Community Dashboard and get the signature secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/community-webhooks/references/verification.md) - `community-signature` verification details and gotchas