> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daily.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Waiting room (knock-to-join)

> Build a lobby where participants request access and owners admit or deny them in real time.

Daily's knock-to-join feature lets you gate entry to a room. Participants who arrive are held in a waiting state until an owner explicitly admits them. This guide covers room configuration, the waiting participant flow, and the admin flow.

## Room configuration

Knock-to-join requires two properties on the room:

* [`privacy: 'private'`](/reference/rest-api/rooms/create-room#body-properties-privacy) — places tokenless participants in the lobby instead of admitting them directly
* [`enable_knocking: true`](/reference/rest-api/rooms/create-room#body-properties-enable-knocking) — allows those lobby participants to ring for admission

```bash theme={null}
POST /v1/rooms
{
  "properties": {
    "privacy": "private",
    "enable_knocking": true
  }
}
```

<Note>
  Any participant who joins with a meeting token — no matter their permissions — bypasses the lobby and enters the call directly. Only tokenless participants are placed in the lobby.
</Note>

### Using Prebuilt

If you're using Daily Prebuilt, the lobby UI is built in. Once the room is configured above, also set `enable_prejoin_ui` on the [room](/reference/rest-api/rooms/create-room#body-properties-enable-prejoin-ui), [domain](/reference/rest-api/domain/set-domain-config#body-properties-enable-prejoin-ui), or [meeting token](/reference/rest-api/meeting-tokens/create-meeting-token#body-properties-enable-prejoin-ui) and you're done — Daily handles the waiting room experience for you.

The rest of this guide covers building a custom lobby UI with the call object API.

## Waiting participant flow

<Steps>
  <Step title="Join the room">
    Call `join()` without a token. The participant lands in the lobby and [`accessState()`](/reference/daily-js/instance-methods/access-state) returns `{ access: { level: 'lobby' } }` — they are connected but hidden from other participants.

    ```typescript theme={null}
    await call.join({ url: 'https://your-domain.daily.co/your-room' });
    // accessState() === { access: { level: 'lobby' } }
    ```
  </Step>

  <Step title="Request access">
    Call [`requestAccess()`](/reference/daily-js/instance-methods/request-access) with a display name. This notifies admins and adds the participant to the waiting list. The returned Promise **blocks until the request is decided** — you can use the return value directly:

    ```typescript theme={null}
    const { granted } = await call.requestAccess({ name: 'Alice' });

    if (granted) {
      showCallUI();
    } else {
      // Access denied — participant has been ejected from the call
      showRejectedUI();
    }
    ```
  </Step>

  <Step title="Alternatively, use events">
    If you need to react to access changes in multiple places (e.g. updating different parts of your UI), the event-based approach may be a better fit. [`access-state-updated`](/reference/daily-js/events/lifecycle-events#access-state-updated) fires when access is granted; a fatal [`error`](/reference/daily-js/events/error-events#error) event with `error.type === 'not-allowed'` fires when access is denied.

    ```typescript theme={null}
    call.on('access-state-updated', ({ access }) => {
      if (access.level === 'full') {
        showCallUI();
      }
    });

    call.on('error', ({ error }) => {
      if (error.type === 'not-allowed') {
        // Access was denied — participant has been removed from the call
        showRejectedUI();
      }
    });

    await call.requestAccess({ name: 'Alice' });
    ```
  </Step>
</Steps>

## Admin flow

<Steps>
  <Step title="Join with admin permissions">
    Join with a meeting token that grants `canAdmin: ['participants']` (or `is_owner: true`). Either bypasses the lobby and enters the call directly.

    ```typescript theme={null}
    await call.join({
      url: 'https://your-domain.daily.co/your-room',
      token: 'ADMIN_TOKEN', // token with canAdmin: ['participants'] or is_owner: true
    });
    ```

    See [meeting token permissions](/reference/rest-api/meeting-tokens/create-meeting-token#body-properties-permissions) for how to create a token with `canAdmin`.
  </Step>

  <Step title="Handle participants already waiting">
    Call [`waitingParticipants()`](/reference/daily-js/instance-methods/waiting-participants) immediately after joining to catch anyone who arrived before you.

    ```typescript theme={null}
    for (const [id, p] of Object.entries(call.waitingParticipants())) {
      showAdmitPrompt(p.id, p.name);
    }
    ```

    Each waiting participant is a `DailyWaitingParticipant`:

    ```typescript theme={null}
    interface DailyWaitingParticipant {
      id: string;    // unique ID for this waiting session
      name: string;  // display name passed to requestAccess()
      awaitingAccess: { level: 'full' };
    }
    ```
  </Step>

  <Step title="Listen for new arrivals">
    Subscribe to lobby events to keep your UI in sync:

    | Event                                                                                                      | Fires when                                                          |
    | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
    | [`waiting-participant-added`](/reference/daily-js/events/participant-events#waiting-participant-added)     | A participant calls `requestAccess()`                               |
    | [`waiting-participant-updated`](/reference/daily-js/events/participant-events#waiting-participant-updated) | A waiting participant calls `requestAccess()` again with a new name |
    | [`waiting-participant-removed`](/reference/daily-js/events/participant-events#waiting-participant-removed) | A participant is admitted, denied, or leaves on their own           |

    ```typescript theme={null}
    call.on('waiting-participant-added', ({ participant }) => {
      showAdmitPrompt(participant.id, participant.name);
    });

    call.on('waiting-participant-removed', ({ participant }) => {
      removeAdmitPrompt(participant.id);
    });
    ```
  </Step>

  <Step title="Admit or deny participants">
    Use [`updateWaitingParticipant()`](/reference/daily-js/instance-methods/update-waiting-participant) to act on one participant, or [`updateWaitingParticipants()`](/reference/daily-js/instance-methods/update-waiting-participants) to act on many at once.

    ```typescript theme={null}
    // Admit one
    await call.updateWaitingParticipant(id, { grantRequestedAccess: true });

    // Deny one
    await call.updateWaitingParticipant(id, { grantRequestedAccess: false });

    // Admit all but one currently waiting
    await call.updateWaitingParticipants([
      id: { grantRequestedAccess: false },
      '*': { grantRequestedAccess: true }
    ]);
    ```
  </Step>
</Steps>

## Complete example

Open `https://localhost?t=YOUR_ADMIN_TOKEN` for the admin view, or `https://localhost` (no token) for the waiting participant view.

<CodeGroup>
  ```javascript app.js theme={null}
  const ROOM_URL = 'https://your-domain.daily.co/your-room';

  // Pass an admin token via ?t=... to join as admin; omit for waiting participant
  const token = new URLSearchParams(window.location.search).get('t') ?? undefined;
  const isAdmin = Boolean(token);

  const call = Daily.createCallObject();

  if (isAdmin) {
    // --- Admin side ---
    document.getElementById('admin-view').hidden = false;
    document.getElementById('admin-status').textContent = 'Joining…';

    call.on('waiting-participant-added', ({ participant }) => {
      document.getElementById('no-waiters').hidden = true;

      const notice = document.createElement('div');
      notice.id = `waiting-${participant.id}`;
      notice.className = 'notice';
      notice.innerHTML = `
        <span>${participant.name || 'Someone'} wants to join</span>
        <button data-id="${participant.id}" data-action="admit">Admit</button>
        <button data-id="${participant.id}" data-action="deny">Deny</button>
      `;
      document.getElementById('lobby-notices').appendChild(notice);
    });

    call.on('waiting-participant-removed', ({ participant }) => {
      document.getElementById(`waiting-${participant.id}`)?.remove();
      if (Object.keys(call.waitingParticipants()).length === 0) {
        document.getElementById('no-waiters').hidden = false;
      }
    });

    document.getElementById('lobby-notices').addEventListener('click', async (e) => {
      const btn = e.target.closest('button');
      if (!btn) return;
      const { id, action } = btn.dataset;
      await call.updateWaitingParticipant(id, {
        grantRequestedAccess: action === 'admit',
      });
    });

    await call.join({ url: ROOM_URL, token });
    document.getElementById('admin-status').textContent = "You're in the call.";

    // Handle anyone already waiting when the admin joins
    const alreadyWaiting = call.waitingParticipants();
    if (Object.keys(alreadyWaiting).length === 0) {
      document.getElementById('no-waiters').hidden = false;
    }
    for (const p of Object.values(alreadyWaiting)) {
      call.emit('waiting-participant-added', { participant: p });
    }
  } else {
    // --- Waiting participant side ---
    document.getElementById('waiting-view').hidden = false;
    document.getElementById('waiting-status').textContent = 'Joining…';

    call.on('access-state-updated', ({ access }) => {
      if (access.level === 'full') {
        document.getElementById('waiting-view').hidden = true;
        document.getElementById('call-view').hidden = false;
      }
    });

    call.on('error', ({ error }) => {
      if (error.type === 'not-allowed') {
        document.getElementById('waiting-view').hidden = true;
        document.getElementById('rejected-view').hidden = false;
      }
    });

    await call.join({ url: ROOM_URL });
    // accessState() === { access: { level: 'lobby' } }

    const name = prompt('Your name') ?? 'Guest';
    document.getElementById('waiting-status').textContent =
      `Waiting to be admitted, ${name}…`;

    await call.requestAccess({ name });
    // accessState() === { access: { level: 'lobby' }, awaitingAccess: { level: 'full' } }
  }
  ```

  ```html index.html theme={null}
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Waiting Room Demo</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <!-- Admin view: always visible once joined as admin -->
    <div id="admin-view" hidden>
      <p class="status" id="admin-status"></p>
      <h2>Lobby</h2>
      <p id="no-waiters">No one waiting.</p>
      <div id="lobby-notices"></div>
    </div>

    <!-- Waiting participant view: shown while in the lobby -->
    <div id="waiting-view" hidden>
      <p class="status" id="waiting-status"></p>
    </div>

    <!-- Waiting participant view: shown once admitted -->
    <div id="call-view" hidden>
      <p class="status admitted">You're in the call.</p>
    </div>

    <!-- Waiting participant view: shown if denied -->
    <div id="rejected-view" hidden>
      <p class="status rejected">Your request to join was denied.</p>
    </div>

    <script src="https://unpkg.com/@daily-co/daily-js"></script>
    <script type="module" src="app.js"></script>
  </body>
  </html>
  ```

  ```css styles.css theme={null}
  body {
    font-family: sans-serif;
    background: #0f0f1a;
    color: #e0e0e0;
    margin: 0;
    padding: 1.5rem;
  }

  h2 {
    margin: 1rem 0 0.5rem;
    font-size: 1rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #a0a0c0;
  }

  .status {
    font-style: italic;
    color: #a0a0c0;
    margin: 0 0 0.5rem;
  }

  .status.admitted {
    color: #2ecc71;
    font-style: normal;
    font-weight: bold;
  }

  .status.rejected {
    color: #e94560;
    font-style: normal;
    font-weight: bold;
  }

  #no-waiters {
    color: #555577;
    font-style: italic;
  }

  .notice {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
    background: #1a1a2e;
    border-radius: 6px;
    margin-bottom: 0.5rem;
  }

  .notice span {
    flex: 1;
  }

  button {
    padding: 0.4rem 0.9rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.875rem;
  }

  button[data-action="admit"] {
    background: #2ecc71;
    color: #fff;
  }

  button[data-action="deny"] {
    background: #e94560;
    color: #fff;
  }
  ```
</CodeGroup>
