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

# Agent onboarding API

> The device-authorization flow an AI agent follows to sign itself up for Bland: it shows its owner a link and a code, the owner finishes signup in a browser, and the agent gets an API key, plus a phone number if the owner starts the Agent Phone Plan.

## Overview

This page is written for the agent. If you are a person setting up an assistant like Grok Bot, start at [Personal AI Agents](/integrations/mcp/personal-agents) and paste the prompt there; your agent reads this page to carry out the flow.

Most Bland integrations start with a human signing up in the dashboard, minting an API key, and pasting it into whatever they're building. That doesn't work well for an autonomous bot: there's no dashboard session to sign into, and often no browser or localhost to redirect back to.

Agent onboarding solves this with a device-authorization flow, the same shape used by tools like `gh auth login` or a smart TV signing into a streaming app: the bot starts the flow over a plain API call, shows its owner a short code and a link, and polls in the background while the owner finishes signup in their own browser. No callback URL, no localhost, no client secret. It works the same way whether the bot is running on your laptop or on a server with no browser at all.

At the end of the flow, the bot receives a dedicated API key. If the owner starts the [Agent Phone Plan](/platform/agent-phone-plan), the bot also receives the phone number provisioned for it. If the owner continues with free credits instead, `phone_number` is `null`. The owner never sees or handles the key.

<Note>
  During signup, the owner chooses between the [Agent Phone Plan](/platform/agent-phone-plan) and connecting on the org's \$2 of free credits. If the owner already has a Bland account, they sign in and choose there instead of creating a new one.
</Note>

## The flow

