# Part 2A — Setup: basics, accounts, numbers

[← Contents](README.md) · Previous: [Part 1](1-how-it-works.md) · Next: [Part 2B — Call flows](2b-flows.md)

Every endpoint here needs `Authorization: Bearer <api key>` (🔒). The envelope and error codes are on the
[contents page](README.md#standard-response-format).

---

# A. Basics

## A1. Ping
**Purpose**: Check the key works and see which features the client app has.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/ping`

**Access Level**: 🔒 Protected · Bearer API key

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "200 OK",
  "message": "pong",
  "action": null,
  "action_time": "2026-11-13T19:57:58.900619432",
  "data": {
    "clientAppId": "bd74035b-bdb6-450d-885b-222e83719906",
    "name": "softphone-test",
    "features": ["INBOUND", "CALLOUTS", "OTP", "AI"]
  }
}
```

| Field | Description |
|-------|-------------|
| `clientAppId` | The client app this key belongs to |
| `name` | Its name at the Ringmaster owner |
| `features` | What it may do: `INBOUND`, `CALLOUTS`, `OTP`, `AI`, `NUMBERS` ([Part 1 §1.2](1-how-it-works.md#12-getting-access)) |

**Error Response JSON Sample** (no or wrong key):
```json
{
  "success": false,
  "httpStatus": "401 UNAUTHORIZED",
  "message": "Missing or invalid API key",
  "code": "UNAUTHORIZED",
  "action_time": "2026-11-13T19:57:59.067583389",
  "data": null
}
```
Also: `403 CLIENT_SUSPENDED` (the owner suspended the app), `429 RATE_LIMITED`.

---

## A2. Script schema
**Purpose**: The block catalogue a flow editor needs: every block type, its group, what it does, its exits, whether
it needs approval; the rules; the message shape; the variables a flow can use; an example flow.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/scripts/schema`

**Access Level**: 🔒 Protected · Bearer API key

**Success Response JSON Sample** (shortened: 18 blocks in `blocks`):
```json
{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Script schema",
  "action": null,
  "action_time": "2026-11-13T19:58:20.112842101",
  "data": {
    "schemaVersion": "1",
    "blocks": [
      { "type": "say", "group": "Speak", "description": "Say a text or play an audio message, then continue.",
        "exits": ["next"], "needsApproval": false },
      { "type": "menu", "group": "Ask",
        "description": "Ask the caller to pick an option by key or, when speech is on, by voice …",
        "exits": ["<option>", "noInput", "notUnderstood", "later"], "needsApproval": false },
      { "type": "collect", "group": "Ask", "description": "Collect a number (account, PIN, amount) into {input.<name>}.",
        "exits": ["ok", "failed", "later"], "needsApproval": true }
    ],
    "rules": [
      "direction is INCOMING (start exit 'call') or OUTGOING (start exit 'out'); never both",
      "every block: {type, …settings, before?: message, wr?: waiting room, x: {exit: blockId}}; a missing exit ends the call",
      "at most 60 blocks, 10 menu keys, 3 retries; text at most 500 characters"
    ],
    "messageShape": {
      "text": { "src": "text", "text": "Habari {name}", "lang": "sw",
                "voice": "marin|coral|shimmer|sage|cedar|ash|echo|ballad|verse", "speed": "0.7-1.5 (1.0 normal)…" },
      "audio": { "src": "record|upload|library", "media": "<mediaId>" }
    },
    "systemVariables": ["date", "time", "lang", "answeredBy", "key", "heard", "number", "caller", "callId"],
    "example": {
      "direction": "INCOMING",
      "start": { "call": "greet" },
      "blocks": {
        "greet": { "type": "say", "msg": { "src": "text", "text": "Karibu!" }, "x": { "next": "menu" } },
        "menu": { "type": "menu", "msg": { "src": "text", "text": "Bonyeza moja kwa mauzo, mbili kwa msaada." },
                  "options": [ { "k": "1", "id": "sales", "label": "Mauzo", "words": ["mauzo", "kununua"] },
                               { "k": "2", "id": "help", "label": "Msaada", "words": ["msaada", "tatizo"] } ],
                  "x": { "sales": "sales", "help": "help", "noInput": "bye", "notUnderstood": "bye" } },
        "sales": { "type": "say", "msg": { "src": "text", "text": "Bei zetu ziko kwenye tovuti yetu." }, "x": { "next": "bye" } },
        "help": { "type": "say", "msg": { "src": "text", "text": "Mhudumu atakupigia leo." }, "x": { "next": "bye" } },
        "bye": { "type": "goodbye", "msg": { "src": "text", "text": "Asante, kwaheri." } }
      }
    }
  }
}
```

| Field | Description |
|-------|-------------|
| `blocks[].exits` | Exit names. `<option>` = one exit per menu/connect option (its `id`); `<outcome>` = one per App action outcome; `<return>` = the names a transferred flow returns with |
| `blocks[].needsApproval` | A risky block: a version using it should be approved before it runs. The engine reports these in `riskyBlocks`; the client app pushes such versions with `runnable: false` and approves them later ([Part 2B, D7](2b-flows.md#d7-approve-a-version-make-it-runnable)) |
| `systemVariables` | Always available in texts as `{name}`: `{caller}`, `{date}`, `{time}`, `{lang}`, `{key}` (last key), `{heard}` (last speech), `{number}` (number called), `{callId}`, `{answeredBy}` (outgoing, with answering-machine detection: `person` / `machine`) |

The full meaning of every block setting is in [Part 5](5-flow-reference.md). The schema is generated from the engine's
code, so it always matches what the engine accepts; cache it per `schemaVersion`.

---

## A3. Check a script (dry run)
**Purpose**: Validate a flow document without storing it: the same checks as pushing a version. Use it in the editor
on every save.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/scripts/validate`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{
  "accountId": "adb72fac-66a2-471b-a437-035fe008197d",
  "definition": {
    "direction": "INCOMING",
    "start": { "call": "a" },
    "blocks": {
      "a": { "type": "say", "msg": { "src": "text", "text": "Hi" }, "x": { "next": "zz" } }
    }
  }
}
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `definition` | object | Yes | The flow document ([Part 5](5-flow-reference.md)) |
| `accountId` | uuid | No | With it, references are checked too: departments, media, the flows a Transfer or Schedule block names |

**Success Response JSON Sample** (the check ran; this flow has an error):
```json
{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Validation result",
  "action": null,
  "action_time": "2026-11-13T18:06:38.004985164",
  "data": {
    "valid": false,
    "errors": [
      { "level": "ERROR", "block": "a", "code": "EXIT_TARGET_MISSING", "message": "Exit 'next' points to a missing block: zz" }
    ],
    "warnings": [],
    "vars": [],
    "texts": ["Hi"],
    "riskyBlocks": []
  }
}
```

| Field | Description |
|-------|-------------|
| `valid` | `true` when there are no errors (warnings are allowed) |
| `errors[]` / `warnings[]` | `{level, block, code, message}`; codes in [Part 4](4-reference.md#script-check-codes). Typical warning: `EXIT_UNCONNECTED` (the call ends there) |
| `vars` | Variables the flow uses that a call-out must provide (`{name}`, `{amount}`) |
| `texts` | Every fixed sentence; the engine turns these into audio when a version is stored |
| `riskyBlocks` | Blocks that make this version need approval (risky block types, and App actions registered as risky) |

The answer is always `200` when the document could be read; `valid` says whether it would be stored. When the client
app has no `AI` feature, each block that uses AI (AI Agent, `speech.match: "smart"`) is an error with code `FEATURE_OFF`.

---

# B. Accounts

An **account** is one business. Its id is used in most paths. `externalRef` is the client app's own id for it.

## B1. Create an account
**Purpose**: Add a business. It starts with sensible defaults (hours Mon–Fri 08–17, Sat 09–13; quiet hours 21:00–07:00;
recording of live parts with a Kiswahili notice; limits; 90 days retention).

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/accounts`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{
  "externalRef": "pmk-chemicals",
  "defaultLanguage": "sw",
  "timezone": "Africa/Dar_es_Salaam"
}
```

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `externalRef` | string | Yes | The client app's id for the business | Not blank, max 120; unique within the client app |
| `defaultLanguage` | string | No | Language callers hear first | `sw` (default), `en` |
| `timezone` | string | No | For hours, quiet hours, "call me at 9" | IANA zone; default `Africa/Dar_es_Salaam` |

**Success Response**: `201 Created`, `data` = the account, same shape as [B3](#b3-get-an-account).

**Errors**:
```json
{ "success": false, "httpStatus": "409 CONFLICT", "message": "An account with this externalRef already exists",
  "code": "ACCOUNT_EXISTS", "action_time": "2026-11-13T19:57:58.985027739", "data": null }
```
```json
{ "success": false, "httpStatus": "422 UNPROCESSABLE_CONTENT", "message": "Validation failed",
  "code": "VALIDATION_FAILED", "action_time": "2026-11-13T19:57:59.051669172", "data": { "externalRef": "must not be blank" } }
```
```json
{ "success": false, "httpStatus": "400 BAD_REQUEST", "message": "Unknown timezone: Mars/Base",
  "code": "BAD_REQUEST", "action_time": "2026-11-13T19:57:59.009578709", "data": null }
```

---

## B2. List accounts
**Purpose**: All accounts of the client app, or the one with a given `externalRef`.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/accounts`

**Access Level**: 🔒 Protected · Bearer API key

| Query | Type | Required | Description |
|-----------|------|----------|-------------|
| `externalRef` | string | No | Return only this account (a list of 0 or 1) |

**Success Response**: `data` = array of accounts ([B3](#b3-get-an-account) shape).

---

## B3. Get an account
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/accounts/{accountId}`

**Access Level**: 🔒 Protected · Bearer API key

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Account",
  "action": null,
  "action_time": "2026-11-13T19:57:58.939219236",
  "data": {
    "id": "adb72fac-66a2-471b-a437-035fe008197d",
    "externalRef": "softphone",
    "status": "ACTIVE",
    "defaultLanguage": "sw",
    "timezone": "Africa/Dar_es_Salaam",
    "hours": {
      "days": {
        "MONDAY": [ { "from": "08:00", "to": "17:00" } ],
        "TUESDAY": [ { "from": "08:00", "to": "17:00" } ],
        "WEDNESDAY": [ { "from": "08:00", "to": "17:00" } ],
        "THURSDAY": [ { "from": "08:00", "to": "17:00" } ],
        "FRIDAY": [ { "from": "08:00", "to": "17:00" } ],
        "SATURDAY": [ { "from": "09:00", "to": "13:00" } ]
      }
    },
    "quietHours": { "from": "21:00", "to": "07:00" },
    "recording": {
      "mode": "LIVE_PARTS",
      "noticeText": "Mazungumzo haya yanarekodiwa kwa ajili ya ubora wa huduma.",
      "noticeMediaId": null,
      "transcribe": true
    },
    "limits": {
      "maxCallMinutes": 15,
      "maxWaitSeconds": 300,
      "callerCallsPer10Min": 100,
      "otpPerDestinationPer10Min": 3,
      "voicemailMaxSeconds": 120,
      "maxBlockVisits": 25
    },
    "hasRequestSigningSecret": false,
    "retentionDays": 90,
    "businessName": "Ringmaster Lab",
    "aiDisclosure": null,
    "createdAt": "2026-11-12T05:20:55.204082Z"
  }
}
```

| Field | Description |
|-------|-------------|
| `status` | `ACTIVE` · `SUSPENDED` (every call to its numbers is refused) |
| `hours.days` | Opening hours per day (`MONDAY`…`SUNDAY`), several ranges allowed; a missing day = closed. Used by the Condition block (`hours` test) and Connect |
| `quietHours` | No call-outs between `from` and `to` (may cross midnight) |
| `recording.mode` | `LIVE_PARTS` (record talk with agents) · `OFF` (only voicemails) |
| `recording.noticeText` / `noticeMediaId` | What callers hear before a recorded part (text, or an uploaded audio file) |
| `recording.transcribe` | Make a transcript and summary of each recording |
| `limits.*` | Guards, see [Part 1 §1.4](1-how-it-works.md#14-an-incoming-call-step-by-step) |
| `hasRequestSigningSecret` | A secret is set for signing Send request blocks' calls to the business's own systems (the secret itself is never returned) |
| `retentionDays` | Recordings and transcripts are deleted after this many days |
| `businessName` | Said in the AI notice ("…huduma ya kidijitali ya *Ringmaster Lab*") |
| `aiDisclosure` | Own AI notice per language `{ "sw": "…", "en": "…" }`; `null` = the engine's default sentence |
| `recording.agentCallNotice` | Also say the notice on calls agents make (default `false`) |
| `agentPhone.showTeam` | Agents see who else in their departments is available, on a call or away (default `true`) |
| `agentPhone.showCallerNumbers` | `false` masks numbers for agents (`+2557•••••250`) in rings, queues, recent calls and search |
| `agentPhone.allowDecline` | Agents may decline rings (`false` = calls are assigned; decline answers `409 DECLINE_NOT_ALLOWED`) |
| `agentPhone.allowCallBack` | Agents may call earlier callers back from their app ([J8](2d-agents.md#j8-call-someone-back)) |
| `agentPhone.allowDialPad` | Agents may dial any number from Agent Phone's dial pad |

**Errors**: `404 NOT_FOUND` "Account not found" (unknown, or another client app's).

---

## B4. Update an account
**Purpose**: Change any setting. Send only what changes; missing fields keep their value.

**Endpoint**: <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/accounts/{accountId}`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{
  "businessName": "PMK Chemicals",
  "hours": { "days": { "MONDAY": [ { "from": "08:00", "to": "12:30" }, { "from": "14:00", "to": "17:00" } ] } },
  "quietHours": { "from": "20:00", "to": "08:00" },
  "recording": { "mode": "LIVE_PARTS", "noticeText": "Simu hii inarekodiwa.", "transcribe": true },
  "limits": { "maxCallMinutes": 20, "maxWaitSeconds": 300, "callerCallsPer10Min": 5,
              "otpPerDestinationPer10Min": 3, "voicemailMaxSeconds": 120, "maxBlockVisits": 25 },
  "retentionDays": 180,
  "aiDisclosure": { "sw": "Unazungumza na msaidizi wa kidijitali wa PMK.", "en": "You are talking to PMK's digital assistant." }
}
```

| Parameter | Type | Description | Validation |
|-----------|------|-------------|------------|
| `status` | string | `ACTIVE` · `SUSPENDED` | Suspending refuses new calls to its numbers and **pauses** its running call-outs; going back to `ACTIVE` does not restart them (the messages may be stale): resume or stop each one |
| `defaultLanguage` | string | `sw` · `en` | |
| `timezone` | string | IANA zone | `400 Unknown timezone` |
| `hours` | object | **Replaces** all opening hours | `HH:mm` |
| `quietHours` | object | `{from, to}` | `HH:mm` |
| `recording` | object | **Replaces** the recording policy | `mode` = `OFF` / `LIVE_PARTS` |
| `limits` | object | **Replaces** all six limits (send all six) | whole numbers |
| `requestSigningSecret` | string | Secret for Send request signatures; `""` removes it | stored encrypted |
| `retentionDays` | integer | Days to keep recordings | 1–3650 |
| `businessName` | string | `""` removes it | |
| `aiDisclosure` | object | `{}` goes back to the default sentence | |
| `agentPhone` | object | **Replaces** the five switches (send all five); missing ones count as `true`. Agents' devices are told at once (`config` signal) | booleans |

**Success Response**: the updated account ([B3](#b3-get-an-account) shape).

---

# C. Numbers

A number moves through three states:

```
POOL ──(owner gives it to the client app)──► ALLOCATED ──assign──► ASSIGNED (to one account) ──release──► ALLOCATED
                                                  ▲  remove (NUMBERS feature) ◄── POOL      transfer: account → account
```
Only an **assigned** number with a pinned, runnable **Incoming** flow answers calls.

## C1. List numbers
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/numbers`

**Access Level**: 🔒 Protected · Bearer API key

| Query | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | `ALLOCATED` (free, not on an account) · `ASSIGNED`. Anything else → `400 Invalid value for parameter: status` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Numbers",
  "action": null,
  "action_time": "2026-11-13T19:58:07.321779977",
  "data": [
    {
      "id": "8a8d9c13-bda2-4a18-8410-c5ec49ed2c66",
      "e164": "+255677000001",
      "status": "ASSIGNED",
      "accountId": "adb72fac-66a2-471b-a437-035fe008197d",
      "label": null,
      "incomingScriptVersionId": "e183c7f1-3137-4848-95c1-11a7e8bef17a",
      "verified": true,
      "verifiedAt": "2026-11-12T05:33:27.481181Z",
      "ringMode": "BROWSER"
    }
  ]
}
```

| Field | Description |
|-------|-------------|
| `status` | `ALLOCATED` · `ASSIGNED` |
| `accountId` | The account it is on (`null` when free) |
| `label` | The client app's name for it ("Mauzo", "Line 7") |
| `incomingScriptVersionId` | The flow version callers hear; `null` = calls are refused |
| `verified` / `verifiedAt` | A real call has reached the engine through the trunk on this number |
| `ringMode` | Where agents are rung: `BROWSER` · `MOBILE` · `BROWSER_AND_MOBILE` |

---

## C2. Add a number
**Purpose**: Bring a Tanzanian number the client app owns at the carrier into Ringmaster (it must be routed to the
engine's trunk). Needs the `NUMBERS` feature.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/numbers`

**Access Level**: 🔒 Protected · Bearer API key · feature `NUMBERS`

**Request JSON Sample**:
```json
{ "e164": "+255677000099", "type": "REGULAR", "label": "Mauzo", "by": "admin@business.co.tz" }
```

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `e164` | string | Yes | The number | `+255` and 9 digits |
| `type` | string | No | `REGULAR` (default) · `TOLL_FREE` | |
| `label` | string | No | Name for it | max 80 |
| `by` | string | No | Who did it in the client app (kept in the audit log) | max 120 |

**Success Response**: `201 Created`, `data` = the number (status `ALLOCATED`).

**Errors**:
```json
{ "success": false, "httpStatus": "403 FORBIDDEN", "message": "Adding and removing numbers is not enabled for this client app",
  "code": "FEATURE_OFF", "action_time": "2026-11-13T19:58:07.349143236", "data": null }
```
`400` "Only Tanzanian numbers (+255 and 9 digits) can be added" · `409 NUMBER_ALREADY_YOURS` · `409 NUMBER_TAKEN` (another client app has it).

---

## C3. Remove a number
**Purpose**: Give a free number back to the pool. Needs the `NUMBERS` feature.

**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/numbers/{numberId}`

**Access Level**: 🔒 Protected · Bearer API key · feature `NUMBERS`

| Query | Type | Required | Description |
|-----------|------|----------|-------------|
| `by` | string | No | Who did it (audit log) |

**Success Response**: `200`, `message` "Number removed", `data` null.

**Errors**: `409 NUMBER_ASSIGNED` (release it first) · `409 NUMBER_BUSY` (a call is in progress) · `409 NUMBER_IN_CALLOUT` (a scheduled, running or paused call-out calls from it).

---

## C4. Assign a number to an account
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/numbers/{numberId}/assign`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{ "accountId": "adb72fac-66a2-471b-a437-035fe008197d", "label": "Mapokezi" }
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `accountId` | uuid | Yes | The account |
| `label` | string | No | Name for the number |

**Success Response**: the number with `status: "ASSIGNED"`. Assigning again to the same account only changes the label.

**Errors**: `409 NUMBER_ASSIGNED` "Number is assigned to another account; release it first" (use [C6](#c6-transfer-a-number-to-another-account) instead).

---

## C5. Release a number from its account
**Purpose**: Take the number off the account; it becomes free (`ALLOCATED`) and its flow is cleared.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/numbers/{numberId}/release`

**Access Level**: 🔒 Protected · Bearer API key

**Success Response**: the number with `status: "ALLOCATED"`, `accountId: null`, `incomingScriptVersionId: null`.

**Errors**: `409 NUMBER_BUSY` · `409 NUMBER_IN_CALLOUT`.

---

## C6. Transfer a number to another account
**Purpose**: Move an assigned number straight to another account of the same client app. The old account's flow is
cleared, so pin the new account's flow next ([C7](#c7-put-a-flow-on-a-number)).

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/numbers/{numberId}/transfer`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{ "accountId": "51f815ba-e9eb-4e29-9297-3c56033d32cd", "label": "Oda", "by": "admin@business.co.tz" }
```

**Errors**:
```json
{ "success": false, "httpStatus": "409 CONFLICT", "message": "The number is already on this account",
  "code": "SAME_ACCOUNT", "action_time": "2026-11-13T19:58:07.424064672", "data": null }
```
`409 NUMBER_NOT_ASSIGNED` (assign it instead) · `409 ACCOUNT_NOT_ACTIVE` · `409 NUMBER_BUSY` · `409 NUMBER_IN_CALLOUT`.

---

## C7. Put a flow on a number
**Purpose**: Choose the flow version callers hear. Takes effect for the next call; calls in progress keep their version.

**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/numbers/{numberId}/script`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{ "scriptVersionId": "e183c7f1-3137-4848-95c1-11a7e8bef17a" }
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `scriptVersionId` | uuid / null | Yes | A runnable version of an **Incoming** flow of the number's account (`versionId` from [Part 2B](2b-flows.md)); `null` takes the flow off (calls are refused) |

**Success Response**: the number, `message` "Incoming script set".

**Errors**:
- `400` "Assign the number to an account first"
- `400` "Script version belongs to another account" · `400` "Numbers run Incoming scripts only"
- `404 NOT_FOUND` "Script version not found"
- `409 VERSION_NOT_RUNNABLE` "Only runnable versions can be put on a number" (approve it, [Part 2B D7](2b-flows.md#d7-approve-a-version-make-it-runnable))

---

## C8. Set where agents are rung
**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/numbers/{numberId}/ring-mode`

**Access Level**: 🔒 Protected · Bearer API key

**Request JSON Sample**:
```json
{ "ringMode": "BROWSER_AND_MOBILE" }
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ringMode` | string | No | `BROWSER` (default; also when missing) · `MOBILE` (agents' `mobile` numbers) · `BROWSER_AND_MOBILE` |

**Success Response**: the number, `message` "Ring mode set".

**Errors**: an unknown value → `400` "Request body is not valid JSON".