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

# Bot Unknown-User Pairing

> Owner-DM inline-button approval for unknown users on Telegram, Discord, and Slack bots

<Note>
  Bot platform adapters now ship in the `praisonai-bot` package. `praisonai bot serve` still works exactly as documented here; for a standalone install see [praisonai-bot Migration](/docs/guides/praisonai-bot-migration).
</Note>

Bot pairing enables owner-approval for unknown users with inline Approve/Deny buttons sent directly to your DM.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, BotConfig

agent = Agent(name="assistant", instructions="You are a helpful assistant.")
config = BotConfig(unknown_user_policy="pair", owner_user_id="123456789")
agent.start("Unknown user requests access — notify the owner.")
```

The user messages the bot before they are allowlisted; the owner receives Approve/Deny buttons in DM.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Unknown User Pairing Flow"
        A[👤 Unknown User] --> B[🤖 Bot]
        B --> C[🔒 Generate Code]
        C --> D[📩 Owner DM]
        D --> E{✅❌ Decision}
        E -->|Approve| F[🟢 User Paired]
        E -->|Deny| G[🔴 Request Denied]
        F --> H[💬 Future Messages]
    end
    
    classDef agent fill:#8B0000,color:#fff
    classDef tool fill:#189AB4,color:#fff
    
    class A,F,H agent
    class B,C,D,E,G tool
```

## Quick Start

<Steps>
  <Step title="Enable Pairing">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents import BotConfig

    config = BotConfig(
        token="YOUR_BOT_TOKEN",
        unknown_user_policy="pair",
        owner_user_id="123456789",   # your Telegram/Discord/Slack user ID
    )

    agent = Agent(
        name="Support",
        instructions="You are a helpful support assistant.",
    )
    ```

    The same config from `gateway.yaml` — both keys are read per-channel ([PR #2856](https://github.com/MervinPraison/PraisonAI/pull/2856)):

    ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    channels:
      telegram:
        token: ${TELEGRAM_BOT_TOKEN}
        unknown_user_policy: pair
        owner_user_id: ${TELEGRAM_OWNER_ID}
        routes:
          default: assistant
    ```
  </Step>

  <Step title="Multi-host deployments (optional)">
    On a single host, callback signing works out of the box — the signing key is
    the persisted gateway secret at `~/.praisonai/gateway/.gateway_secret`
    (mode `0600`, auto-created on first use), so buttons survive restarts with no
    operator action.

    Set `PRAISONAI_CALLBACK_SECRET` to a shared value **only** when running the
    gateway on multiple hosts/replicas so each verifies the same signatures:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User as Unknown User
    participant Bot
    participant Store as PairingStore
    participant Owner

    User->>Bot: DM
    Bot->>Store: is_paired? → No
    Bot->>Store: generate_code()
    Bot->>Owner: DM with Approve / Deny buttons
    Bot-->>User: "Your request has been sent..."
    Owner->>Bot: Taps Approve (HMAC-signed callback with platform type)
    Bot->>Store: verify_and_pair()
    Bot-->>User: "You've been approved! Send me a message."
    User->>Bot: Future DMs flow straight through
