# Guide to Sendbird Webhooks: Features and Best Practices

Sendbird webhooks tell your server what is happening inside your chat application: a message is sent, edited or deleted, a user joins or leaves a channel, a channel is frozen, a user is blocked, banned or reported, or the profanity filter catches something. Teams use them to send push, SMS or email notifications to offline users, moderate content, sync chat activity to a CRM, or feed analytics.

This guide covers webhooks for Sendbird Chat (Platform API v3): how they are configured and signed, the events and their payloads, the short timeout and retry behaviour, and the best practices for running a Sendbird receiver in production.

## What are Sendbird webhooks?

Each Sendbird application has one webhook endpoint. You set its URL and the events to send on the Sendbird Dashboard, and when one of those events happens, Sendbird sends an HTTP `POST` with a JSON body to the endpoint.

Every request carries an `x-sendbird-signature` header: a hex HMAC-SHA256 of the request body, keyed with the application's master API token. There is no separate webhook secret, and secondary API tokens don't work.

Requests also carry an older `x-signature` header. Sendbird says it never matches when the body contains non-ASCII characters, such as emoji, and that it may be deprecated. Verify `x-sendbird-signature` and ignore `x-signature`.

## Sendbird webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Settings > Chat > Webhooks** on the Sendbird Dashboard |
| Endpoints | One per application |
| Endpoint requirements | HTTP/1.1 with keep-alive, accepts `POST`, parses JSON |
| Signature header | `x-sendbird-signature` |
| Scheme | HMAC-SHA256, lowercase hex |
| Signed content | The raw request body |
| Key | The application's master API token |
| Legacy header | `x-signature`, which fails on non-ASCII bodies. Don't use it |
| Replay window | None. No timestamp is signed |
| Event name | `category` in the body, as `<resource>:<action>` |
| Other headers | `user-agent: SendBird`, `content-type: application/json` |
| Response window | 5 seconds |
| Retries | The same request is sent up to three times until a response arrives |

Sendbird publishes no source IP list for webhooks. Its IP whitelist setting restricts inbound API calls, not where webhooks come from.

## Common events

Sendbird documents 48 events. The name is in `category` and uses a colon.

| Area | Events |
| --- | --- |
| Open channels | `open_channel:create`, `:remove`, `:enter`, `:exit`, `:message_send`, `:message_update`, `:message_delete` |
| Group channels | `group_channel:create`, `:changed`, `:remove`, `:invite`, `:decline_invite`, `:join`, `:leave`, `:message_send`, `:message_read`, `:message_update`, `:message_delete`, `:freeze_unfreeze`, `:reaction_add`, `:reaction_delete` |
| Users | `user:block`, `user:unblock`, `user:mute`, `user:ban`, `user:deactivate`, `user:push_preference`, `user:push_channel_preference`, `user:device_token_register`, `user:device_token_unregister` |
| Operators | `operators:register_by_operator`, `operators:unregister_by_operator` |
| Reports | `message:report`, `user:report`, `open_channel:report`, `group_channel:report` |
| Moderation | `profanity_filter:replace`, `:block`, `:moderate`, `:detect`, `advanced_profanity_filter:block`, `:no_action`, `:replace`, `message:moderation_extension_triggered`, `image_moderation:block` |
| Other | `alert:user_message_rate_limit_exceeded`, `announcement:create_channels`, `announcement:send_messages` |

`group_channel:message_read` fires when a user has no more unread messages in a channel, not for every message they read. `user:mute` and `user:ban` cover both a single channel and the whole application.

Here is Sendbird's `open_channel:create` example:

```json
{
  "category": "open_channel:create",
  "created_at": 1540866408000,
  "operators": [
    {
      "user_id": "Jay",
      "nickname": "Mighty",
      "profile_url": "https://sendbird.com/main/img/profiles/profile_26_512px.png",
      "metadata": {}
    }
  ],
  "channel": {
    "name": "Jeff and friends",
    "channel_url": "sendbird_open_channel_1_2681099203cd6b78414fe672927a43fcf3a30f09",
    "custom_type": "",
    "is_distinct": false,
    "is_public": false,
    "is_super": false,
    "is_ephemeral": false,
    "is_discoverable": false,
    "data": ""
  },
  "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

```

Every payload has `category` and `app_id`. The rest depends on the event: `channel`, `sender`, `members`, `user`, `blocker` and so on. Timestamps such as `created_at` are in milliseconds, and channels are identified by `channel_url`.

## Setting up Sendbird webhooks

1. On the Sendbird Dashboard, go to **Settings > Chat > Webhooks**.
2. Turn webhooks on, enter your endpoint URL, and choose the events to send.
3. Copy the application's **master API token** from the dashboard and store it server-side, for example as `SENDBIRD_MASTER_API_TOKEN`.

If you use Hookdeck, create a Sendbird source, paste the master API token into it, and use the Hookdeck source URL as the webhook endpoint.

Your endpoint must support HTTP/1.1 with keep-alive. Most web frameworks and load balancers do this by default, but check any proxy that sits in front of your receiver.

## Securing Sendbird webhooks

Compute an HMAC-SHA256 of the raw request body with the master API token, encode it as hex, and compare it with `x-sendbird-signature` in constant time.

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

