# CallingYou.AI — Agent signup runbook

Audience: an autonomous agent setting up CallingYou.AI for a business.

Status: **preview**. Shapes can change. Read `https://callingyou.ai/.well-known/agent-access.json` (schema 0.3) for the current manifest.

Base URL: `https://api.callingyou.ai`

Send `Authorization: Bearer <api_key>` on every call after verify, except the two signup calls and the public discovery URLs. Store the key in a secret store. It is shown once.

## 1. Request a code

`POST /v1/agent/signup` does not create the business. It sends a six-digit code to the owner. The code lasts 10 minutes. The owner reads it back to you.

`agent_name` is 2 to 40 characters (letters, digits, spaces, hyphens). `business_name` is 1 to 120 characters. `organization_name` is an alias, and a non-empty `business_name` wins. Send `owner_phone` (E.164) or `owner_email` or both. Optional: `country`, `locale`, `vertical_pack` (alias `sector`), `website_url`, `place_id`.

Country is the body value, or the country of the phone. Supported values are `IT`, `FR`, `DE`, `ES` and `US`. Locale defaults from the country: `it-IT`, `fr-FR`, `de-DE`, `es-ES`, and `en` for the US. An unknown or empty pack is stored as `general`. It is never rewritten to `hvac`.

```sh
curl -sS -X POST https://api.callingyou.ai/v1/agent/signup \
  -H 'Content-Type: application/json' \
  -d '{
    "agent_name": "North Desk",
    "business_name": "Example Heating",
    "owner_phone": "+15555550123",
    "owner_email": "owner@example.com",
    "country": "US",
    "locale": "en",
    "vertical_pack": "hvac"
  }'
```

`202` body:

```json
{
  "signup_id": "…",
  "channel": "sms",
  "sent_to": "+•••••••0123",
  "expires_at": "…",
  "vertical_pack": "hvac"
}
```

`channel` is `whatsapp`, `sms` or `email`. A phone uses WhatsApp when a platform template is configured, otherwise SMS. Email alone uses email. `sent_to` is masked: a phone is `+`, seven bullets, and the last four digits. An email keeps the first character and the domain. `vertical_pack` is null when you named no pack.

The SMS sentence (and the email body) is: "{{agent}} is setting up CallingYou.AI for you. Your code is {{code}}." The email subject is "{{agent}} sent you a CallingYou.AI code."

If that place, phone or email already belongs to an account, nothing is created. The response is still `202`, and the code goes to the account's own contact. Every verify attempt then counts as a wrong code until `410 signup_closed`.

Validation is `422`, in this order:

| Code | When |
|---|---|
| `agent_name_invalid` | Missing, or outside the 2–40 character set |
| `business_name_required` | Empty, or longer than 120 characters |
| `contact_required` | No phone and no email |
| `phone_invalid` | Present, and not E.164 |
| `email_invalid` | Present, and not an email |
| `country_required` | No country and no phone to infer one |
| `country_not_supported` | Outside IT, FR, DE, ES, US |

Limits, all `429 rate_limited`: 30 signups a minute globally, 10 an hour per IP, then 3 codes an hour and 10 a day for that phone or email.

## 2. Verify the code

```sh
curl -sS -X POST https://api.callingyou.ai/v1/agent/signup/<signup_id>/verify \
  -H 'Content-Type: application/json' \
  -d '{"code":"482913"}'
```

| Status | Code | When |
|---|---|---|
| `404` | `signup_not_found` | Unknown id, or the session did not come from this signup |
| `409` | `already_verified` | This signup was already proved |
| `410` | `signup_expired` | Older than 10 minutes |
| `410` | `signup_closed` | Five wrong codes, including the fifth on this call |
| `422` | `invalid_code` | Wrong code. Body includes `attempts_left` |
| `201` | | Right code |

