---
title: "Authorizing channels"
description: "Sign private and presence channel subscriptions from your backend so only the clients you approve can subscribe, and presence members carry an identity."
canonical: "https://bird.com/docs/guides/realtime/authorizing-channels"
---

# Authorizing channels

Any client holding the app key can subscribe to public channels. Two channel-name prefixes require your backend to authorize the subscription. You do not configure channels separately.

A channel named `private-…` requires your backend to approve each subscription. A channel named `presence-…` does the same and also attaches an identity to the subscriber, so everyone on the channel can see who else is there. Any other name is public.

Only your backend holds the app secret. The client asks your server to sign a specific subscription, and the Realtime edge verifies that signature before accepting it. Your server decides whether the caller may subscribe without exposing the secret to the client.

## Point the client at your endpoint

Give the client an `authEndpoint` on your own backend:

**TypeScript**

```typescript
import { BirdRealtime } from "@messagebird/realtime";

const bird = new BirdRealtime({
  appKey: "your-app-key",
  region: "us1",
  authEndpoint: "/bird/auth",
});

const room = bird.subscribe("presence-room-1");
```

Examples: [TypeScript](/docs/guides/realtime/authorizing-channels.ts.md) · [Swift](/docs/guides/realtime/authorizing-channels.swift.md) · [Kotlin](/docs/guides/realtime/authorizing-channels.kotlin.md)

The client calls this endpoint for each private or presence subscription, including subscriptions restored after a reconnect. Authorization applies to one connection because the signature includes its connection ID.

The browser client requires a same-origin endpoint by default. Set `allowCrossOriginAuth: true` to use a cross-origin `authEndpoint`. The browser client sends configured `authHeaders` only to same-origin endpoints.

## What your endpoint receives and returns

The client POSTs JSON:

```json
{ "connection_id": "26896.319537", "channel_name": "presence-room-1" }
```

Respond with the signature:

```json
{ "auth": "your-app-key:8f9a…" }
```

For a presence channel, also return the member identity as a JSON **string**, the same string you signed:

```json
{
  "auth": "your-app-key:8f9a…",
  "member_data": "{\"member_id\":\"u_42\",\"member_info\":{\"name\":\"Ada\"}}"
}
```

`member_id` is the identity other members see and the value the disconnect operation targets. `member_info` is optional JSON data delivered to every channel member. It has a 1 KB limit, so include only small, non-sensitive profile data.

Authorize the caller in this endpoint using its session cookie or bearer token. Return `403 Forbidden` when the caller must not join the channel. For presence channels, assign the identity in the same response.

## The string you sign

Concatenate with colons, then HMAC-SHA256 with the app secret and hex-encode. Prefix the result with the app key and a colon.

| Channel type          | String to sign                                 |
| --------------------- | ---------------------------------------------- |
| `private-…`           | `<connection_id>:<channel_name>`               |
| `private-encrypted-…` | `<connection_id>:<channel_name>`               |
| `presence-…`          | `<connection_id>:<channel_name>:<member_data>` |

For presence channels, sign the exact `member_data` string you return. Re-serializing the same object can change its key order or spacing and invalidate the signature.

An encrypted channel signs like a private one, and its auth response additionally returns the channel's decryption key as `shared_secret`. The SDK helper adds it automatically; [Encrypted channels](/docs/guides/realtime/encrypted-channels) covers the derivation and channel behavior.

Each server SDK provides an `authorizeChannel` helper. It signs with the configured app credentials and returns the response body without making a network request. For encrypted channels, the helper also adds `shared_secret`.

**TypeScript**

```typescript
app.post("/bird/auth", async (req, res) => {
  const { connection_id, channel_name } = req.body;

  // Your own authorization decision goes here.
  const user = getUserFromSession(req);
  if (!user || !mayJoin(user, channel_name)) return res.sendStatus(403);

  const memberData = channel_name.startsWith("presence-")
    ? JSON.stringify({ member_id: user.id, member_info: { name: user.name } })
    : undefined;

  res.json(
    await bird.realtime.authorizeChannel({
      connectionId: connection_id,
      channelName: channel_name,
      memberData,
    }),
  );
});
```

Examples: [TypeScript](/docs/guides/realtime/authorizing-channels.ts.md) · [Python](/docs/guides/realtime/authorizing-channels.py.md) · [Go](/docs/guides/realtime/authorizing-channels.go.md) · [PHP](/docs/guides/realtime/authorizing-channels.php.md)

The signing contract is the same in a language without an SDK: HMAC-SHA256 the string with the app secret, hex-encode it, and prefix it with the app key and a colon.

## Members and connections

A member is an identity, while a connection is one open WebSocket. If someone opens your app in three tabs, one member holds three connections. `member_added` fires when the first connection subscribes, and `member_removed` fires when the last one leaves. Other connections change the channel's connection count without producing member events.

## Common failures

A rejected subscription arrives as a client error. Check these common causes:

- **Invalid signature.** The string you signed doesn't match. Almost always a re-serialized `member_data`, or a signature computed over the channel name without its `private-` or `presence-` prefix.
- **Invalid key.** The key in `auth` belongs to a different app, or it has been revoked. Rotating keys means updating both the client's `appKey` and the secret your endpoint signs with.
- **Missing member data.** A presence subscription arrived without `member_data`. Presence channels cannot be joined anonymously.
- **A `403` from your own endpoint.** Your authorization decision refused, which is the intended outcome for a user who may not join.

## Next steps

- [Send your first realtime event](/docs/get-started/send-your-first-realtime-event) is the end-to-end walkthrough this guide builds on.
- [Encrypted channels](/docs/guides/realtime/encrypted-channels) build on this signature to also hand approved subscribers a decryption key.
- [Presence channels](/docs/guides/realtime/presence-channels) cover the member list, member events, and reading presence from your server.
- [Terminating member connections](/docs/guides/realtime/terminating-member-connections) is the other signature your backend computes, and the one that lets you close a member's connections.
- [Webhooks & events](/docs/guides/webhooks) covers `realtime.*` events, including members joining and leaving, on your own endpoint.

## Related resources

- [Realtime](/realtime-api) (product)
- [Build your first integration](/learn/paths/integration) (course)

[Get an implementation brief](/learn/workspace?topic=realtime)