```

The pairing system intercepts messages from users not in `allowed_users` and routes them through an approval workflow controlled by the `unknown_user_policy`, publishing a `pairing_approved` event on the default EventBus after every successful approval (CLI, inline button, or web UI).

<Note>
  The pairing flow now fires only when `BotConfig.is_explicitly_allowed(user_id)` returns `False` (empty allowlist, or user not on it). Users on `allowed_users` skip pairing entirely — PR [#2856](https://github.com/MervinPraison/PraisonAI/pull/2856).
</Note>

<Info>
  **Inline approvals (PR #1892)**: When the owner taps **Approve** on the Telegram / Discord / Slack DM, the requester is paired immediately on the platform type (`telegram` / `discord` / `slack`) so all of their later messages flow straight through. If you saw "approve looked successful but the next message was still blocked" on an older release, upgrade to pick up this fix.
</Info>

<Info>
  **Gateway mode (`praisonai gateway start`)**: As of PR #1791, the gateway polling path runs the same pairing pipeline as the standalone bot. If `pairing.enabled: true` is set in your gateway config, unknown users get the same Approve/Deny inline-button flow. No code changes needed — configure `unknown_user_policy: "pair"` and `owner_user_id` in your `channels:` YAML entry.
</Info>

<Warning>
  **Behavior change in PR #1885 (Telegram only).** Before this fix, Telegram bots with an empty `allowed_users` list ignored `unknown_user_policy` and let everyone through — `"pair"` only worked when `allowed_users` was non-empty. After the fix, empty `allowed_users` correctly routes every Telegram user through the pairing flow when the policy is `"pair"`. If you were running with an empty allowlist and `"pair"` (or default `"deny"`), upgrading will start enforcing the policy. Set `unknown_user_policy: "allow"` to restore the old "anyone can message" behavior. Discord and Slack already behaved correctly and are unchanged.
</Warning>

***

## Policy Options

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    MSG["📩 Message from Unknown User"] --> POLICY{"unknown_user_policy?"}
    
    POLICY -->|"deny"| DENY["🔇 Silently drop message"]
    POLICY -->|"pair"| PAIR["🔗 Generate pairing code"]
    POLICY -->|"allow"| ALLOW["✅ Process message directly"]
    
    PAIR --> OWNER_ID{"owner_user_id set?"}
    OWNER_ID -->|"Yes"| INLINE["📱 Inline approval buttons"]
    OWNER_ID -->|"No"| CLI["📝 CLI fallback instructions"]
    
    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef deny fill:#EF4444,stroke:#7C90A0,color:#fff
    classDef allow fill:#10B981,stroke:#7C90A0,color:#fff
    classDef pair fill:#189AB4,stroke:#7C90A0,color:#fff
    
    class MSG input
    class POLICY,OWNER_ID check
    class DENY deny
    class ALLOW allow
    class PAIR,INLINE,CLI pair
```

| Policy             | Behaviour                                                                                                 | When to use                                                       |
| ------------------ | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `"deny"` (default) | Silently drops messages from users not in `allowed_users`.                                                | Closed / internal bots.                                           |
| `"pair"`           | Generates a code and DMs the owner an Approve/Deny button. Falls back to CLI if `owner_user_id` is unset. | Semi-public bots where you want owner control.                    |
| `"allow"`          | Lets every unknown user through (no persistent pair).                                                     | Fully public bots (combine with rate limits / approval protocol). |

### Which Policy Should I Choose?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TD
    START["🤖 Setting up a bot?"] --> USERS{"Who should use it?"}
    
    USERS -->|"Only me/team"| INTERNAL["Internal Bot"]
    USERS -->|"Selected people"| GATED["Gated Bot"]  
    USERS -->|"Anyone"| PUBLIC["Public Bot"]
    
    INTERNAL --> DENY_REC["✅ Use 'deny'<br/>+ allowed_users list"]
    GATED --> PAIR_REC["✅ Use 'pair'<br/>+ owner_user_id"]
    PUBLIC --> ALLOW_REC["✅ Use 'allow'<br/>+ rate limiting"]
    
    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef recommendation fill:#10B981,stroke:#7C90A0,color:#fff
    
    class START start
    class USERS question
    class INTERNAL,GATED,PUBLIC option
    class DENY_REC,PAIR_REC,ALLOW_REC recommendation
```

***

## CLI Fallback

When `owner_user_id` is not set, the bot replies to the requester:

```
Your pairing code: abc12345. Ask the owner to run: praisonai pairing approve telegram abc12345
```

The owner can then approve manually:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai pairing approve <channel_type> <code>
```

Where `<channel_type>` is one of: `telegram`, `discord`, `slack`.

***

## Security: HMAC-signed Callbacks