`201` body: `organization_id`, `api_key` (`cy_live_…`, shown once), `plan` (`trial`), `email_verified`, `phone_verified`, `vertical_pack`, `persona_build_id` (null until a build is attached) and `next_steps`. For locale `en` the steps are:

1. If you proved an email only, prove the owner's phone before a trial number can be assigned.
2. Connect a calendar so the assistant can book.
3. Assign a number for your business.
4. Test call forwarding to that number.
5. Choose a plan when the trial should become a subscription.

The key's label is `Created by <agent_name>`, cut at 60 characters.

Humans can open `https://callingyou.ai/sign-up` and use Google, Apple, Microsoft, Facebook or LinkedIn. Those tokens are identity only. They do not grant calendar scopes.

## 3. Prove the owner's phone

Do this when verify reports `phone_verified: false`. A trial number cannot be assigned until an Owner has a verified phone.

`POST /v1/me/phone` is for a person signed in as the owner. An API key on that route, and on its verify route, receives `403 agent_forbidden`. Use these two instead:

```sh
curl -sS -X POST https://api.callingyou.ai/v1/tenants/me/owner-phone \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"+15555550123"}'
```

`202` body: `{"sent": true}`. A number that is not E.164 is `400 phone_invalid` (signup uses `422` for the same code). The same destination limits apply: 3 an hour and 10 a day, then `429 rate_limited`. No owner on the business is `404 owner_not_found`. A session whose role is not Owner receives `403 forbidden_role`.

```sh
curl -sS -X POST https://api.callingyou.ai/v1/tenants/me/owner-phone/verify \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"code":"482913"}'
```

`200` body: `phone_e164` and `phone_verified_at`. `400 code_invalid` includes `attempts_left`. `410 code_expired` means the code aged out. `409 phone_in_use` is returned after the right code when that number already belongs to another account.

The same two calls exist as MCP tools `add_owner_phone` and `verify_owner_phone` (`setup.write`).

## 4. What the owner does in the dashboard

An API key receives `403 agent_forbidden` ("Only an owner, signed in to the dashboard, can do this. Agents and API keys cannot.") on:

- `DELETE /v1/phone-numbers/{id}`
- `DELETE /v1/team/members/{user_id}`
- `DELETE /v1/team/invites/{id}`
- `DELETE /v1/sip-connections/{id}`
- `DELETE /v1/calendar/appointments/{id}`
- `DELETE /v1/contacts/{id}`
- `DELETE /v1/calendar/connectors/{provider}`
- `DELETE /v1/assistants/{id}`
- `POST /v1/billing/portal`
- `PUT /v1/me/voice-pin` and `DELETE /v1/me/voice-pin`
- `GET` and `POST /v1/me/trusted-phones`, `POST /v1/me/trusted-phones/verify`, `DELETE /v1/me/trusted-phones/{id}`
- `POST /v1/me/phone` and `POST /v1/me/phone/verify`
- `PUT /v1/me/email`
- `GET /v1/connected-agents`, `POST /v1/connected-agents/keys`, `DELETE /v1/connected-agents/keys/{id}`, `DELETE /v1/connected-agents/grants/{id}`
- `GET /v1/oauth/requests/{id}` and `POST /v1/oauth/requests/{id}/decision`

An OAuth access token (`cy_at_`) is refused on the REST API with `401`. It works at `/mcp` only. A CalDAV or CardDAV `DELETE` signed in with the API key is refused too.

`GET /v1/me` with the key omits `phone_e164` and `voice_pin_set`. On a trial it includes `trial.stage` (`not_started`, `active`, `grace` or `rest`), `started_at`, `ends_at`, `grace_ends_at`, `minutes_used`, `minutes_included` (30) and `minutes_hard` (35). The stage stays `not_started` until the first number is assigned. On a paid plan `trial` is null.

## 5. Build the profile

Signup already applied `vertical_pack` when you sent one. You can still search, build and accept.

### Search