function verifySendbirdSignature(rawBody, signatureHeader, masterToken) {
  if (!signatureHeader) return false;

  const expected = crypto.createHmac('sha256', masterToken).update(rawBody).digest('hex');

  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/sendbird', express.raw({ type: '*/*' }), (req, res) => {
  const token = process.env.SENDBIRD_MASTER_API_TOKEN;
  if (!token) return res.sendStatus(500); // fail closed

  // Use x-sendbird-signature. The legacy x-signature fails on non-ASCII bodies.
  if (!verifySendbirdSignature(req.body, req.get('x-sendbird-signature'), token)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.category. Persist the event, then process it asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_sendbird_signature(raw_body: bytes, signature_header, master_token: str) -> bool:
    if not signature_header:
        return False
    expected = hmac.new(master_token.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    received = signature_header.strip().lower().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_sendbird_signature(raw_body, request.headers.get("x-sendbird-signature"),
#                           os.environ["SENDBIRD_MASTER_API_TOKEN"])

```

Sendbird's own Python sample builds the payload as a string and compares with `==`. Hash the raw bytes you received instead, and compare in constant time.

To check your verifier, including non-ASCII handling, use the token `sendbird-test-master-token` and this exact body, encoded as UTF-8:

```
{"category":"group_channel:message_send","payload":{"message":"Hello 👋"},"app_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}

```

The signature should be:

```
855298b4fbc3df6592adf293c8e30aa98f548306078af48342aa22abe3406183

```

> **Make Sendbird webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `x-sendbird-signature` header, answers Sendbird without waiting for your service, and durably queues every message and channel event before your service sees it.

## Sendbird webhook limitations and pain points

### The signing key is your master API token

**The Problem:** Every service that verifies Sendbird webhooks needs the application's most powerful credential.

**Why It Happens:** Sendbird signs with the master API token, and secondary tokens don't verify. The master token has full access to the Platform API.

**Workarounds:**

* Verify in one place, then pass verified events on, so only that component holds the token.
* Keep the token in a secrets manager and out of logs.

**How Hookdeck Can Help:** Hookdeck verifies the signature with the token stored on the source, so your services receive already-verified events and never need the master token for webhooks.

### Five seconds and three attempts

**The Problem:** Slow handlers cause duplicate deliveries, and an outage loses events.

**Why It Happens:** Sendbird waits five seconds for a response and sends the same request up to three times until it gets one. Sendbird warns that synchronous processing can stop the endpoint working when many events happen at once. After the third attempt, the event isn't sent again.

**Workarounds:**

* Respond `200` immediately and process asynchronously, as Sendbird recommends.
* Deduplicate, because a slow first attempt and its retry can both be processed.
* After an outage, use Sendbird's **Get failed webhooks** API to see what failed, and reconcile through the Platform API.

**How Hookdeck Can Help:** Hookdeck answers Sendbird as soon as each request is stored, then delivers to your service with its own retries, rate limits and replay, so bursts and outages don't lose events.

### The legacy `x-signature` header

**The Problem:** Verification works in testing and fails in production on messages with emoji or accented text.

**Why It Happens:** Sendbird still sends `x-signature`, and on non-ASCII bodies it never matches. Code that verifies against it passes on simple English test messages.

**Workarounds:**

* Verify `x-sendbird-signature` only.
* Include non-ASCII characters in your verification tests, such as the test vector above.

**How Hookdeck Can Help:** Hookdeck's Sendbird source verifies `x-sendbird-signature` and ignores `x-signature`.

### High volume from busy apps

**The Problem:** A popular chat app sends a request for every message, and your receiver can't keep up.

**Why It Happens:** Message events fire for every message in every channel you subscribe to. There's no batching.

**Workarounds:**

* Subscribe only to the events you act on.
* Filter early, for example on `channel.custom_type`, and drop what you don't need before doing any work.

**How Hookdeck Can Help:** Hookdeck filters can drop events by `category` or any body field before they reach your service, and destination rate limits smooth out bursts.

### No event ID or replay protection

**The Problem:** You can't tell a retry from a new event by an ID, and a captured request still verifies.

**Why It Happens:** Sendbird documents no delivery or event ID, and the signature covers only the body.

**Workarounds:**

* Use IDs in the payload, such as a message's `message_id`, or build a key from `category`, `channel_url` and the payload's timestamp.
* Keep the endpoint on HTTPS.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeated deliveries based on the fields you choose.

## Best practices

### Verify against the raw body

The signature covers the exact bytes Sendbird sent, including multibyte UTF-8. In Express, mount `express.raw()` on the webhook route and parse after verification. In a Next.js route handler, use `Buffer.from(await request.arrayBuffer())`. In FastAPI, read `await request.body()`.

### Route on `category`

Split `category` on the colon to get the resource and action, and acknowledge categories you don't handle with `200`.

### Keep test and production apps separate

Each application has its own endpoint and master token. Point each environment's app at its own endpoint, and check `app_id` before acting.

### Acknowledge fast and process asynchronously

See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Sendbird signs each Chat webhook with a hex HMAC-SHA256 of the raw body in `x-sendbird-signature`, keyed with the application's master API token. Ignore the legacy `x-signature`, verify against the raw UTF-8 bytes, route on `category`, deduplicate, and answer within five seconds, because Sendbird only makes three attempts.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks every Sendbird signature, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Sendbird webhooks reliably in minutes.