# How to add SMS, WhatsApp and phone approvals to CopilotKit and OpenDots agents

> CopilotKit's useHumanInTheLoop works while the user is in your app. Here is how to reach them by Slack, SMS or a phone call when they are not, using redphonr.

Source: https://blog.blueprintd.co/posts/human-approval-for-copilotkit-and-opendots-agents/
Type: Tutorial
Published: 3 October 2026

> **Disclosure:** We make redphonr. CopilotKit and OpenDots details come from their documentation and website as of October 2026. OpenDots is an alpha template, so check its repository for the latest structure.

CopilotKit makes in-app agents easy, and its `useHumanInTheLoop` hook is a good way to ask the user before the agent acts. It has one assumption built in: the user is looking at your app when the question comes up.

That holds for a copilot in a sidebar. It does not hold for an agent that runs overnight, works through a queue, or, like CopilotKit's new OpenDots template, is meant to be an always-on coworker. This tutorial shows how to cover both cases: keep `useHumanInTheLoop` for the user in front of you, and use [redphonr](https://www.redphonr.com/?utm_source=blog&utm_medium=post&utm_campaign=human-approval-for-copilotkit-and-opendots-agents) to reach everyone else.

> **In short:** Add the redphonr MCP server to your CopilotKit agent's `mcpServers`, tell the agent to call `request_approval` when nobody is in the conversation, and set the default to `deny`. The request reaches a person on Slack, SMS, WhatsApp or a phone call, escalates if they do not answer, and the agent resumes with their decision.

## Two kinds of human-in-the-loop

| | In the app | Outside the app |
|---|---|---|
| When | The user is in your UI right now | The agent runs in the background, or the approver is someone else |
| CopilotKit tool | `useHumanInTheLoop` renders an approval card | Not built in |
| How the person answers | Click in your app | Slack, SMS, WhatsApp, Telegram, Discord, email or a phone call |
| If they do not answer | The agent stays paused | redphonr escalates, then returns your default (deny) |
| Audit trail | Build your own | Every decision logged |

You need the left column for good in-app UX and the right column for anything that runs when no one is watching.

## The in-app part: useHumanInTheLoop

For completeness, this is the CopilotKit side. The hook registers a tool that pauses the agent and renders your component; calling `respond` resumes it:

```tsx
import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
import { z } from "zod";

useHumanInTheLoop({
  name: "confirmDelete",
  description: "Ask the user in the app before deleting records",
  parameters: z.object({
    itemName: z.string().describe("What will be deleted"),
    itemCount: z.number().describe("How many items"),
  }),
  render: ({ args, respond }) => (
    <ApprovalCard
      text={`Delete ${args.itemCount} ${args.itemName}?`}
      onApprove={() => respond?.("approved")}
      onDeny={() => respond?.("denied")}
    />
  ),
});
```

If the tab is closed, nobody sees the card. That is the gap the next steps fill.

## Step 1: get a redphonr API key

Create a free account at redphonr.com and copy an API key from the dashboard. It starts with `rp_live_`. Store it as `REDPHONR_API_KEY` in your server environment, never in client code.

## Step 2: connect the redphonr MCP server

CopilotKit's `BuiltInAgent` takes a list of MCP servers. Add redphonr over HTTP:

```ts
import { BuiltInAgent } from "@copilotkit/runtime/v2";

const agent = new BuiltInAgent({
  model: "openai:gpt-5.4-mini",
  mcpServers: [
    {
      type: "http",
      url: "https://mcp.redphonr.com/mcp",
      // HTTP options are passed to the MCP SDK's StreamableHTTPClientTransport.
      options: {
        requestInit: {
          headers: { Authorization: `Bearer ${process.env.REDPHONR_API_KEY}` },
        },
      },
    },
  ],
});
```

The agent now has `request_approval`, `ask_human`, `collect_input`, `verify_output`, `handoff` and `alert_human` alongside its other tools.

## Step 3: tell the agent which one to use

Two approval tools only help if the agent knows when to pick each. Add this to the agent's instructions:

```text
Before any irreversible or costly action (deletes, payments, deploys, access changes, bulk email):
- If the user is in this conversation, call confirmDelete (or your in-app approval tool).
- If you are running in the background, or the action needs someone else's approval,
  call redphonr's request_approval with the full context and default "deny".
Never proceed if the answer is not "approved".
```

## Step 4: decide how people are reached

In the redphonr dashboard, open **Reach me** and set the order, for example Slack immediately, SMS after five minutes, a phone call after fifteen. For a team, create an escalation flow that goes to whoever is on call this week, then their backup.

## Step 5: test the "no" path

Trigger a gated action with no one in the app. You should get a Slack card or a text within seconds. Deny it, and check that the agent stops and reports what it did not do. Then let a request time out and check that it is treated as denied.

## Where OpenDots fits

OpenDots is CopilotKit's open-source template for always-on AI coworkers: each Dot has its own role, permissions and sandbox, and you work with it through a web editor, voice calls and Slack. Its approval model is reviewing a Dot's work before it is saved.

Always-on is exactly when redphonr earns its place. A Dot working through the night will hit decisions nobody is online to review. Where the template configures a CopilotKit agent, add the redphonr server from step 2 and the instructions from step 3, and the Dot can reach a person by phone or SMS instead of waiting for someone to open the editor. The template is in alpha, so look for where it defines each Dot's agent and tools in its repository.

## Why not build this part yourself?

You can. A Slack bot and a Twilio number get you a first version. The parts that take longer are the ones you only need when something goes wrong: escalating when nobody answers, on-call rotation, closing the request on every channel once someone replies, a deny default on timeout, and a log of who approved what. The [human-in-the-loop guide](https://blog.blueprintd.co/posts/human-in-the-loop-ai-agents-guide/) covers those design choices, and the [comparison of human-in-the-loop tools](https://blog.blueprintd.co/posts/best-human-in-the-loop-tools-for-ai-agents/) lists other options.

**[Add phone and SMS approvals to your agent](https://www.redphonr.com/?utm_source=blog&utm_medium=post&utm_campaign=human-approval-for-copilotkit-and-opendots-agents)**

## Frequently asked questions

### Does CopilotKit have human-in-the-loop?

Yes. The useHumanInTheLoop hook registers a tool that pauses the agent and renders your own UI, such as an approve and deny card, in the app. The agent stays paused until your component calls respond with the user's answer.

### What happens if the user is not in the app when a CopilotKit agent needs approval?

useHumanInTheLoop renders inside your app, so someone has to be looking at it. For agents that run in the background, add a delivery layer such as redphonr, which reaches the person on Slack, SMS, WhatsApp or a phone call and returns their answer to the agent.

### Can a CopilotKit agent use MCP servers?

Yes. CopilotKit's BuiltInAgent accepts an mcpServers list with HTTP or SSE transports, so you can connect the redphonr MCP server and the agent gets request_approval, ask_human and the other redphonr tools.

### Is OpenDots a replacement for redphonr?

No. OpenDots is an open-source, self-hosted template for building always-on AI coworkers with web, voice and Slack interfaces. Its approval model is reviewing a Dot's work in its own interface. redphonr is a managed service any agent can call to reach a person anywhere, with escalation, on-call rotation and deny on timeout. The two work together.