<Info>
  **Internals (PR #2108):** Pairing approve/deny now dispatches through the generic `InteractiveRegistry` under the reserved `pair` namespace. The behavior, callback format, and HMAC verification are identical — but you can now register your own handlers under different namespaces (e.g. `approval`, `menu`) on the same adapter without conflicting. See [Interactive Bot Actions](/docs/features/interactive-bot-actions).
</Info>

All inline-button callbacks are cryptographically signed to prevent tampering:

* **Callback format**: `pair:{action}:{channel}:{user_id}:{code}:{sig}`
* **Signature**: First 8 hex chars of `HMAC-SHA256(callback_secret, payload)`,
  where `callback_secret` is `PRAISONAI_CALLBACK_SECRET` when set, otherwise
  the persisted gateway secret from `~/.praisonai/gateway/.gateway_secret`.
* **Verification**: Tampered `callback_data` fails verification and is silently ignored + logged

<Note>
  Callback and pairing-code signing share the same HMAC secret at
  `~/.praisonai/gateway/.gateway_secret` (mode `0600`), auto-created on first
  use. Inline buttons survive bot restarts by default — no operator action
  required on a single host.

  * **Single host, default install:** nothing to do — buttons and codes just work across restarts.
  * **Multiple hosts / replicas:** set `PRAISONAI_CALLBACK_SECRET` to the same value on every host.
  * **Explicit control:** setting `PRAISONAI_CALLBACK_SECRET` overrides the persisted secret — useful for rotation or shared-vault-managed environments.

  ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
  ```
</Note>

***

## Platform-specific UI

<AccordionGroup>
  <Accordion title="Telegram">
    Uses `InlineKeyboardMarkup` with ✅ Approve / ❌ Deny buttons. Callbacks are handled via `CallbackQueryHandler` that parses the signed `callback_data` and verifies the HMAC signature.

    **What the owner sees:**

    ```
    User @username wants to chat. Approve access?
    [✅ Approve] [❌ Deny]
    ```

    **Implementation:** Telegram's `InlineKeyboardButton` with `callback_data` containing the signed pairing payload. The `{channel}` field in the `pair:{action}:{channel}:{user_id}:{code}:{sig}` format contains the platform identifier (`telegram`), not the chat ID.
  </Accordion>

  <Accordion title="Discord">
    Uses `discord.ui.View` with success (green) and danger (red) button styles. Handled via `button.callback` method that verifies the HMAC signature in the `custom_id`.

    **What the owner sees:**

    ```
    User username#1234 wants to chat. Approve access?
    [✅ Approve] [❌ Deny]
    ```

    **Implementation:** Discord's Button components in an Action Row with HMAC-signed `custom_id` values. The `{channel}` field in the callback format is the platform identifier (`discord`).
  </Accordion>

  <Accordion title="Slack">
    Uses Block Kit `actions` block with primary (blue) and danger (red) button styles. Handled via `@app.action("pair_approve")` and `@app.action("pair_deny")` decorators that verify the signature in the button's `value`.

    **What the owner sees:**

    ```
    *@username* wants to chat. Approve access?
    [✅ Approve] [❌ Deny]
    ```

    **Implementation:** Slack Block Kit buttons with HMAC-signed values and dedicated action handlers. The `{channel}` field in the callback format is the platform identifier (`slack`).
  </Accordion>
</AccordionGroup>

***

## Configuration Options

For the complete `BotConfig` options including `unknown_user_policy` and `owner_user_id`, see the canonical reference at [Messaging Bots Configuration](/docs/docs/features/messaging-bots#configuration-options).

***

## Common Patterns

<Tabs>
  <Tab title="Semi-public Bot">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents import BotConfig

    config = BotConfig(
        token="your-bot-token",
        unknown_user_policy="pair",    # Enable approval workflow
        owner_user_id="123456789",     # Your platform user ID
    )

    agent = Agent(
        name="Support Bot",
        instructions="Help users with their questions",
    )
    ```

    Perfect for customer support or community bots where you want to vet new users.
  </Tab>

  <Tab title="Internal Bot">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent  
    from praisonaiagents import BotConfig

    config = BotConfig(
        token="your-bot-token",
        unknown_user_policy="deny",           # Block unknown users
        allowed_users=["user123", "user456"], # Explicit allowlist
    )

    agent = Agent(
        name="Internal Assistant", 
        instructions="Help with internal tasks",
    )
    ```

    For team or company-internal bots with a fixed user list.
  </Tab>

  <Tab title="Public Bot">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents import BotConfig

    config = BotConfig(
        token="your-bot-token", 
        unknown_user_policy="allow",     # Open to everyone
        auto_approve_tools=False,        # Still require tool approval
    )

    agent = Agent(
        name="Public Assistant",
        instructions="Help anyone with general questions",
    )
    ```

    For fully public bots. Combine with rate limiting and tool approval for safety.
  </Tab>

  <Tab title="Gateway YAML">
    ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    channels:
      telegram_support:
        token: ${TELEGRAM_BOT_TOKEN}
        allowed_users: ["123456789"]
        unknown_user_policy: "pair"
        owner_user_id: "123456789"
    ```

    Gateway mode configuration for the pairing workflow.
  </Tab>
</Tabs>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Share the callback secret only across multiple hosts">
    On a single host, inline buttons survive restarts by default — the signing key
    is the persisted gateway secret at `~/.praisonai/gateway/.gateway_secret`
    (mode `0600`, auto-created on first use). No environment variable is needed.

    Set `PRAISONAI_CALLBACK_SECRET` to a shared value only when running the gateway
    on multiple hosts/replicas so each verifies the same signatures:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Generate and export the same value on every host
    export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"

    # Or set permanently in your deployment
    echo "PRAISONAI_CALLBACK_SECRET=$(openssl rand -hex 32)" >> .env
    ```
  </Accordion>

  <Accordion title="Use platform-native user IDs for owner_user_id">
    Each platform has its own user ID format:

    * **Telegram**: Numeric ID (e.g., `123456789`)
    * **Discord**: Snowflake ID (e.g., `123456789012345678`)
    * **Slack**: User ID format (e.g., `U1234ABCD`)

    Find your ID by messaging the bot directly and checking the logs, or use platform-specific methods.
  </Accordion>

  <Accordion title="Combine 'allow' policy with rate limiting and tool approval">
    If using `unknown_user_policy="allow"` for a public bot, protect yourself with:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    config = BotConfig(
        unknown_user_policy="allow",
        auto_approve_tools=False,     # Users still need approval for dangerous tools
        debounce_ms=2000,            # Coalesce rapid messages
    )
    ```

    Consider also implementing rate limiting at the platform level.
  </Accordion>

  <Accordion title="Treat denied pairings as final">
    When you deny a pairing request, the code is consumed and cannot be retried. The user must send a new message to generate a fresh code. This prevents spam and ensures each approval decision is deliberate.
  </Accordion>
</AccordionGroup>

***

<Note>
  Pair a user with a non-empty `--label` (the canonical id) and `StoreBackedIdentityResolver` automatically unifies their session across every channel paired under the same label. No separate `praisonai identity link` calls needed. See [Cross-Platform Sessions › StoreBackedIdentityResolver](/docs/docs/features/cross-platform-mirror#storebackedidentityresolver).
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Messaging Bots" icon="robot" href="/docs/features/messaging-bots">
    Complete bot configuration and setup
  </Card>

  <Card title="Bot Security" icon="shield" href="/docs/best-practices/bot-security">
    Security best practices for bots
  </Card>

  <Card title="Web-UI HTTP API Approval" icon="globe" href="/docs/features/bot-pairing#web-ui-http-api-approval">
    HTTP API endpoints for web admin UI approval
  </Card>

  <Card title="Cross-Platform Sessions" icon="users-rectangle" href="/docs/features/cross-platform-mirror">
    Unified per-user sessions across platforms
  </Card>
</CardGroup>