<Steps>
  <Step title="Start the flow">
    Call `POST /v1/agent/onboarding/start`. No API key is required, this is how a bot gets one.

    ```bash theme={null}
    curl -X POST https://api.bland.ai/v1/agent/onboarding/start \
      -H "Content-Type: application/json" \
      -d '{"client_name": "my-bot"}'
    ```

    ```json Response theme={null}
    {
      "data": {
        "device_code": "Xk9pQ2mZ...",
        "user_code": "BQPL-7VXR",
        "verification_url": "https://app.bland.ai/agent-setup",
        "verification_url_complete": "https://app.bland.ai/agent-setup?code=BQPL-7VXR",
        "expires_in": 900,
        "interval": 5
      },
      "errors": null
    }
    ```

    `client_name` is optional (up to 64 characters, letters, numbers, spaces, and `._/-`). It's shown to the owner so they know which bot is asking.

    `prefill` is also optional. If you already know your owner's name, email, or phone number, pass them so the sign-up form arrives pre-filled:

    ```bash theme={null}
    curl -X POST https://api.bland.ai/v1/agent/onboarding/start \
      -H "Content-Type: application/json" \
      -d '{"client_name": "my-bot", "prefill": {"name": "Sam Lee", "email": "sam@example.com", "phone": "+14155550123"}}'
    ```

    `prefill` takes `name`, `first_name`, `last_name`, `email`, and `phone`, each a string up to 128 characters. When `first_name` and `last_name` are absent, `name` is split on its first space. `phone` is normalized to E.164, with US as the default region. `email` must have the shape `x@y.z`. Invalid values are dropped silently; a bad pre-fill never fails the request.

    Values that survive validation are appended to `verification_url_complete` as a URL fragment, form-encoded the way `URLSearchParams` does it (`@` becomes `%40`, `+` becomes `%2B`, a space becomes `+`):

    ```text theme={null}
    https://app.bland.ai/agent-setup?code=BQPL-7VXR#first_name=Sam&last_name=Lee&email=sam%40example.com&phone=%2B14155550123
    ```

    The sign-up page reads the fragment and pre-fills the form. Pre-fill values ride only in the fragment, which stays in the owner's browser; Bland never stores or logs them. If you build the link yourself from `verification_url` and `user_code`, you can append a fragment in the same format.
  </Step>

  <Step title="Show the link and code to your owner">
    Display `verification_url_complete` (or `verification_url` plus `user_code` if you can't render a clickable link) wherever your owner will see it: a chat message, a terminal, a Slack DM.

    The owner opens the link, signs up or logs in, enters the code if it isn't already pre-filled, and then either starts the [Agent Phone Plan](/platform/agent-phone-plan) or connects you on the org's free credits. This happens once, in their browser. Your bot doesn't participate in it beyond polling for the result.
  </Step>

  <Step title="Poll until approved">
    Call `POST /v1/agent/onboarding/poll` with the `device_code`, waiting at least `interval` seconds between calls.

    ```bash theme={null}
    curl -X POST https://api.bland.ai/v1/agent/onboarding/poll \
      -H "Content-Type: application/json" \
      -d '{"device_code": "Xk9pQ2mZ..."}'
    ```

    Each response has a `status`:

    | `status` | Meaning |
    | - | - |
    | `pending` | Still waiting on the owner. Keep polling at the returned `interval`. |
    | `approved` | Onboarding is complete. The response includes `api_key`, `org_id`, and `phone_number` (`null` when the owner connected you without the plan). |
    | `expired` | The code expired, or the `device_code` was never valid. These two cases aren't distinguished, so don't use `expired` to debug a typo, start over with a fresh `/start` call instead. |

    If you poll faster than `interval`, you get a `429` with error code `SLOW_DOWN` instead of a status:

    ```json 429 SLOW_DOWN theme={null}
    {
      "data": null,
      "errors": [
        {
          "error": "SLOW_DOWN",
          "message": "Poll more slowly.",
          "interval": 10
        }
      ]
    }
    ```

    Back off to the `interval` in the error body (also sent as a `Retry-After` header) before polling again.
  </Step>

  <Step title="Use the key">
    Once `status` is `approved`, the response depends on the choice the owner made:

    ```json With the plan theme={null}
    {
      "data": {
        "status": "approved",
        "api_key": "org_...",
        "org_id": "...",
        "phone_number": "+14155550123",
        "plan": {
          "name": "agent_phone_basic",
          "display_name": "Agent Phone Plan",
          "status": "active",
          "phone_number": "+14155550123",
          "concurrency": 1,
          "max_call_duration_minutes": 60,
          "allowed_countries": ["US", "CA"],
          "current_period_end": "2026-10-07T00:00:00.000Z"
        },
        "client_name": "my-bot"
      },
      "errors": null
    }
    ```

    ```json With free credits theme={null}
    {
      "data": {
        "status": "approved",
        "api_key": "org_...",
        "org_id": "...",
        "phone_number": null,
        "plan": {
          "name": null,
          "display_name": null,
          "status": "none",
          "phone_number": null,
          "concurrency": null,
          "max_call_duration_minutes": null,
          "allowed_countries": null,
          "current_period_end": null
        },
        "client_name": "my-bot"
      },
      "errors": null
    }
    ```

    When the owner connected you without the plan, `phone_number` is `null` and `plan.status` is `"none"`. The API key works the same way; your calls go out from Bland's shared pool of numbers, and texting is not included. The owner can add the plan later from [Billing & Plans](https://app.bland.ai/dashboard/pay?tab=plan).

    This is the only time `api_key` is returned. Store it immediately, the code is single-use, so polling again after this returns `expired`.
  </Step>
</Steps>

<Note>
  **The device flow is region-bound.** Start and poll must target the same API host the owner's dashboard uses. For most accounts that is `https://api.bland.ai`, paired with `https://app.bland.ai`. Regional dashboards pair with a regional API host: `eu.app.bland.ai` with `eu.api.bland.ai`, `ca.app.bland.ai` with `ca.api.bland.ai`, and both `asia.app.bland.ai` and `au.app.bland.ai` with `asia.api.bland.ai`. If poll returns `expired` right after the owner told you they approved, check the host pairing first.
</Note>

## Try it with a client

<Tabs>
  <Tab title="cURL">
    Use the two calls above directly: `POST /v1/agent/onboarding/start`, then poll `POST /v1/agent/onboarding/poll` with the returned `device_code` until `status` is `approved`.
  </Tab>

  <Tab title="bland-cli">
    The [Bland CLI](/sdks/cli) wraps the whole flow in one command:

    ```bash theme={null}
    npm install -g bland-cli
    bland auth login --device
    ```

    `bland signup` is an alias for the same command, if you don't have an account yet.

    It prints the link and code, polls for you, and stores the resulting API key in your CLI profile once approved. See [Getting started](/sdks/cli#getting-started).

    If you pass `--base-url`, it must match the owner's dashboard region.
  </Tab>

  <Tab title="MCP server (stdio)">
    Run the Bland CLI's MCP server as a stdio server and your agent can run the whole flow itself, with no API key set up first. In Claude Code:

    ```bash theme={null}
    claude mcp add bland-agent -- npx -y bland-cli@0.10.0 mcp
    ```

    Any other MCP host that launches stdio servers takes the same command:

    ```json theme={null}
    { "mcpServers": { "bland-agent": { "command": "npx", "args": ["-y", "bland-cli@0.10.0", "mcp"] } } }
    ```

    That server exposes two tools for this flow:

    * `bland_auth_login` starts it and returns the link and code to show your owner.
    * `bland_auth_poll` checks whether the owner has approved yet. Call it once per the returned `interval`, not in a loop.

    Once `bland_auth_poll` reports `approved`, the API key is saved to the machine's CLI profile and every other `bland_` tool works. The key is never returned in a tool result, so it does not enter the conversation.

    The [Bland plugin](/integrations/mcp/norm) for Claude Code, Cursor, and Codex uses the hosted MCP server instead and needs an API key configured up front; use it once your agent has its key.
  </Tab>
</Tabs>

## Texting from the new number

Texting requires the [Agent Phone Plan](/platform/agent-phone-plan). If the owner connected you with free credits, there is no dedicated number to text from, and this section doesn't apply until they add the plan.

Outbound texts go through [`POST /v1/sms/send`](/api-v1/post/sms-send). Replies arrive at the same number and are answered automatically. When a thread has a goal, pass it as `objective`, and every reply in that conversation works toward it:

```bash theme={null}
curl -X POST https://api.bland.ai/v1/sms/send \
  -H "authorization: $API_KEY" \
  -H "content-type: application/json" \
  -d '{"user_number": "+14155550123", "agent_number": "+12765550123", "agent_message": "Hi, do you have an opening next Tuesday afternoon for a haircut?", "objective": "Book a haircut for the owner next Tuesday afternoon. Confirm the time and price, then end the conversation."}'
```

A conversation without an objective is answered from the number's texting prompt, which starts out generic. To change that default for every conversation on the number, use [Update SMS Configuration](/api-v1/post/sms-update).

Calls use the same number. A call placed without `from` leaves from the plan's number, so the person you called can call or text it back.

## Error codes

| Code | HTTP status | Where | Meaning |
| - | - | - | - |
| `SLOW_DOWN` | 429 | `/poll` | Polling faster than `interval`. Back off and retry. |
| `TOO_MANY_REQUESTS` | 429 | `/start`, `/poll`, `/approve` (browser) | Rate limited. Respect the `Retry-After` header and retry. |
| `FEATURE_DISABLED` | 503 | `/start` | Agent onboarding is temporarily disabled. Retry later. |
| `PLAN_REQUIRED` | 402 | `/approve` (browser) | The org has no active Agent Phone Plan and the request didn't opt out with `skip_plan: true`. Returned only on the plan path; connecting with free credits never hits it. |
| `PLAN_NUMBER_PENDING` | 409 | `/approve` (browser) | The plan is active but the phone number is still being provisioned. |
| `CODE_NOT_FOUND` | 404 | `/approve` (browser) | The code doesn't exist or already expired. |
| `CODE_ALREADY_USED` | 409 | `/approve` (browser) | The code was already approved. |
| `ORG_CONTEXT_REQUIRED` | 403 | `/approve` (browser) | The signed-in account isn't acting as an org, so it can't approve a device code. |
| `INVALID_ORIGIN` | 403 | `/approve` (browser) | The approve request did not come from the Bland dashboard. |

The `/approve` codes happen in the owner's browser during signup, your bot only ever sees their effect: `poll` staying `pending` for longer than expected, or eventually returning `expired`.

## Security notes

* **Codes expire in 15 minutes.** If the owner doesn't finish in time, start over with a new `/start` call.
* **Codes are single-use.** The first successful `poll` after approval returns the `api_key` and consumes the code. Any poll after that returns `expired`.
* **The API key is scoped to that org** and can be revoked at any time from **Settings > API Keys** in the dashboard, without affecting any other keys on the account.
* **Each approval mints a new key.** Running onboarding again for the same org creates another org key rather than reusing one, so the list in Settings > API Keys grows by one per connected agent. Revoke the ones you no longer use.
* Treat the returned `api_key` like a password: store it in your bot's secret storage, not in logs or chat transcripts.

<Note>
  MCP requires an API key. If your agent talks to Bland over [MCP](/integrations/mcp/overview), use the `api_key` from this flow the same way you'd use any other Bland API key.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Agent Phone Plan" icon="phone" href="/platform/agent-phone-plan">
    What's included, limits, and how to cancel.
  </Card>

  <Card title="Command Line Interface" icon="terminal" href="/sdks/cli">
    `bland auth login --device` and the rest of the CLI.
  </Card>
</CardGroup>

***

Docs for agents: [llms.txt](/llms.txt)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.