`input` is a business name, a phone number, a Google Maps link, or a website, Instagram or Facebook link. `region` is an optional two-letter country code. It defaults from the business locale (`it-IT` to `IT`, `fr-FR` to `FR`, `de-DE` to `DE`, `es-ES` to `ES`, `en` to `US`).

```sh
curl -sS -X POST https://api.callingyou.ai/v1/persona/search \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"input":"Example Heating","region":"US"}'
```

The response has `kind` (`text`, `phone`, `maps`, `website`, `instagram` or `facebook`), `candidates`, `link` and `hint`. A name, phone number or Maps link returns up to three Google Maps `candidates`, each with `place_id`, `name`, `address`, `city`, `phone`, `website`, `primary_type`, `types`, `google_maps_uri`, `business_status` and `hours`. A website or social link returns the normalised page in `link` and no candidates. When nothing matches, `hint` is `add_town_or_website`. Live Google Maps searches are limited to 10 per business per UTC day. After that the call returns `429` with `persona_cap_reached`. A build can still start from a website, Instagram or Facebook link.

### Start a build

Send at least one of `place_id`, `website`, `instagram` or `facebook`.

```sh
curl -sS -X POST https://api.callingyou.ai/v1/persona/builds \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"place_id":"ChIJ...","website":"https://example.com"}'
```

The response is `202` with `id`, `status: "queued"` and `sources`. No source, an unknown field or a malformed value returns `400` with `invalid_argument`.

### Poll

```sh
curl -sS https://api.callingyou.ai/v1/persona/builds/<id> \
  -H 'Authorization: Bearer <api_key>'
```

`status` moves from `queued` and `running` to `done`, `partial` or `failed`. `done` means every source the build tried answered. `partial` means at least one answered and at least one failed. `failed` means none answered.

Each entry in `sources` (`google`, `website`, `instagram`, `facebook`) has a `state` (`skipped`, `pending`, `running`, `done` or `failed`), a `via` and, on failure, an `error` such as `unreachable`, `timeout`, `rate_limited`, `not_found` or `daily_cap_reached`. The website source falls back to the page title, description and `tel:` links when the page cannot be read. `via` then reads `fallback`. A source with `via: "fixture"` returned bundled sample data.

After `done` or `partial`, `result` holds the merged draft: `trade_name`, `phone`, `address`, `city`, `hours`, `description`, `services`, `booking_url`, `socials` and `pack`. Each value carries its `source`. When the sources disagree on `trade_name`, `phone`, `address` or `hours`, `conflicts` lists every value and a `chosen` default. `result.pack.pack` suggests a pack from the Google Maps category. `mapped: false` means no category matched and the suggestion is `general`.

### Accept

`choices` picks the source to keep for each conflict (`owner`, `google`, `website`, `facebook` or `instagram`). The source must be one of that conflict's values. `edits` overrides `trade_name` (1 to 120 characters), `phone` (up to 32 characters of digits, `+`, spaces, brackets, `-`, `.`), `about` (up to 600 characters) or `catalog` (up to 12 items with `id`, `label` and `offered`). Both are optional. Sending either without `build_id` returns `400`.

```sh
curl -sS -X POST https://api.callingyou.ai/v1/tenants/me/profile/accept \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"build_id":"<id>","choices":{"phone":"google"},"edits":{"about":"Heating repair."}}'
```

The response is `{"profile", "accepted": true, "instructions_chars", "tools"}`. Accept writes the name, phone, address, city, about text, services, social links, booking link and hours. When the draft has hours, it also replaces the weekly hours in `/v1/concierge/settings`. It does not change the pack you already set. A body with no `build_id` accepts the profile as it stands.

| Code | Status | Meaning |
|---|---|---|
| `not_found` | `404` | No profile, or the build belongs to another business |
| `build_not_ready` | `409` | Still `queued` or `running` |
| `build_failed` | `409` | No source answered |
| `already_accepted` | `409` | This build was accepted before |

