diff --git a/.well-known/agent-skills/didww-connect.tar.gz b/.well-known/agent-skills/didww-connect.tar.gz index 6ce99f4..be12b10 100644 Binary files a/.well-known/agent-skills/didww-connect.tar.gz and b/.well-known/agent-skills/didww-connect.tar.gz differ diff --git a/.well-known/agent-skills/didww-to-vogent.tar.gz b/.well-known/agent-skills/didww-to-vogent.tar.gz index 790f557..03bb61a 100644 Binary files a/.well-known/agent-skills/didww-to-vogent.tar.gz and b/.well-known/agent-skills/didww-to-vogent.tar.gz differ diff --git a/.well-known/agent-skills/index.json b/.well-known/agent-skills/index.json index 31b44f1..cf0245c 100644 --- a/.well-known/agent-skills/index.json +++ b/.well-known/agent-skills/index.json @@ -10,8 +10,8 @@ "type": "archive", "description": "Connect a DIDWW phone number to ANY AI voice platform: helps choose the platform, then wires the SIP trunk pair and number. Use whenever the user wants a voice AI agent on a DIDWW number and has not committed to a platform, asks which AI platform to use with DIDWW, or names a platform that has no dedicated didww-to-X skill installed. Covers ElevenLabs, Vapi, Retell, Ultravox, Vogent, OpenAI Realtime, xAI, LiveKit and the enterprise-gated platforms.", "url": "didww-connect.tar.gz", - "digest": "sha256:50a913c53d738eeb409f565143d176998a04edd8008dcbdbf0777a056a52a9f5", - "version": "1.0.2", + "digest": "sha256:625aba79d3bfbface58b6897546596fe84ca768568755668af946789adf2f56f", + "version": "1.0.3", "documentation": "https://github.com/didww/ai/blob/main/docs/didww-connect.md", "install": "npx didww-connect" }, @@ -78,10 +78,10 @@ { "name": "didww-to-vogent", "type": "archive", - "description": "Connect a DIDWW phone number to a Vogent voice agent over SIP import. Use whenever the user wants a Vogent agent on a real phone number via DIDWW, mentions pairing DIDWW with Vogent, sip_import numbers, sip.vogent.ai, or debugging calls between the two.", + "description": "Connect a DIDWW phone number to a Vogent voice agent over SIP import. Use whenever the user wants a Vogent agent on a real phone number via DIDWW, mentions pairing DIDWW with Vogent, sip_import numbers, sip.vogent.ai, warm transfers from a Vogent agent to a live person, or debugging calls between the two.", "url": "didww-to-vogent.tar.gz", - "digest": "sha256:1c2227a36a7909cc0fd6c7ebe496c275070b466086a9e0c18614bd05b4df31bd", - "version": "1.0.2", + "digest": "sha256:2e757a041ae71ff66e4cd119c0b5c02bd41519218d0e68bc21411d3e4955b380", + "version": "1.0.3", "documentation": "https://github.com/didww/ai/blob/main/docs/didww-to-vogent.md", "install": "npx didww-to-vogent" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index bb10552..a6dff6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to the skills in this repository. Each skill is versioned independently in its `package.json`. +## didww-to-vogent 1.0.3, didww-connect 1.0.3 (2026-11-03) + +Found while connecting a DIDWW number to a Vogent agent with the skill, verified on real calls, and aligned with DIDWW's [Vogent integration guide](https://doc.didww.com/integrations/vogent/index.html): the skill and its docs page now follow the guide's three sections (Route Incoming Calls, Enable Outbound Calling, Connect DIDWW SIP Trunks in Vogent), its terms and its troubleshooting. + +- **Transfers work only as Warm Transfer.** Cold Transfer (SIP REFER, target built from the phone number, no SIP URI setting) is marked `TRANSFERRED` in Vogent, but DIDWW never places the call: no outbound CDR, also with `0.0.0.0/0` open. Warm Transfer makes Vogent dial the destination itself through the number's `terminationUri`, the DIDWW outbound trunk, from Vogent's servers. New "Live-agent transfers" section following the guide's Step 4: the Call Transfer function (`POST /api/functions`, `allowedNumbers`, never a number linked to the agent), the Transfer Type, which only the dashboard sets (new functions default to Cold; Prompted needs a prompt, which switching to Cold and back clears), adding it to the agent (`PUT /api/agents/{id}` with `linkedFunctionDefinitionInputs`) and the prompt line. +- **The outbound trunk is required even for inbound-only**, because Vogent imports a number only with an outbound trunk's host and credentials (the docs said "needed only for calls the agent places"). +- **Outbound trunk Allowed SIP IPs** as in the guide: Vogent's enterprise ranges plus DIDWW's seven inbound signaling IPs; without enterprise ranges, DIDWW support; `0.0.0.0/0` only for temporary testing at the user's own risk; `0.0.0.0/32` matches nothing. +- **Inbound trunk:** UDP 5060, User Part of R-URI `{DID}` (Vogent also accepts `+{DID}`), and Authorization with the outbound trunk's username and password instead of an invented digest pair. +- **Linking the number to the agent is not dashboard-only.** `PUT /api/phone_numbers/{id}` with `{"agentId"}` works. New `connect.py --agent` links right after the import, and `--phone-id ID --agent ID` links a number imported earlier; the "MANUAL STEP" print is gone. +- **Opening line:** without one the agent waits for the caller and the call sounds silent. Standard agents: Opening Line, Inbound calls Only; flow agents: `flowDefinition.openingLineType` `INBOUND_ONLY`. +- **Testing** as in the guide's Step 5, including `POST /api/dials` for an outbound call and Dial History's Transferred status with the `transfer_call` function. +- **Failure map** from the guide's troubleshooting: 401/403/407 on outbound calls or transfers, 484 Address Incomplete, rejection after authentication, the caller shown with an extra `+1` (cosmetic; `cli_prefix` `+` is a likely fix, marked untested), and the API key's secret versus its ID. +- `didww-connect`: Vogent's row, ingress notes and allowed-IP notes follow the guide. ## Agent Skills discovery index (2026-10-30) - New `.well-known/agent-skills/index.json` in the [Agent Skills discovery](https://github.com/cloudflare/agent-skills-discovery-rfc) format v0.2.0, the same format Telnyx serves at `telnyx.com/.well-known/agent-skills/index.json`. Every skill is a `type: "archive"` entry: a `.tar.gz` with `SKILL.md`, `scripts/`, `references/` and `LICENSE` at the root, and its sha256 `digest`. Extra fields (`version`, `documentation`, `install`) are ignored by clients that do not know them. diff --git a/README.md b/README.md index 6aaf958..9f467f7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Open-source tools from [DIDWW](https://www.didww.com/) for building AI voice age | [didww-to-vapi](skills/didww-to-vapi) | Vapi | [![npm](https://img.shields.io/npm/v/didww-to-vapi?label=)](https://www.npmjs.com/package/didww-to-vapi) | BYO SIP trunk credential, number and assistant binding | | [didww-to-retell](skills/didww-to-retell) | Retell AI | [![npm](https://img.shields.io/npm/v/didww-to-retell?label=)](https://www.npmjs.com/package/didww-to-retell) | Fully MCP-driven: agent, number import and binding in one call | | [didww-to-ultravox](skills/didww-to-ultravox) | Ultravox | [![npm](https://img.shields.io/npm/v/didww-to-ultravox?label=)](https://www.npmjs.com/package/didww-to-ultravox) | Address-based routing and account SIP settings | -| [didww-to-vogent](skills/didww-to-vogent) | Vogent | [![npm](https://img.shields.io/npm/v/didww-to-vogent?label=)](https://www.npmjs.com/package/didww-to-vogent) | SIP import plus one documented dashboard step | +| [didww-to-vogent](skills/didww-to-vogent) | Vogent | [![npm](https://img.shields.io/npm/v/didww-to-vogent?label=)](https://www.npmjs.com/package/didww-to-vogent) | SIP import and agent linking; warm transfer type is one dashboard setting | | [didww-to-openai-realtime](skills/didww-to-openai-realtime) | OpenAI Realtime | [![npm](https://img.shields.io/npm/v/didww-to-openai-realtime?label=)](https://www.npmjs.com/package/didww-to-openai-realtime) | Direct SIP to the Realtime API with a generated webhook backend | | [didww-to-xai](skills/didww-to-xai) | xAI Grok | [![npm](https://img.shields.io/npm/v/didww-to-xai?label=)](https://www.npmjs.com/package/didww-to-xai) | Number registration with digest auth, per-call session control | | [didww-to-livekit](skills/didww-to-livekit) | LiveKit | [![npm](https://img.shields.io/npm/v/didww-to-livekit?label=)](https://www.npmjs.com/package/didww-to-livekit) | Inbound trunk, dispatch rule and optional outbound trunk for a LiveKit worker | diff --git a/docs/didww-connect.md b/docs/didww-connect.md index e981230..7ff028c 100644 --- a/docs/didww-connect.md +++ b/docs/didww-connect.md @@ -65,7 +65,7 @@ The skill asks one question, maps the answer to a platform, and tells you the au | API-first, EU region, bring your own model keys | Vapi | REST script; Vapi's MCP is read-only for numbers | | Maximum MCP automation, one-call setup | Retell | Retell's MCP imports numbers and creates agents | | Richest call transfers, own speech model | Ultravox | REST; address-based routing, no number import | -| Small and simple | Vogent | REST import; agent binding is one dashboard click | +| Small and simple | Vogent | REST import and agent linking; the warm transfer type is one dashboard setting | | You own the agent code, minimal stack | OpenAI Realtime or xAI Grok | A webhook backend is required; not no-code | | Any STT, LLM and TTS combination, open source | LiveKit | REST script creates trunk and dispatch rule; the worker is your code | | Data control, own servers | LiveKit self-hosted | Same skill; you run the SIP service | @@ -81,7 +81,7 @@ The skill asks one question, maps the answer to a platform, and tells you the au The number and the inbound trunk are configured through the DIDWW MCP under your own account permissions. Claude Code asks for confirmation before each tool call unless you have allowed that tool. The outbound trunk is created by you in the DIDWW User Panel, because the MCP has no outbound trunk tools. - **Inbound SIP trunk.** Host and port are the chosen platform's SIP ingress (see the table below), digest authentication is preferred over IP filtering, and media encryption matches the platform's setting. The DID is assigned to this trunk. -- **Outbound SIP trunk.** Needed only for calls the agent places. You create it in the DIDWW User Panel under **Voice**, **Outbound Trunks**, **Create New**, because the DIDWW MCP has no outbound trunk tools. If that page shows **Get Access**, the account is not approved for outbound calling yet: the application needs a business account with a company name and VAT/TAX ID, and DIDWW replies within 48 business hours. The inbound side can go live meanwhile. Choose the **Credentials & IP-Based** authentication method. It needs at least one **Allowed SIP IP address**: the platform's SIP source addresses, listed on each platform's page. Where a platform publishes none, contact DIDWW support (support@didww.com). Do not enter `0.0.0.0/0`; DIDWW advises against open ranges. Set **Media encryption mode** to match the platform side. DIDWW generates the username and password; give them to the agent in a file or environment variables rather than pasting them into the chat, and it passes them to the platform side. Enable the trunk if it shows **Disabled**. The host is the one nearest to the platform's region: `nyc.us.out.didww.com`, `lac.us.out.didww.com`, `mia.us.out.didww.com`, `fra.eu.out.didww.com`, `ams.eu.out.didww.com`, `sg.out.didww.com` or `hk.out.didww.com`. +- **Outbound SIP trunk.** Needed for calls the agent places and for transfers, and for Vogent always, because its number import needs the trunk's credentials. You create it in the DIDWW User Panel under **Voice**, **Outbound Trunks**, **Create New**, because the DIDWW MCP has no outbound trunk tools. If that page shows **Get Access**, the account is not approved for outbound calling yet: the application needs a business account with a company name and VAT/TAX ID, and DIDWW replies within 48 business hours. The inbound side can go live meanwhile. Choose the **Credentials & IP-Based** authentication method. It needs at least one **Allowed SIP IP address**: the platform's SIP source addresses, listed on each platform's page. Where a platform publishes none, contact DIDWW support (support@didww.com). Do not enter `0.0.0.0/0`; DIDWW advises against open ranges. For Vogent, whose warm transfers dial from its own servers, the [Vogent integration guide](https://doc.didww.com/integrations/vogent/index.html) allows `0.0.0.0/0` only for temporary testing, at your own risk: see [DIDWW to Vogent](didww-to-vogent.md). Set **Media encryption mode** to match the platform side. DIDWW generates the username and password; give them to the agent in a file or environment variables rather than pasting them into the chat, and it passes them to the platform side. Enable the trunk if it shows **Disabled**. The host is the one nearest to the platform's region: `nyc.us.out.didww.com`, `lac.us.out.didww.com`, `mia.us.out.didww.com`, `fra.eu.out.didww.com`, `ams.eu.out.didww.com`, `sg.out.didww.com` or `hk.out.didww.com`. - **Spend controls.** On the outbound trunk, under **Termination**: **24 Hour Limit (USD)** at its minimum of 50 (the default is 3000, and DIDWW disables the trunk when the limit is reached), **Capacity limit** matching the expected concurrency, and the usage limit notification on. Under **CLI Settings**, allow only the connected number as caller ID; under **Destination Dialing Settings**, reject all destinations except the countries the agent calls. On the inbound trunk the skill sets a capacity limit through the MCP. - **Number.** An existing DID is confirmed, or one is bought. For registration-required countries the skill says so before promising a date. @@ -93,7 +93,7 @@ The number and the inbound trunk are configured through the DIDWW MCP under your | Vapi | `sip.vapi.ai` (EU: `sip.eu.vapi.ai`) | Vapi's inbound credential lists DIDWW's seven signaling IPs; Max transfers 1 for transfers | | Retell | `sip.retellai.com` | TCP | | Ultravox | The account's own SIP domain from `GET /api/sip` | Address-based routing to `agent_@` | -| Vogent | `sip.vogent.ai` | Digest auth | +| Vogent | `sip.vogent.ai`, port 5060, UDP | User Part of R-URI `{DID}` or `+{DID}`; Authorization with the outbound trunk's credentials; transfers only as Warm Transfer | | OpenAI Realtime | `sip:{project_id}@sip.api.openai.com;transport=tls` | TLS and SRTP mandatory; EU host `sip-eu.api.openai.com` | | xAI Grok | `sip:{number}@sip.voice.x.ai;transport=tls` | Digest auth set when the number is registered with xAI | | LiveKit | `sip:{sip_subdomain}.sip.livekit.cloud` (copy the SIP URI from Project settings) | 5060 TCP/UDP or 5061 TLS; region-pinned `{sip_subdomain}.{eu|us|uk|india|japan|aus|canada|sa}.sip.livekit.cloud`; numbers carry the `+` | diff --git a/docs/didww-to-vogent.md b/docs/didww-to-vogent.md index cbfe171..6889789 100644 --- a/docs/didww-to-vogent.md +++ b/docs/didww-to-vogent.md @@ -1,19 +1,25 @@ # DIDWW to Vogent skill -Use the **didww-to-vogent** skill with **Claude Code** to connect a DIDWW phone number to a **Vogent** voice agent. Vogent imports the number in one API call; the one step its API does not cover, attaching the agent to the number, is a single click that the skill walks you through. +Use the **didww-to-vogent** skill with **Claude Code** to connect a DIDWW phone number to a **Vogent** voice agent. Vogent's API imports the number and links it to the agent; the one setting its API does not expose, the transfer type of a Call Transfer function, is a single dashboard step that the skill walks you through. -- Import the DIDWW number into Vogent as a `sip_import` number with the DIDWW outbound host and credentials. -- Build the DIDWW inbound trunk against `sip.vogent.ai` with digest authentication. -- Finish with the one documented dashboard step and a verification call. +- Route incoming calls from a DIDWW number to a Vogent agent, with an opening line that greets the caller. +- Import the DIDWW number into Vogent over SIP with the DIDWW outbound trunk's host and credentials, and link it to the agent. +- Place outbound calls through DIDWW and transfer active calls to a live person with Vogent warm transfers. + +This skill automates the steps described in the [Vogent integration guide](https://doc.didww.com/integrations/vogent/index.html). Read that guide for the manual path and screenshots. + +> [!IMPORTANT] +> Transfers from a Vogent agent through DIDWW work only as **Warm Transfer**, where Vogent places the transfer call itself through your DIDWW outbound trunk. **Cold Transfer** is marked as transferred in Vogent, but DIDWW never places the call. Both were verified on real calls in October 2026. ## How the skill works | Side | Path the skill uses | Authentication | |---|---|---| | DIDWW: number, inbound trunk | DIDWW MCP `https://api.didww.com/mcp` | OAuth sign-in, no keys | -| DIDWW: outbound trunk, spend limits | You, in the DIDWW User Panel. The MCP has no outbound trunk tools | Your panel login | -| Vogent: number import | REST `POST /api/phone_numbers` via `scripts/connect.py` | `VOGENT_API_KEY` (Bearer) | -| Vogent: agent-to-number binding | Dashboard, one step | Your Vogent sign-in | +| DIDWW: outbound trunk, inbound trunk authorization, spend limits | You, in the DIDWW User Panel. The MCP has no outbound trunk tools, and the outbound password stays out of the chat | Your panel login | +| Vogent: number import and linking the number to the agent | REST `POST /api/phone_numbers`, then `PUT /api/phone_numbers/{id}`, via `scripts/connect.py` | `VOGENT_API_KEY` (Bearer) | +| Vogent: Call Transfer function | REST `POST /api/functions` and `PUT /api/agents/{id}` | `VOGENT_API_KEY` (Bearer) | +| Vogent: transfer type (Warm Transfer) | Dashboard, one setting: the API does not expose it | Your Vogent sign-in | ## Before you begin @@ -26,8 +32,8 @@ Use the **didww-to-vogent** skill with **Claude Code** to connect a DIDWW phone ``` Then run `/mcp` inside Claude Code and sign in to the DIDWW User Panel when the browser opens. See [Getting started with DIDWW MCP](https://doc.didww.com/mcp/getting-started.html). -- For outbound calls, access to DIDWW outbound trunks on the account. If **Voice**, **Outbound Trunks** in the DIDWW User Panel shows **Get Access**, apply there first ([Get access to outbound trunks](https://doc.didww.com/voice/outbound-trunks/get-access.html)): it needs a business account with a company name and VAT/TAX ID, and DIDWW replies within 48 business hours. Inbound calls work without it. -- A Vogent account with an agent, and an API key from the API Keys page in the `app.vogent.ai` sidebar. +- Access to DIDWW outbound trunks on the account, **even for an inbound-only line**: Vogent imports a number over SIP only with the termination address and credentials of an outbound trunk. If **Voice**, **Outbound Trunks** in the DIDWW User Panel shows **Get Access**, apply there first ([Get access to outbound trunks](https://doc.didww.com/voice/outbound-trunks/get-access.html)): it needs a business account with a company name and VAT/TAX ID, and DIDWW replies within 48 business hours. +- A [Vogent](https://app.vogent.ai/) account with an agent, and a Vogent API key: in Vogent, go to **API > Keys**, click **New Key**, and copy the secret key. The UUID-shaped value in the key list is the key's ID, not the key, and Vogent answers it with "unauthorized". ## Step 1. Install the skill @@ -60,51 +66,75 @@ The skill confirms the number (or country to buy one in), the agent, and a trans ### DIDWW side -The number and the inbound trunk are configured through the DIDWW MCP under your own account permissions. Claude Code asks for confirmation before each tool call unless you have allowed that tool. The outbound trunk is created by you in the DIDWW User Panel, because the MCP has no outbound trunk tools. +The number and the inbound trunk are configured through the DIDWW MCP under your own account permissions. Claude Code asks for confirmation before each tool call unless you have allowed that tool. The outbound trunk, and the outbound credentials on the inbound trunk, are set by you in the DIDWW User Panel. -- **Inbound SIP trunk.** Host `sip.vogent.ai`, digest authentication with a generated username and password. The DID is assigned to this trunk. -- **Outbound SIP trunk.** Needed only for calls the agent places. You create it in the DIDWW User Panel under **Voice**, **Outbound Trunks**, **Create New**, because the DIDWW MCP has no outbound trunk tools. If that page shows **Get Access**, the account is not approved for outbound calling yet: the application needs a business account with a company name and VAT/TAX ID, and DIDWW replies within 48 business hours. The inbound side can go live meanwhile. Choose the **Credentials & IP-Based** authentication method. It needs at least one **Allowed SIP IP address**: Vogent has no static SIP source range outside its enterprise plan; enterprise customers get the range from Vogent, anyone else contacts DIDWW support (support@didww.com). Do not enter `0.0.0.0/0`; DIDWW advises against open ranges. Set **Media encryption mode** to match the platform side. DIDWW generates the username and password; give them to the agent in a file or environment variables rather than pasting them into the chat, and it passes them to the platform side. Enable the trunk if it shows **Disabled**. The host is the one nearest to the platform's region: `nyc.us.out.didww.com`, `lac.us.out.didww.com`, `mia.us.out.didww.com`, `fra.eu.out.didww.com`, `ams.eu.out.didww.com`, `sg.out.didww.com` or `hk.out.didww.com`. -- **Spend controls.** On the outbound trunk, under **Termination**: **24 Hour Limit (USD)** at its minimum of 50 (the default is 3000, and DIDWW disables the trunk when the limit is reached), **Capacity limit** matching the expected concurrency, and the usage limit notification on. Under **CLI Settings**, allow only the connected number as caller ID; under **Destination Dialing Settings**, reject all destinations except the countries the agent calls. On the inbound trunk the skill sets a capacity limit through the MCP. +- **Inbound SIP trunk** ([1. Route Incoming Calls to Vogent](https://doc.didww.com/integrations/vogent/index.html#vogent-inbound)). Host `sip.vogent.ai`, port 5060, transport UDP. **User Part of R-URI** keeps the default `{DID}`; Vogent accepts the called number with or without the `+`, so `+{DID}` also works. The DID is assigned to this trunk, with a capacity limit set through the MCP. +- **Outbound SIP trunk** ([2. Enable Outbound Calling from Vogent through DIDWW](https://doc.didww.com/integrations/vogent/index.html#vogent-outbound)). Required even for an inbound-only line: Vogent needs its host and credentials to import the number, and it carries outbound calls and warm transfers. You create it in the DIDWW User Panel under **Voice**, **Outbound Trunks**, **Create New**. If that page shows **Get Access**, the account is not approved for outbound calling yet; the inbound trunk can be prepared meanwhile. Keep the **Credentials & IP-based** authentication method. In **Allowed SIP IP addresses**, enter the outbound SIP source IP ranges Vogent provides for your enterprise account, together with DIDWW's inbound signaling IPs, as listed in the guide. Vogent does not provide static ranges outside its enterprise plan; without them, contact DIDWW support (support@didww.com) to confirm a supported authentication setup. `0.0.0.0/0` removes the IP restriction and is meant only for temporary testing, at your own risk: SIP digest authentication still applies, but DIDWW does not recommend it. Do not enter `0.0.0.0/32`, which matches no address and silently blocks every call. DIDWW generates the username and password (the key icon in the **Credentials** column); give them to the agent in a file or environment variables rather than pasting them into the chat. Enable the trunk if it shows **Disabled**. The host is the one nearest to where calls are placed: `nyc.us.out.didww.com`, `lac.us.out.didww.com`, `mia.us.out.didww.com`, `fra.eu.out.didww.com`, `ams.eu.out.didww.com`, `sg.out.didww.com` or `hk.out.didww.com`. +- **Outbound credentials on the inbound trunk** ([Step 4: Add Outbound Credentials to the Inbound Trunk](https://doc.didww.com/integrations/vogent/index.html#vogent-inbound-authorization)). On the Vogent inbound trunk, **Authorization**, **Enable Authorization**, with **Auth User** and **Auth Password** set to the outbound trunk's username and password. You enter them in the DIDWW User Panel so the outbound password stays out of the chat. +- **Spend controls.** On the outbound trunk, under **Termination**: **24 Hour Limit (USD)** at its minimum of 50 (the default is 3000, and DIDWW disables the trunk when the limit is reached), **Capacity limit** matching the expected concurrency, and the usage limit notification on. Under **Destination Dialing Settings**, reject all destinations except the countries the agent calls or transfers to. Under **CLI Settings**, the verified transfer setup used **Send Original CLI** on CLI mismatch with any DID allowed; a stricter caller ID rule is untested with transfers. ### Vogent side -One call to the phone numbers API with `type: "sip_import"` carrying the E.164 number, the `terminationUri` set to the DIDWW outbound host, and the DIDWW outbound trunk credentials: +These follow [3. Connect DIDWW SIP Trunks in Vogent](https://doc.didww.com/integrations/vogent/index.html#vogent-connect) in the guide. + +- **Number import** ([Step 1: Import Your DIDWW Number into Vogent](https://doc.didww.com/integrations/vogent/index.html#vogent-import-number)). One call to the phone numbers API with `type: "sip_import"` carrying the E.164 number with the `+`, the `terminationUri` set to a DIDWW outbound signaling endpoint as a hostname only (no `sip:` prefix or port), and the DIDWW outbound trunk credentials. +- **Link the number to your agent** ([Step 2: Link the Number to Your Agent](https://doc.didww.com/integrations/vogent/index.html#vogent-link-number)). `PUT /api/phone_numbers/{id}` with `{"agentId": "..."}`, the ID shown under your agent's name. The script does it with `--agent`. **Agents**, your agent, **Numbers**, **Link Number** in the dashboard does the same. +- **Opening line** ([Step 3: Set the Agent's Opening Line](https://doc.didww.com/integrations/vogent/index.html#vogent-opening-line)). Without one the agent waits for the caller to speak first. For a **Standard** agent the skill sets **Opening Line** to **Inbound calls Only** with a greeting; for a **Flow** agent the greeting lives in the flow (`flowDefinition.openingLineType` `INBOUND_ONLY` in its versioned prompt). ```bash -VOGENT_API_KEY=... python3 ~/.claude/skills/didww-to-vogent/scripts/connect.py \ - --number +33144556677 --didww-host fra.eu.out.didww.com --didww-user USER --didww-pass PASS +DIDWW_TRUNK_PASS=... VOGENT_API_KEY=... python3 ~/.claude/skills/didww-to-vogent/scripts/connect.py \ + --number +33144556677 --didww-host fra.eu.out.didww.com --didww-user USER --agent AGENT_ID ``` +To link the agent to a number imported earlier, run `connect.py --phone-id PHONE_NUMBER_ID --agent AGENT_ID`. + +## Step 4. Add call transfers to your live agent + +This follows [Step 4: Add Call Transfers to Your Live Agent](https://doc.didww.com/integrations/vogent/index.html#vogent-transfer-calls) in the guide. Call transfers in Vogent are a **Call Transfer** function that the agent runs during a call, for example when the caller asks for a person. Vogent offers **Cold Transfer** and **Warm Transfer**. With DIDWW only Warm Transfer works: + +- **Cold Transfer** hands the call to DIDWW with a SIP REFER whose target Vogent builds from the phone number alone; it has no SIP URI setting. Vogent marks the call as transferred, but DIDWW never places the new call: there is no outbound call record and the caller waits in silence. +- **Warm Transfer** makes Vogent place a new call to the destination itself, through the number's `terminationUri` (the DIDWW outbound trunk) from Vogent's own servers, so the outbound trunk's **Allowed SIP IP addresses** must accept them. Vogent briefs the person who answers, then connects the caller. Both call legs stay connected for the transferred conversation, so Vogent's per-minute rate and the DIDWW outbound call both run; review both charges before using transfers. + +The skill creates the function with `POST /api/functions` (`type` `transfer`, the destination in `allowedNumbers` as `{"number": "+E164"}`, and a started message for the caller), adds it to the agent with `PUT /api/agents/{id}` (`linkedFunctionDefinitionInputs`, listing every function the agent keeps), and for a Standard agent adds a prompt line saying when to call `transfer_call` and with which destination. For a Flow agent, the transfer is a function call node in the flow. Do not use a DIDWW number that is linked to your Vogent agent as the destination, or the transferred call returns to your agent. + > [!IMPORTANT] -> The agent binding is not in the API response. Open the agent in the Vogent dashboard, go to the **Numbers** tab and attach the imported number. The skill tells you exactly this and waits for your confirmation before the verification call. +> Finish the function in the Vogent dashboard: open **Functions**, the Call Transfer function, **Config**, and set **Transfer Type** to **Warm Transfer**. The API does not expose this setting, and new functions default to Cold Transfer. With **Warm Transfer Type** set to **Prompted**, enter the **Prompt** the agent uses to brief the person who answers. Switching to Cold Transfer and back clears that prompt; enter it again before you save. -## Step 4. Verify the connection +## Step 5. Test the configuration -The skill ends every build with the same checks. Do them before real traffic: +The skill ends every build with the same checks ([Step 5: Test the Configuration](https://doc.didww.com/integrations/vogent/index.html#vogent-test)). Do them before real traffic: -1. One inbound call from a mobile phone reaches the agent. -2. One outbound call shows the DIDWW number as caller ID. -3. One transfer, if configured. +1. **Inbound call.** Call the DIDWW number: the agent answers with its opening line, and **Dial History** in Vogent shows the call as **Incoming**. +2. **Outbound call**, if the agent places calls. From the agent's **Make Call**, **Phone Call**, with your DIDWW number as **From Number**, or with `POST /api/dials` (`callAgentId`, `fromNumberId`, `toNumber` in E.164). Check the caller ID. +3. **Call transfer**, if configured. Ask the agent for a person: the destination rings and hears the briefing, then both sides hear each other. **Dial History** shows **Transferred** and the `transfer_call` function in the **Transcript** tab; the status alone does not confirm that the recipient answered. The DIDWW **Outbound Call Logs** show the transfer as a call through the Vogent outbound trunk. ## Troubleshooting -The skill carries this failure map and works through it when a test call fails. +The skill carries this failure map, which matches the guide's [Troubleshooting](https://doc.didww.com/integrations/vogent/index.html#troubleshooting), and works through it when a test call fails. | Symptom | Likely cause | Fix | |---|---|---| -| Inbound rejected | Digest pair mismatch between the DIDWW trunk and what Vogent expects for the imported number | Re-enter the same username and password on both sides | -| Import accepted but calls fail | `terminationUri` has a `sip:` prefix or is not the region-nearest DIDWW host | Use the bare host, for example `fra.eu.out.didww.com` | -| Agent does not answer | The dashboard binding step was skipped | Attach the number in the agent's Numbers tab | -| Outbound call rejected by DIDWW | Source IP not in the outbound trunk's Allowed SIP IP addresses, caller ID not allowed in its CLI settings, destination not allowed, or the 24-hour limit reached, which disables the trunk | Check the outbound trunk in the DIDWW User Panel and re-enable it | +| The call connects, but nobody speaks | The agent waits for the caller to speak first | Set **Opening Line** to **Inbound calls Only** with a greeting | +| The call is not answered by the agent | The number is not linked to the agent, **User Part of R-URI** is not `{DID}` or `+{DID}`, the number is not imported in E.164 with the `+`, or the inbound trunk is not assigned to the DID | Link the number with `connect.py --phone-id ... --agent ...` or on the agent's **Numbers** tab, and check the inbound trunk and the import | +| The number import fails in Vogent | **Termination URI** is not a bare DIDWW outbound signaling endpoint, or the username and password are not the outbound trunk's | Use the hostname only, for example `fra.eu.out.didww.com`, and the outbound trunk credentials | +| **Dial History** shows **Transferred**, but the destination does not ring | **Transfer Type** is not **Warm Transfer**, the outbound trunk's **Allowed SIP IP addresses** do not accept Vogent's servers, or the destination is not in **Allowed Numbers** | Set **Warm Transfer**, fix the allowed addresses, and add the destination. A transfer DIDWW does not receive leaves no entry in the **Outbound Call Logs** | +| **401**, **403** or **407** on an outbound call or transfer | The imported number's credentials differ from the outbound trunk's, the inbound trunk's **Authorization** is off or uses another pair, or the allowed addresses do not accept Vogent's servers | Put the outbound trunk's username and password on both, and fix the allowed addresses | +| **484 Address Incomplete** on an outbound call | The number is not in full E.164 format | Dial `+` and the full international number, without `0`, `00` or `011` prefixes | +| The outbound call or transfer is rejected after authentication | Caller ID not allowed in **CLI Settings**, destination not allowed, the **24 Hour Limit (USD)** or **Capacity limit** reached, or the account balance too low | Check the outbound trunk and the balance in the DIDWW User Panel, and re-enable the trunk if the limit disabled it | +| Vogent shows the caller's number with an extra **+1** | DIDWW sends the caller's number without the `+`, and Vogent reads it as a US number; a call from `+442079460000` appears with `+1` in front | Calls connect normally; only the number shown in Vogent is affected. A caller ID prefix `+` on the DIDWW inbound trunk is the likely fix, not yet tested | +| The Vogent API returns "unauthorized" | The request uses the ID of the API key instead of the secret key | Create a new key under **API > Keys** and copy the secret key | ## Keys, billing and safety -- **Vogent credentials.** API keys are on the API Keys page in the `app.vogent.ai` sidebar. Vogent publishes no pricing page; confirm the per-minute rate inside the panel before scaling. +- **Vogent credentials.** In Vogent, go to **API > Keys**, click **New Key**, and copy the secret key. Vogent publishes no pricing page; confirm the per-minute rate inside the panel before scaling. +- **Call details.** **Dial History** in Vogent shows each call's transcript, function calls and completion reason. `GET /api/dials/{id}` returns the same, but the API cannot list calls, so the skill takes the id from the dashboard URL. - **Two balances.** DIDWW is prepaid, and so is the AI platform. Either one reaching zero stops calls silently. In the DIDWW User Panel, open **Billing, Payment Methods** to top up, enable **Auto-charge**, and switch on the **Low Balance Notification**, which is off by default. -- **Cost per minute.** The real cost is the platform's per-minute or per-token price plus DIDWW call charges. The skill quotes the all-in figure before real traffic starts; check the platform's current pricing page, these numbers change. +- **Cost per minute.** The real cost is the platform's per-minute or per-token price plus DIDWW call charges, and a warm transfer adds a second DIDWW call for the transferred conversation. The skill quotes the all-in figure before real traffic starts; check the platform's current pricing page, these numbers change. ## Additional resources +- [Vogent integration guide](https://doc.didww.com/integrations/vogent/index.html): the same connection configured by hand. +- [Vogent SIP trunking overview](https://docs.vogent.ai/telephony/sip/overview), [create phone number API](https://docs.vogent.ai/api-reference/create-number), [update phone number API](https://docs.vogent.ai/api-reference/update-phone-number) and [function calling](https://docs.vogent.ai/platform-overview/tools/function-calling) in the Vogent documentation. - [Inbound trunks](https://doc.didww.com/voice/inbound-trunks/index.html) and [Outbound trunks](https://doc.didww.com/voice/outbound-trunks/index.html) in the DIDWW documentation. - [DIDWW MCP](https://doc.didww.com/mcp/index.html): the connector the skill uses for the carrier side. - Skill source and tests: [`skills/didww-to-vogent`](https://github.com/didww/ai/tree/main/skills/didww-to-vogent) in the didww/ai repository. Package: [npmjs.com/package/didww-to-vogent](https://www.npmjs.com/package/didww-to-vogent). diff --git a/docs/index.md b/docs/index.md index 7ef2e5b..b5c25bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ DIDWW publishes a family of **agent skills** for **Claude Code** that put an AI | [didww-to-vapi](didww-to-vapi.md) | Vapi | `npx didww-to-vapi` | REST script; inbound + outbound credentials, number, transfers | | [didww-to-retell](didww-to-retell.md) | Retell AI | `npx didww-to-retell` | Fully MCP-driven when Retell's MCP is connected | | [didww-to-ultravox](didww-to-ultravox.md) | Ultravox | `npx didww-to-ultravox` | Address-based routing; no number import | -| [didww-to-vogent](didww-to-vogent.md) | Vogent | `npx didww-to-vogent` | REST import plus one dashboard click | +| [didww-to-vogent](didww-to-vogent.md) | Vogent | `npx didww-to-vogent` | REST import and agent linking; one dashboard setting for warm transfers | | [didww-to-openai-realtime](didww-to-openai-realtime.md) | OpenAI Realtime | `npx didww-to-openai-realtime` | Direct SIP; you run a webhook backend | | [didww-to-xai](didww-to-xai.md) | xAI Grok | `npx didww-to-xai` | Direct SIP with number registration; you run a backend | | [didww-to-livekit](didww-to-livekit.md) | LiveKit | `npx didww-to-livekit` | Trunk and dispatch rule; you run the worker | diff --git a/skills/didww-connect/SKILL.md b/skills/didww-connect/SKILL.md index ad22213..835df39 100644 --- a/skills/didww-connect/SKILL.md +++ b/skills/didww-connect/SKILL.md @@ -28,7 +28,7 @@ Ask what matters most and map: | API-first, EU region, BYO model keys | Vapi | REST scripts (their MCP is read-only for numbers) | | Max MCP automation, one-call setup | Retell | Their MCP imports numbers and creates agents | | Richest transfers, own speech model | Ultravox | REST; address-based routing, no number import | -| Small and simple | Vogent | REST import; agent binding is one dashboard click | +| Small and simple | Vogent | REST import and agent linking; warm transfer type is one dashboard setting | | Developer owns the brain, minimal stack | OpenAI Realtime or xAI | Webhook backend required, not no-code | | Any STT/LLM/TTS combination (Claude as the brain), open source | LiveKit | REST script creates trunk + dispatch rule; the worker is code | | Enterprise procurement in play | Bland, Synthflow, Regal | Sales-gated; never promise same-day | @@ -47,7 +47,8 @@ created by the user in the DIDWW panel. preferred, media encryption matched to the platform's setting, `capacity_limit` = the concurrent calls the user expects. Attach the DID. -3. Outbound trunk (panel, only if the agent places calls): +3. Outbound trunk (panel, only if the agent places calls or transfers; + Vogent always, its number import needs the trunk's credentials): - Access first: under Voice, Outbound Trunks a Get Access button means the account is not approved yet. The application needs a business account (company name, VAT/TAX ID) and DIDWW answers within 48 @@ -59,7 +60,9 @@ created by the user in the DIDWW panel. addresses (each pairing skill lists them). Where the platform publishes none, do not invent a range and do not suggest `0.0.0.0/0` (DIDWW advises against open ranges); send the user to - DIDWW support (support@didww.com) for that case. + DIDWW support (support@didww.com) for that case. DIDWW's Vogent + guide allows `0.0.0.0/0` only for temporary testing, at the user's + own risk; `didww-to-vogent` covers it. - Termination: 24 Hour Limit (USD) at its minimum, 50 (the default is 3000; the trunk is disabled when the limit is reached), Capacity limit = expected concurrent calls, usage limit notification on. @@ -85,7 +88,10 @@ ElevenLabs `sip.rtc.elevenlabs.io:5060` (TCP/TLS, digest). Vapi DIDWW's seven signaling IPs as gateways; `max_transfers` 1 for transfers). Retell `sip.retellai.com` (TCP). Ultravox: the account's own SIP domain, read from `GET /api/sip`, address-based -routing. Vogent `sip.vogent.ai`. OpenAI +routing. Vogent `sip.vogent.ai` (UDP 5060, R-URI `{DID}` or `+{DID}`; +inbound trunk Authorization = the outbound trunk's pair; the outbound +trunk is needed even inbound-only; transfers only as Warm Transfer; +DIDWW's guide `doc.didww.com/integrations/vogent/`). OpenAI `sip:{project_id}@sip.api.openai.com;transport=tls` (TLS+SRTP mandatory, webhook backend is the gate). xAI `sip:{number}@sip.voice.x.ai;transport=tls` (digest via number @@ -102,9 +108,13 @@ Retell `18.98.16.120/30`, `3.42.144.0/23`, `153.57.128.0/18` (plus `143.223.88.0/21`, `161.115.160.0/19` for some US traffic). LiveKit Cloud (Canada, EU, India, Japan, US) `143.223.88.0/21`, `161.115.160.0/19`, `153.57.128.0/18`; self-hosted: its public IP. -ElevenLabs, Ultravox, Vogent: no published range outside enterprise -plans (ElevenLabs Enterprise static SIP is a /24 from their support); -ask DIDWW support. OpenAI and xAI: no outbound trunk needed. +ElevenLabs, Ultravox: no published range outside enterprise plans +(ElevenLabs Enterprise static SIP is a /24 from their support); ask +DIDWW support. Vogent: its enterprise ranges plus DIDWW's seven +inbound signaling IPs (DIDWW's Vogent guide); without enterprise +ranges ask DIDWW support. Its warm transfers dial from its own servers; +`0.0.0.0/0` only for temporary testing, never `0.0.0.0/32` (matches +nothing). OpenAI and xAI: no outbound trunk needed. ## Panels, keys and money @@ -142,4 +152,4 @@ the companion. Built around [DIDWW](https://www.didww.com/?ref=didww-connect), a licensed global operator providing the numbers and two-way SIP trunking under all of the above; platform details verified August 2026, DIDWW -outbound trunk rules October 2026. +outbound trunk rules and Vogent transfers October 2026. diff --git a/skills/didww-connect/package.json b/skills/didww-connect/package.json index eb4c118..da664fa 100644 --- a/skills/didww-connect/package.json +++ b/skills/didww-connect/package.json @@ -1,6 +1,6 @@ { "name": "didww-connect", - "version": "1.0.2", + "version": "1.0.3", "description": "Connect a DIDWW phone number to ANY AI voice platform: helps choose the platform, then wires the SIP trunk pair and number.", "bin": { "didww-connect": "bin/install.js" diff --git a/skills/didww-connect/references/panels-and-billing.md b/skills/didww-connect/references/panels-and-billing.md index 1293ea9..bc054c1 100644 --- a/skills/didww-connect/references/panels-and-billing.md +++ b/skills/didww-connect/references/panels-and-billing.md @@ -79,7 +79,10 @@ keys where a column says so. ## Vogent -- Panel `app.vogent.ai`. Keys: API Keys page in the sidebar. No MCP. +- Panel `app.vogent.ai`. Keys: API tab in the sidebar, Keys, New Key; + copy the secret (the UUID in the key list is the key's ID and gets + "unauthorized"). No MCP. Dial History shows each call's transcript; + `GET /api/dials/{id}` returns it, but the API cannot list dials. - Money: per-minute, around 0.09 USD/min per their marketing, but there is NO public pricing page; verify actual rates inside the panel before the user scales anything. diff --git a/skills/didww-to-vogent/SKILL.md b/skills/didww-to-vogent/SKILL.md index ac111d5..1b009e5 100644 --- a/skills/didww-to-vogent/SKILL.md +++ b/skills/didww-to-vogent/SKILL.md @@ -4,74 +4,196 @@ description: > Connect a DIDWW phone number to a Vogent voice agent over SIP import. Use whenever the user wants a Vogent agent on a real phone number via DIDWW, mentions pairing DIDWW with Vogent, sip_import numbers, - sip.vogent.ai, or debugging calls between the two. + sip.vogent.ai, warm transfers from a Vogent agent to a live person, or + debugging calls between the two. --- # DIDWW to Vogent -A clean self-serve pairing with one honest manual step. Vogent has no -official MCP; its API (Bearer key from the app's API Keys page) imports -the number in one call via `scripts/connect.py`, and the DIDWW MCP -(OAuth) automates the number and the inbound trunk. The DIDWW outbound -trunk is a second manual step: the MCP has no outbound trunk tools, so -the user creates it in the DIDWW panel. The agent-to-number binding is the -gap: Vogent documents it only in the dashboard (Numbers tab under the -Agent), so the skill imports automatically and then walks the user -through that single click. +A clean self-serve pairing that follows DIDWW's Vogent integration guide +(`doc.didww.com/integrations/vogent/`; section numbers below are the +guide's). Vogent has no official MCP; its REST API (Bearer key) imports +the number and links the agent with `scripts/connect.py` and creates +transfer functions, while the DIDWW MCP (OAuth) automates the number and +the inbound trunk. The DIDWW outbound trunk is the manual step: the MCP +has no outbound trunk tools, so the user creates it in the DIDWW panel, +and Vogent cannot import the number without its credentials. + +Live-agent transfers with DIDWW work ONLY as **Warm Transfer**: Vogent +dials the destination itself through the DIDWW outbound trunk. Cold +Transfer (SIP REFER) never reaches DIDWW. Verified on real calls, +October 2026. ## The flow 1. **Confirm in one message:** number (or country to buy), which agent, transfer target if any. -2. **DIDWW inbound trunk (MCP):** host `sip.vogent.ai`, digest auth - with an invented username/password pair, `capacity_limit` = expected - concurrent calls, DID attached. - **DIDWW outbound trunk (panel, the user does it):** step 3's import - needs its credentials. Under Voice, Outbound Trunks a Get Access button means the account is - not approved yet (business account with company name and VAT/TAX ID; - DIDWW answers within 48 business hours): say so and finish the - inbound side meanwhile. Create New, - Authentication method Credentials & IP-Based, which needs at least - one Allowed SIP IP. Vogent has no static SIP egress range outside its - enterprise plan: an enterprise customer gets the range from Vogent, - anyone else asks DIDWW support (support@didww.com); never invent a - range or suggest `0.0.0.0/0`. - Termination: 24 Hour Limit (USD) at its minimum, 50 (default 3000), - Capacity limit = expected concurrency, usage limit notification on; - CLI limited to the DID; Destination dialing Reject All except the - countries the agent calls. Enable the trunk if it shows Disabled. - Host: the region-nearest of `nyc.us.out.didww.com`, - `lac.us.out.didww.com`, `mia.us.out.didww.com`, - `fra.eu.out.didww.com`, `ams.eu.out.didww.com`, `sg.out.didww.com`, - `hk.out.didww.com`. The user hands the generated credentials over in - a file or environment variables, not pasted into chat. -3. **Vogent side (`scripts/connect.py`):** one call to - `POST /phone_numbers` with `type: "sip_import"` carrying the E.164 - number, `terminationUri` (the DIDWW outbound host) and the step 2 - credentials. -4. **Bind the agent (dashboard, one step):** open the agent, Numbers tab, - attach the imported number. Tell the user exactly this and wait for - their done. -5. **Verify:** inbound from a mobile, outbound caller ID, transfer. +2. **DIDWW inbound trunk (MCP; guide 1, Route Incoming Calls to + Vogent):** host `sip.vogent.ai`, port 5060, `transport_protocol` UDP + (the default), `username` (User Part of R-URI) the default `{DID}`: + Vogent accepts the called number with or without the `+`, so `+{DID}` + also works. `capacity_limit` = expected concurrent calls. Attach the + DID. +3. **DIDWW outbound trunk (panel, the user does it; guide 2, Enable + Outbound Calling from Vogent through DIDWW), REQUIRED even for an + inbound-only line:** Vogent imports a number over SIP only with an + outbound trunk's termination address and credentials. Under Voice, + Outbound Trunks a Get Access button means the account is not approved + yet (business account with company name and VAT/TAX ID; DIDWW answers + within 48 business hours): say so and finish the DIDWW inbound trunk + meanwhile. Create New, Friendly name (e.g. `Vogent`), Authentication + method Credentials & IP-based. Allowed SIP IP addresses: the outbound + SIP source ranges Vogent provides for an enterprise account, together + with DIDWW's seven inbound signaling IPs (the guide's list: + `46.19.209.14`, `46.19.210.14`, `46.19.212.14`, `46.19.213.14`, + `46.19.214.14`, `46.19.215.14`, `185.238.173.14` and + `184.195.128.0/21`). Vogent publishes no static range + outside its enterprise plan: without one, send the user to DIDWW + support (support@didww.com) to confirm a supported authentication + setup. `0.0.0.0/0` is for temporary testing only, at the user's own + risk: DIDWW does not recommend it, the digest credentials are then the + only protection, so the spend limits below matter (the October 2026 + warm transfer tests ran this way). Never `0.0.0.0/32`: it matches + nothing and silently blocks every call. Termination: 24 Hour Limit + (USD) at its minimum, 50 (default 3000), Capacity limit = expected + concurrency, usage limit notification on; Destination dialing Reject + All except the countries the agent calls or transfers to. CLI: the + verified setup used On CLI mismatch "Send Original CLI" with Allow any + DID(s); a stricter CLI rule is untested with transfers. Enable the + trunk if it shows Disabled. Host: the region-nearest of + `nyc.us.out.didww.com`, `lac.us.out.didww.com`, + `mia.us.out.didww.com`, `fra.eu.out.didww.com`, + `ams.eu.out.didww.com`, `sg.out.didww.com`, `hk.out.didww.com`. The + user copies the credentials (key icon in the Credentials column) into + a file or environment variables, not into chat; read them inside + scripts and never print them. +4. **Add the outbound credentials to the inbound trunk (panel; guide 2, + Step 4):** Inbound Trunks, the Vogent trunk, Edit, Authorization, + Enable Authorization, Auth User and Auth Password = the outbound + trunk's username and password. Never an invented pair. The user + pastes them so the password stays out of the chat; the MCP + (`auth_enabled`, `auth_user`, `auth_password`) only if the user + accepts it passing through the chat. +5. **Import the number (`scripts/connect.py`; guide 3, Step 1):** one + call to `POST /api/phone_numbers` with `type: "sip_import"` carrying + the E.164 number with `+`, `terminationUri` = a DIDWW outbound + signaling endpoint near where calls are placed, as a hostname only + (no `sip:` prefix, no port), and the outbound trunk credentials. + Dashboard alternative: Settings, Call Settings, Numbers, Add Phone + Number, Import via SIP (country code plus the national part, not the + country code twice). +6. **Link the number to the agent (guide 3, Step 2):** `PUT + /api/phone_numbers/{id}` with `{"agentId": ""}` (the ID + under the agent's name; `GET /api/agents` lists them). + `connect.py --agent ` does it right after the import; + `connect.py --phone-id --agent ` links a number imported + earlier. Dashboard: Agents, the agent, Numbers, Link Number. +7. **Opening line (guide 3, Step 3):** without one the agent waits for + the caller to speak first, which callers hear as silence. Standard + agents: Config tab, Opening Line, Inbound calls Only, with a greeting + (API: `openingLine` on the agent). Flow agents keep the greeting in + the flow (API: the versioned prompt's `flowDefinition.openingLineType` + `INBOUND_ONLY` via `PUT /api/agents/{id}/versioned_prompts/{vpId}`, + sending the whole flowDefinition back with only that field changed; + back up the prompt first). +8. **Call transfers, if a target was given:** "Live-agent transfers" + below (guide 3, Step 4). +9. **Test (guide 3, Step 5):** inbound: the agent answers with its + opening line and Dial History shows the call as Incoming. Outbound: + the agent's Make Call, Phone Call, From Number = the DIDWW number, or + `POST /api/dials` with `callAgentId`, `fromNumberId` (the imported + number's id) and `toNumber` in E.164; check the caller ID. Transfer: + the destination rings and hears the briefing, then both sides hear + each other; Dial History shows Transferred and `transfer_call` in the + Transcript tab (the status alone does not prove the recipient + answered); DIDWW's Outbound Call Logs show the leg on the Vogent + outbound trunk. + +## Live-agent transfers + +Vogent offers Cold and Warm Transfer. With DIDWW only Warm works. + +- **Cold Transfer does NOT work with DIDWW.** Vogent hands the call off + with a SIP REFER whose target it builds from the phone number alone + (no SIP URI setting, unlike ElevenLabs), marks the dial + `TRANSFERRED`, and DIDWW never places the call: no outbound CDR, the + caller waits in silence. Verified twice, also with `0.0.0.0/0` open. +- **Warm Transfer works.** Vogent dials the destination itself through + the number's `terminationUri` (the DIDWW outbound trunk) with the + imported credentials, from its own servers, briefs the person who + answers, then connects the caller. So the outbound trunk's Allowed SIP + IPs must accept Vogent's servers (step 3). A warm transfer is an + additional outbound call through DIDWW and both legs stay up, so + Vogent's per-minute rate and the DIDWW outbound call both run for the + whole transferred conversation. + +Setup (guide 3, Step 4: Add Call Transfers to Your Live Agent): +1. **Create the Call Transfer function:** `POST /api/functions` with + `{"type": "transfer", "name": "transfer_call", "description": "...", + "allowedNumbers": [{"number": "+E164"}], "lifecycleMessages": + {"started": ["Sure, let me connect you to a colleague, one moment + please."]}}`. Dashboard: Functions, New Function, Display Name, Name, + Description, Function Type Call Transfer. Allowed Numbers holds the + destination with Allow transfer to any number off. Never a DIDWW + number linked to the agent: the transferred call comes straight back + to the agent. +2. **Set Transfer Type to Warm Transfer in the Vogent dashboard** + (Functions, the function, Config tab): the API does not expose the + transfer type and new functions default to Cold. Warm Transfer Type + Prompted needs a Prompt, for example: "You are briefing a human + colleague who has just answered the phone. In one short sentence, + say that a caller needs help and summarise what they asked for, then + say you are connecting them now." Switching to Cold and back clears + the Prompt; re-enter it before saving. +3. **Add the function to the agent:** `PUT /api/agents/{id}` with + `linkedFunctionDefinitionInputs: [{"functionDefinitionId": ""}]` + (send every function the agent should keep). Dashboard: the agent's + Config tab, Functions. +4. **Tell the agent when to transfer:** Standard agents: Model tab, + Prompt, a line naming `transfer_call` and the destination, then + Update. Flow agents: a function call node in the flow. ## Failure map -- Inbound rejected: digest pair mismatch between the DIDWW trunk and what - Vogent expects for the imported number. -- Import accepted but calls dead: check the terminationUri has no `sip:` - prefix and is the region-nearest DIDWW host. -- Outbound 403 or rejected from DIDWW: source not in the outbound - trunk's Allowed SIP IPs, caller ID not allowed in its CLI settings, - destination not allowed, or the 24 Hour Limit was reached (DIDWW then - disables the trunk; re-enable it in the panel). -- Agent not answering: the dashboard binding step was skipped; it is not - in the API response. +- Call connects but nobody speaks: no opening line (step 7). +- Agent not answering at all: the number is not linked to the agent + (step 6), the inbound trunk's User Part of R-URI is not `{DID}` or + `+{DID}`, the number is not imported in E.164 with the `+`, or the + inbound trunk is not assigned to the DID. +- Number import fails: `terminationUri` is not a bare DIDWW outbound + host (no `sip:`, no port), or the username and password are not the + outbound trunk's. +- Dial History says Transferred but the destination does not ring: + Transfer Type is not Warm (a REFER-based cold transfer leaves no + outbound CDR at DIDWW), the outbound trunk's Allowed SIP IPs do not + accept Vogent's servers (or hold `0.0.0.0/32`), or the destination is + not in the function's Allowed Numbers. +- 401, 403 or 407 on an outbound call or transfer: the imported number's + credentials differ from the outbound trunk's, the inbound trunk's + Authorization is off or carries another pair (step 4), or the Allowed + SIP IPs do not accept Vogent's servers. +- 484 Address Incomplete on an outbound call: dial the full E.164 + number (`+...`), without `0`, `00` or `011` prefixes. +- Outbound call or transfer rejected after authentication: caller ID not + allowed in CLI Settings, destination not allowed, the 24 Hour Limit or + Capacity limit reached (the 24-hour limit disables the trunk; + re-enable it in the panel), or the DIDWW balance is too low. +- Caller shown in Vogent with an extra `+1` (a call from `+442079460000` + appears as `+1 442079460000`): DIDWW sends the caller ID without `+` + and Vogent assumes a US number. Calls connect normally; only the + number shown in Vogent is affected. Possible fix, untested: + `cli_prefix` `+` on the DIDWW inbound trunk. +- API returns "unauthorized": the saved value is the key's ID (a UUID), + not the secret; create a new key and copy the secret. ## Panel map and money -Vogent keys: `app.vogent.ai`, API Keys page in the sidebar. Pricing has -NO public page; marketing says around 0.09 USD/min, so verify the real -rate inside the panel before the user scales. DIDWW side: prepaid; +Vogent keys: `app.vogent.ai`, API, Keys, New Key; copy the secret key +when shown. Dial History shows each call's transcript, function calls +and completion reason; `GET /api/dials/{id}` returns the same, but the +API cannot list dials, so take the id from the dashboard URL. Pricing +has NO public page; marketing says around 0.09 USD/min, so verify the +real rate inside the panel before the user scales. DIDWW side: prepaid; top-up, Auto-charge and Low Balance Notification (OFF by default) in Billing, Payment Methods. @@ -79,4 +201,6 @@ Billing, Payment Methods. Built around [DIDWW](https://www.didww.com/?ref=didww-to-vogent) SIP trunking and Vogent's number API; verified August 2026, DIDWW outbound -trunk rules October 2026. +trunk rules October 2026; inbound, agent linking, opening line and warm +transfer re-verified on real calls October 2026, and aligned with +DIDWW's Vogent integration guide November 2026. diff --git a/skills/didww-to-vogent/package.json b/skills/didww-to-vogent/package.json index 76e358e..899e232 100644 --- a/skills/didww-to-vogent/package.json +++ b/skills/didww-to-vogent/package.json @@ -1,6 +1,6 @@ { "name": "didww-to-vogent", - "version": "1.0.2", + "version": "1.0.3", "description": "Connect a DIDWW phone number to a Vogent voice agent over SIP import.", "bin": { "didww-to-vogent": "bin/install.js" diff --git a/skills/didww-to-vogent/scripts/connect.py b/skills/didww-to-vogent/scripts/connect.py index 4ebc1f6..c489bbd 100644 --- a/skills/didww-to-vogent/scripts/connect.py +++ b/skills/didww-to-vogent/scripts/connect.py @@ -1,18 +1,29 @@ #!/usr/bin/env python3 -"""Import a DIDWW number into Vogent via sip_import. +"""Import a DIDWW number into Vogent via sip_import and link the agent. Usage: VOGENT_API_KEY=... python3 scripts/connect.py \ --number +33144556677 --didww-host fra.eu.out.didww.com \ - --didww-user USER --didww-pass PASS + --didww-user USER --didww-pass PASS [--agent AGENT_ID] + VOGENT_API_KEY=... python3 scripts/connect.py --phone-id ID --agent AGENT_ID -All four values are required (Vogent's CreatePhoneNumberSIPImportInput -marks username and password required, non-null). DIDWW_TRUNK_PASS in the +The four import values are required (Vogent's +CreatePhoneNumberSIPImportInput marks username and password required, +non-null): the DIDWW outbound trunk's host and credentials, so that +trunk must exist even for an inbound-only line. DIDWW_TRUNK_PASS in the environment may replace --didww-pass to keep it out of shell history. +--agent links the agent with PUT /api/phone_numbers/{id} {"agentId"} +right after the import; --phone-id with --agent links a number imported +earlier, without importing. GET /api/agents lists agent ids. Steps per +DIDWW's Vogent guide (doc.didww.com/integrations/vogent/), section 3. + +VOGENT_API_KEY is the secret key (app.vogent.ai, API, Keys, New Key, +shown once), not the UUID-shaped key ID, which gets "unauthorized". """ import json, os, re, sys, urllib.error, urllib.request -VALUE = {"--number", "--didww-host", "--didww-user", "--didww-pass"} +VALUE = {"--number", "--didww-host", "--didww-user", "--didww-pass", + "--agent", "--phone-id"} a, opts, i = sys.argv[1:], {}, 0 while i < len(a): if a[i] in ("-h", "--help"): @@ -24,7 +35,34 @@ KEY = os.environ.get("VOGENT_API_KEY", "") if not KEY: - sys.exit("Set VOGENT_API_KEY (app.vogent.ai: API Keys).") + sys.exit("Set VOGENT_API_KEY to the secret key (app.vogent.ai: API, Keys, New Key).") +API = "https://api.vogent.ai/api/phone_numbers" +HEADERS = {"Authorization": "Bearer " + KEY, "Content-Type": "application/json"} +agent, phone_id = opts.get("--agent"), opts.get("--phone-id") + +def call(url, method, body): + req = urllib.request.Request(url, method=method, + data=json.dumps(body).encode(), headers=HEADERS) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + +def bind(pid): + """Link the number to the agent; the agent's Numbers tab, Link Number does the same.""" + try: + call(f"{API}/{pid}", "PUT", {"agentId": agent}) + except urllib.error.HTTPError as e: + sys.exit(f"Vogent API {e.code} linking phone number {pid} to agent {agent}:\n{e.read().decode()[:2000]}") + print(f"Phone number {pid} linked to agent {agent}.") + +if phone_id is not None: + if not agent or set(opts) != {"--phone-id", "--agent"}: + sys.exit("--phone-id links an imported number: give it with --agent only.\n" + __doc__) + if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", phone_id): + sys.exit("--phone-id takes the id the import returned (letters, digits, _ and -).") + bind(phone_id) + print("Set an opening line (Inbound calls Only) or the agent waits for the caller; then call the number.") + sys.exit(0) + number = opts.get("--number", "") password = opts.get("--didww-pass") or os.environ.get("DIDWW_TRUNK_PASS", "") if (not re.fullmatch(r"\+[1-9]\d{6,14}", number) or not opts.get("--didww-host") @@ -44,17 +82,20 @@ def redact(v): return {k: "***" if "password" in k.lower() else redact(x) for k, x in v.items()} return [redact(x) for x in v] if isinstance(v, list) else v -req = urllib.request.Request( - "https://api.vogent.ai/api/phone_numbers", method="POST", - data=json.dumps(body).encode(), - headers={"Authorization": "Bearer " + KEY, - "Content-Type": "application/json"}) try: - with urllib.request.urlopen(req, timeout=30) as r: - out = json.load(r) + out = call(API, "POST", body) except urllib.error.HTTPError as e: sys.exit(f"Vogent API {e.code}:\n{e.read().decode()[:2000]}") print(json.dumps(redact(out), indent=1)) -print("\nDIDWW inbound trunk host: sip.vogent.ai (digest auth).") -print("MANUAL STEP: bind the agent in the dashboard " - "(agent, Numbers tab, attach this number), then test both directions.") +pid = str(out.get("id") or "") +print("\nDIDWW inbound trunk: host sip.vogent.ai, UDP 5060, User Part of R-URI {DID} " + "(or +{DID}); Authorization = this outbound trunk's username and password.") +if agent and re.fullmatch(r"[A-Za-z0-9_-]{1,64}", pid): + bind(pid) +elif agent: + sys.exit(f"Imported, but the response has no usable id ({pid!r}); link the number " + "in the dashboard (agent, Numbers, Link Number).") +else: + print(f"Next: link the agent with --phone-id {pid or ''} --agent , " + "or in the dashboard (agent, Numbers, Link Number).") +print("Set an opening line (Inbound calls Only) or the agent waits for the caller; then call the number.") diff --git a/skills/didww-to-vogent/tests/test_connect.py b/skills/didww-to-vogent/tests/test_connect.py index 7180e63..4a069fc 100644 --- a/skills/didww-to-vogent/tests/test_connect.py +++ b/skills/didww-to-vogent/tests/test_connect.py @@ -6,7 +6,7 @@ results = [] def check(name, ok, detail=""): results.append(bool(ok)); print(f"{'PASS' if ok else 'FAIL'} {name} {detail}") -def run_script(argv, env, canned, fail=None): +def run_script(argv, env, canned, fail=None, fail_method=None): """Run the script with stubbed HTTP; return (captured requests, output, exit code).""" reqs = [] class R: @@ -16,7 +16,7 @@ def __enter__(s): return s def __exit__(s, *a): pass def fake_urlopen(req, timeout=None): reqs.append({"url": req.full_url, "method": req.get_method(), "headers": {k.lower(): v for k, v in req.header_items()}, "body": json.loads(req.data) if req.data else None}) - if fail: raise urllib.error.HTTPError(req.full_url, fail, "err", {}, io.BytesIO(b'{"error":"stubbed failure"}')) + if fail and fail_method in (None, req.get_method()): raise urllib.error.HTTPError(req.full_url, fail, "err", {}, io.BytesIO(b'{"error":"stubbed failure"}')) return R(canned) old_env, old_argv, old_open = dict(os.environ), sys.argv, urllib.request.urlopen os.environ.update(env); sys.argv = [P] + argv; urllib.request.urlopen = fake_urlopen @@ -35,7 +35,18 @@ def fake_urlopen(req, timeout=None): b = reqs[0]["body"] if reqs else {} check("one POST to /api/phone_numbers with a Bearer key", len(reqs) == 1 and reqs[0]["url"].endswith("/api/phone_numbers") and reqs[0]["headers"].get("authorization") == "Bearer k", [r["url"] for r in reqs]) check("type sip_import with sipImport{phoneNumber, terminationUri, username, password}", b.get("type") == "sip_import" and b.get("sipImport") == {"phoneNumber": "+33144556677", "terminationUri": "fra.eu.out.didww.com", "username": "u", "password": "p"}, json.dumps(b)) -check("tells the user about the manual agent-binding step and sip.vogent.ai", "MANUAL STEP" in out and "sip.vogent.ai" in out) +check("prints the DIDWW inbound trunk (sip.vogent.ai, {DID}, outbound pair as Authorization) and how to link the agent", "sip.vogent.ai" in out and "{DID}" in out and "Authorization" in out and "--phone-id pn_1 --agent" in out and "MANUAL STEP" not in out, out[-200:]) +reqs, out, code = run_script(full + ["--agent", "ag_1"], {"VOGENT_API_KEY": "k"}, {"id": "pn_1"}) +check("--agent: POST import, then PUT /api/phone_numbers/{id} {agentId} (linking is not dashboard-only)", code == 0 and [r["method"] for r in reqs] == ["POST", "PUT"] and reqs[1]["url"].endswith("/api/phone_numbers/pn_1") and reqs[1]["body"] == {"agentId": "ag_1"} and reqs[1]["headers"].get("authorization") == "Bearer k", [(r["method"], r["url"]) for r in reqs]) +reqs, out, code = run_script(full + ["--agent", "ag_1"], {"VOGENT_API_KEY": "k"}, {"id": "pn_1"}, fail=404, fail_method="PUT") +check("linking failure after a good import exits nonzero and names the step", code != 0 and len(reqs) == 2 and "linking phone number" in out, out[-150:]) +reqs, out, code = run_script(full + ["--agent", "ag_1"], {"VOGENT_API_KEY": "k"}, {"status": "ok"}) +check("import response without an id: no PUT guessed, exits nonzero", code != 0 and [r["method"] for r in reqs] == ["POST"]) +reqs, out, code = run_script(["--phone-id", "pn_9", "--agent", "ag_1"], {"VOGENT_API_KEY": "k"}, {"id": "pn_9"}) +check("--phone-id + --agent links an imported number: one PUT, no import", code == 0 and [(r["method"], r["url"].rsplit("/", 1)[-1]) for r in reqs] == [("PUT", "pn_9")] and reqs[0]["body"] == {"agentId": "ag_1"}) +for argv in (["--phone-id", "pn_9"], ["--phone-id", "pn_9", "--agent", "ag_1", "--number", "+33144556677"], ["--phone-id", "../agents", "--agent", "ag_1"]): + reqs, out, code = run_script(argv, {"VOGENT_API_KEY": "k"}, {}) + check(f"refused before any API call: {' '.join(argv)}", code != 0 and not reqs) reqs, out, code = run_script(["--number", "+33144556677"], {"VOGENT_API_KEY": "k"}, {}) check("missing --didww-host prints usage, no API call", code != 0 and not reqs and "Usage" in out) reqs, out, code = run_script(full[:6], {"VOGENT_API_KEY": "k"}, {}) @@ -44,7 +55,7 @@ def fake_urlopen(req, timeout=None): check("password can come from DIDWW_TRUNK_PASS", code == 0 and reqs and reqs[0]["body"]["sipImport"]["password"] == "envp") reqs, out, code = run_script(full, {"VOGENT_API_KEY": "k"}, {"id": "pn_3", "sipImport": {"password": "SECRETPW"}}) check("echoed response redacts the password", "SECRETPW" not in out and "***" in out) -reqs, out, code = run_script(full + ["--agent", "x"], {"VOGENT_API_KEY": "k"}, {}) +reqs, out, code = run_script(full + ["--agnet", "x"], {"VOGENT_API_KEY": "k"}, {}) check("unknown flag refused instead of ignored", code != 0 and not reqs and "Bad argument" in out) reqs, out, code = run_script(full, {"VOGENT_API_KEY": "k"}, {}, fail=500) check("API error exits nonzero with the status, no traceback", code != 0 and "500" in out and "Traceback" not in out, out[-120:])