> ## 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.",
    )
    ```
  </Step>

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

    Without this, inline-button callbacks stop working across bot restarts.
  </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).

<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(PRAISONAI_CALLBACK_SECRET, payload)`
* **Verification**: Tampered `callback_data` fails verification and is silently ignored + logged

<Warning>
  Without `PRAISONAI_CALLBACK_SECRET` set in your environment, a random per-process secret is used and inline buttons stop working after bot restart. Always set this in production:

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

***

## 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/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="Always set PRAISONAI_CALLBACK_SECRET in production">
    Generate a strong secret and set it as an environment variable:

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

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

    Without this, inline buttons stop working when your bot restarts.
  </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/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>