To change the pack later, `GET /v1/tenants/me/profile`, keep the returned fields, and `PUT /v1/tenants/me/profile` with `sector` and `vertical_pack`. Then accept again. Profile edits clear acceptance. Packs include `hotel`, `bed_breakfast`, `spa`, `wellness`, `salon`, `beauty`, `retail`, `boutique`, `hvac`, `appliance`, `auto`, `clinic`, `gym`, `legal`, `software`, `real_estate` and `general`. An unknown id becomes `general`.

The assistant collects facts and leaves quotes, prices, stock and job estimates to the team. Hotel and B&B assistants capture inquiries. They do not hold room inventory. Retail calendar actions organise visits. Treatment resources need the team's confirmation when the connected system does not have them.

## 6. Read the account, pricing and checkout

```sh
curl -sS https://api.callingyou.ai/v1/me \
  -H 'Authorization: Bearer <api_key>'

curl -sS https://api.callingyou.ai/v1/pricing
```

During preview, pricing describes tiers and does not publish amounts.

```sh
curl -sS -X POST https://api.callingyou.ai/v1/billing/checkout \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"plan":"starter"}'
```

`plan` is `starter`, `growth` or `scale`. Anything else is `400 invalid_argument`. The response carries a `url` to open, or to hand to a person. During preview this can return `503` with `billing_unavailable` when the Stripe secret or that plan's price id is unset.

`POST /v1/billing/subscribe` takes `{"plan","payment":{"type":"shared_payment_token","token":"spt_…"}}`. While `STRIPE_PROFILE_ID` is unset or blank, the MCP tool `subscribe_with_payment_token` is omitted from `tools/list`, and this route returns `503 billing_unavailable` before the Stripe secret is checked. The billing portal is not available to an API key.

## 7. Number, forwarding, settings

`POST /v1/phone-numbers/assign` with `{"country":"US"}` takes one number of that country from the pool. The first number on a trial starts the clock: 14 days of use, grace through day 21.

| Code | Status | When |
|---|---|---|
| `owner_phone_required` | `403` | No Owner has a verified phone |
| `trial_already_used` | `409` | That phone or that Google `place_id` already has a grant |
| `trial_daily_cap` | `429` | New grants in the last 24 hours reached `CALLINGYOU_TRIAL_DAILY_CAP` (code default 20) |
| `number_limit_reached` | `409` | The plan already holds its number. A trial holds one |
| `documents_required` | `409` | Empty pool in a country that needs documents |
| `country_not_enabled` | `403` | Outside `CALLINGYOU_NUMBER_COUNTRIES` |
| `pool_empty` | `503` | The order that should have filled the pool failed |

`POST /v1/phone-numbers/orders` buys a specific number. Italy answers `409 country_kyc_pending` while `CALLINGYOU_IT_KYC_READY` is off. A paid plan skips the trial rules.

`PUT /v1/concierge/settings` takes `week`, `tz`, `escalation_phone` and `confirmations` (`policy`, `lead_hours`, `sms_wait_hours`, `quiet_from`, `quiet_to`). `POST /v1/concierge/sinks/test` exercises the escalation sink. The always-on line stays refused until a sink passes.

`GET` and `POST /v1/calendar/connectors/*` connect a calendar. The native calendar is always available. Humans finish registration, when they still need to, at `POST /v1/me/complete-registration`.

Forwarding:

```sh
curl -sS 'https://api.callingyou.ai/v1/forwarding/codes?business_e164=%2B15555550100&mode=unanswered' \
  -H 'Authorization: Bearer <api_key>'
```

`409 forwarding_not_configured` means the business has no CallingYou.AI number.

```sh
curl -sS -X POST https://api.callingyou.ai/v1/phone-numbers/<number_id>/forwarding-test \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"business_e164":"+15555550100","mode":"unanswered"}'
```

`202` body: `test_id` and `status`. `business_e164` and `mode` are optional and otherwise come from the stored forwarding record. `409 forwarding_test_running` means a test is already dialing. `503 setup_number_missing` means that country has no setup number. An API key or an OAuth principal may start 5 tests per business per hour, then `429 rate_limited`. A person session has no such hourly cap.

```sh
curl -sS https://api.callingyou.ai/v1/forwarding-tests/<test_id> \
  -H 'Authorization: Bearer <api_key>'
```

`200` body: `id`, `status`, `failure`, `created_at`, `finished_at`. Another business's test is `404`.

To dial one customer now: `POST /v1/calendar/appointments/<id>/confirm-call`. A customer who replies STOP is marked `do_not_call`. Set it with `PATCH /v1/contacts/<id>` and `{"do_not_call": true}`.

## 8. MCP

`POST https://api.callingyou.ai/mcp` is stateless Streamable HTTP. `GET` and `DELETE` answer `405`.

Connect with an OAuth client or with the API key as `Authorization: Bearer cy_live_…`. ChatGPT registers from `https://chatgpt.com/oauth/client.json` (`private_key_jwt`, redirect `https://chatgpt.com/connector_platform_oauth_redirect`). Claude Code registers from `https://claude.ai/oauth/claude-code-client-metadata` (loopback redirects, no client secret). Claude on claude.ai and the desktop app uses a client metadata document when the server advertises it, otherwise `POST /oauth/register`, method `none`, redirect `https://claude.ai/api/mcp/auth_callback`. Any other client uses dynamic registration. The server hard-codes none of these clients.

Authorisation-server metadata: `GET https://api.callingyou.ai/.well-known/oauth-authorization-server`. Protected resource: `GET https://api.callingyou.ai/.well-known/oauth-protected-resource/mcp`. Server card: `GET https://api.callingyou.ai/mcp/server-card` and `GET https://api.callingyou.ai/.well-known/mcp/server-card.json`.

Scopes: `account.read`, `setup.write`, `numbers.write`, `calls.read`, `billing.write`. The API key holds all five on `/mcp`. If a scope is missing and the client host is `chatgpt.com`, the tool result is `isError: true` and `_meta["mcp/www_authenticate"]` carries the challenge. Other clients receive HTTP `403` with `{"error":"insufficient_scope","message":"Missing scope {needed}."}`.

`/mcp` allows 120 calls a minute per token. Over that: `429 {"error":"agent_rate_limited"}` and `Retry-After: 60`. A body over 256 KiB is `413 payload_too_large`. An `Origin` header must be `https://claude.ai`, `https://chatgpt.com`, or the admin origin. A missing `Origin` is allowed. Any other origin is `403 origin_forbidden`.

`get_profile` needs no scope. It returns `name`, `nickname`, and `id` and `email` when it has them.

The owner sees keys and grants at `GET /v1/connected-agents` from a dashboard session. Your key receives `403 agent_forbidden` there. A revocation updates the API instance that handled it and the database. Another instance keeps the previous grant until it restarts or reloads.

## 9. Discovery

| Document | URL |
|---|---|
| This guide | `https://callingyou.ai/agent-signup.md` |
| Agent manifest | `https://callingyou.ai/.well-known/agent-access.json` |
| API catalog | `https://api.callingyou.ai/.well-known/api-catalog` |
| API catalog (website) | `https://callingyou.ai/.well-known/api-catalog` |
| AI catalog | `https://callingyou.ai/.well-known/ai-catalog.json` |
| OpenAPI | `https://api.callingyou.ai/openapi.json` |
| Health | `https://api.callingyou.ai/health` |

The catalog is a linkset. The anchors are `{api}/v1` and `{api}/mcp`. The v1 anchor points at the OpenAPI document, this guide, and `/health`. The mcp anchor points at the server card and this guide.

AI disclosure is on for every call the platform makes or answers. It cannot be turned off. Questions from a person: hello@callingyou.ai.
