AgentPhone's docs are unusually agent-friendly on the surface — llms.txt, .md versions of every page, an OpenAPI 3.1 spec, a hosted MCP server, even a POST /v1/feedback endpoint built for AI agents to report docs mismatches. Underneath that, the machine-readable spec and the prose guides disagree on core flows, the two SDK pages contradict snippets elsewhere in the docs, and the same "message" object ships two different field casings depending on which endpoint returned it.
1. The "buy a specific number" flow is missing from both the request table and the OpenAPI spec (critical)
Problem: The Phone Numbers guide documents a search-then-buy flow: "Pass the same phoneNumber to POST /v1/numbers" with a full curl example (-d '{"phoneNumber": "+14155550123"}') and a Python retry loop built around it. But the same page's "Create number" request-body table lists only country, areaCode, and agentId — no phoneNumber. The OpenAPI spec agrees with the table: CreateNumberRequest properties are ["country", "areaCode", "agentId", "externalId", "type", "imessageType"]. The field the guide's whole purchase flow depends on does not exist in the machine-readable contract. Meanwhile the spec's externalId, type, and imessageType fields appear nowhere in the guide — even though the Feedback page's own example note queries GET /v1/numbers?externalId=, implying externalId is a real, usable filter.
Consequence: Any consumer that trusts the spec — generated SDKs, AI coding agents, request validators — will reject or strip phoneNumber and silently fall back to buying a random number in the default country. A developer who searched inventory, picked a number, and "bought" it can end up owning a completely different number at $3.00/month. Conversely, developers reading only the table never learn the search-to-purchase flow exists.
The fix: Add phoneNumber to the CreateNumberRequest schema and to the guide's request-body table, and document externalId, type, and imessageType (or remove them from the spec if internal).
2. Sub-Accounts page uses a TypeScript SDK that doesn't match the TypeScript SDK docs (critical)
Problem: The Sub-Accounts page initializes the client as import AgentPhone from 'agentphone'; const client = new AgentPhone({ apiKey: 'sk_live_...' }) and calls client.subAccounts.create(...) and client.numbers.list({ headers: {...} }). The TypeScript SDK page documents a different surface: import { AgentPhoneClient } from "agentphone"; new AgentPhoneClient({ token: "YOUR_API_KEY" }), with methods named listNumbers, createNumber, etc. Default vs named import, apiKey vs token, list vs listNumbers — one of these pages describes an SDK that doesn't exist. Neither SDK page documents a subAccounts/sub_accounts resource or a per-request headers option at all, even though targeting a sub-account requires the X-Sub-Account-Id header. The SDK page's own quick start compounds the confusion by mixing casings in a single call: attachNumberToAgent({ agent_id: agent.id, numberId: number.id }).
Consequence: Copy-pasting the Sub-Accounts TypeScript example into a project using the real agentphone package fails at the import line. Developers (and AI agents, which can't arbitrate between two official pages) can't tell which client constructor or auth option name is correct, and there is no documented SDK path to set X-Sub-Account-Id — the one thing sub-accounts require.
The fix: Rewrite the Sub-Accounts snippets against the actual published SDK (AgentPhoneClient/token), document the sub-accounts resource and per-request header support on both SDK pages (or state plainly that sub-account targeting is curl/raw-HTTP only), and normalize parameter casing in the quick start.
3. Error Handling page's claim about 401 bodies contradicts the live API (significant)
Problem: The page states: "Some responses use a plain {"detail": "..."} body with no error code instead. This includes authentication errors (401/403)...". A live unauthenticated GET /v1/agents returns a body containing both shapes plus more: {"detail":"Authentication required...","error":{"message":"...","code":"HTTP_401","type":"api_error"},"request_id":"..."} — a full error envelope with code HTTP_401, which the docs say is absent.
Consequence: Developers who follow the docs and branch on "plain detail body means auth/throttle class" will misclassify errors; code generated from the documented taxonomy (e.g., "if error.code exists it's a messaging-limit error") takes the wrong branch on every 401. The page's own two-shape dichotomy — the basis for its retry-logic examples — doesn't hold.
The fix: Re-verify each status code against the live API and document the actual union body (detail + envelope + request_id), or fix the API to match the documented shapes.
4. Dead links on the pages that tell you where to go next (significant)
Problem: The Error Handling page's recommended screening step — "Screen numbers with GET /v1/numbers/lookup before sending" — links to https://docs.agentphone.ai/api-reference/numbers/lookup, which returns HTTP 404. The real page is /api-reference/numbers/lookup-numbers-v-1-numbers-lookup-get. The Feedback page's example payload uses "url": "https://docs.agentphone.ai/phone-numbers" as the docs page being reported — that URL also 404s (the guide lives at /documentation/guides/phone-numbers).
Consequence: The lookup link is the one actionable next step in a section about avoiding billed delivery failures; humans hit a 404 and agents following the link index nothing. The Feedback example teaches users to file reports pointing at a URL that doesn't resolve — ironic for an endpoint whose category list includes docs_mismatch.
The fix: Point the lookup link at the real API-reference slug and use a resolvable URL in the Feedback example.
5. The OpenAPI spec carries a second, undocumented API surface for calls and messages (significant)
Problem: The Calls guide's POST /v1/calls table lists six fields; the spec's CreateOutboundCallRequest has eleven — modelTier, variables, callScreeningIdentity, callScreeningPurpose, and disableRecording are never mentioned in the guide. The guide's POST /v1/calls/web table omits metadata, yet the Code Examples page sends metadata: { userId: "usr_123" } in its web-call snippet, and the spec's CreateWebCallRequest includes it. The Messages guide's request table omits the spec's recipients property entirely. The same pattern extends to webhooks: the guide states timeout is "5-120, default: 30" while the spec's WebhookCreateRequest.timeout declares no default, minimum, or maximum.
Consequence: Call screening, per-call recording opt-out, per-call model tier, and multi-recipient sends are real capabilities a developer can only discover by reading the raw JSON spec. Worse, the Code Examples page uses a field (metadata) the canonical guide says nothing about, so readers can't tell whether it's supported, deprecated, or a typo — and spec-driven validators can't enforce the webhook timeout bounds the guide promises.
The fix: Sync every request-body table against the spec (and the spec's constraints against the guides), and document each spec-only field or mark it internal/beta explicitly.
6. Cursor pagination is "ISO timestamps" in the reference and message IDs in the Python SDK example (significant)
Problem: The Pagination page is unambiguous: "before — ISO timestamp — return messages before this time", and its own examples pass oldest_message['receivedAt']. The Python SDK page's example for the same operation passes IDs: client.conversations.get_messages(..., before="msg_older", after="msg_newer") # cursor pagination.
Consequence: One of these silently fails. If the API expects timestamps, the SDK example returns wrong or empty pages (or a 422) for everyone who copies it; if it accepts IDs, the reference is wrong. Pagination bugs are exactly the kind that ship to production unnoticed because the first page always works.
The fix: Pick one cursor type, state it in both places, and make the SDK example pass a value of that type.
7. The same message object has two field schemas — including a leaked from_ Python artifact (significant)
Problem:GET /v1/numbers/{id}/messages returns messages shaped {"id", "from_", "to", "body", ...} — confirmed in both the guide's example JSON and the spec's MessageResponse schema. GET /v1/conversations/{id}/messages returns the same conceptual object shaped {"id", "body", "fromNumber", "toNumber", ...} (MessageInConversation). The trailing underscore in from_ is a leaked Python/Pydantic reserved-word workaround serialized straight into the public API, and the docs reproduce it without a word of warning. The conversation shape also carries ten fields (errorCode, deliveredAt, readAt, reactions, replyTo...) the per-number shape lacks.
Consequence: Developers who write a message parser against one endpoint get undefined sender fields from the other. Agents merging history from both endpoints produce records where half the senders are missing. Nobody expects to type msg["from_"] against a JSON API.
The fix: Unify the message schema (alias from_ → from or fromNumber with a deprecation window), and until then, add an explicit callout in both guides that the two endpoints return different shapes.
8. "Base URL: https://api.agentphone.ai/v1" — except the entire WhatsApp API, which isn't under /v1 (significant)
Problem: Welcome and the API Reference introduction both declare the base URL as https://api.agentphone.ai/v1. The WhatsApp guide's endpoints are all rooted elsewhere: GET /integrations/whatsapp/status, POST /integrations/whatsapp/{connection_id}/templates, etc. — no /v1 segment. The FAQ repeats the un-versioned paths. Nothing anywhere flags that one documented product area lives outside the stated base URL.
Consequence: Any client configured with the documented base URL produces https://api.agentphone.ai/v1/integrations/whatsapp/... and 404s on every WhatsApp call. This hits exactly the developers doing the right thing: parameterizing the base URL instead of hardcoding full endpoint strings.
The fix: Either version the WhatsApp routes, or amend the Base URL sections to state that /integrations/* endpoints are unversioned and show a full URL example.
9. Three docs domains, and pages on one that point at another (minor)
Problem: The docs are served in full at three hosts: docs.agentphone.ai, docs.agentphone.to (byte-identical llms.txt), and agentphone.ai/docs. On docs.agentphone.ai itself, the .md headers of the Calls and Webhooks pages tell agents "For a complete documentation index, see https://docs.agentphone.to/llms.txt" and point the MCP server at docs.agentphone.to — a different domain than the page being read. Support emails alternate the TLD too: founders@agentphone.to on the WhatsApp and Error Handling pages while everything else brands as agentphone.ai.
Consequence: Agents following the in-page index pointer hop domains mid-crawl and index the same corpus twice; developers seeing an .to email on an .ai site reasonably wonder if it's a phishing artifact. Three live mirrors also means cached search results and shared links fragment across hosts.
The fix: Pick one canonical docs host, 301 the others, and make every in-page self-reference (llms.txt, MCP URL, support email) use it.
10. Code Examples' "Voice Webhook Response Reference" documents 3 of the 6 response fields (minor)
Problem: The section presents itself as the reference ("the JSON response controls what the agent says and does. All fields are optional.") but its table lists only text, hangup, and action. The Calls and Webhooks guides document three more: digits (DTMF/IVR navigation), send_message (text the caller mid-call), and interim (NDJSON streaming) — interim being the field every other page calls the single most important latency tool.
Consequence: A developer who lands on Code Examples first — the page explicitly built for copy-paste — builds a voice handler with no streaming and no IVR support, then hears about interim only after shipping callers 30 seconds of silence.
The fix: Make the table match the Calls guide's six-field reference, or replace it with a link to that canonical table.
11. Python SDK usage example reads a field the Usage API doesn't document (minor)
Problem: The Python SDK example prints usage.plan.name from client.usage.get(). The Usage & Billing page's documented GET /v1/usage response contains numbers, stats, periodStart, and periodEnd — no plan object, and the platform elsewhere describes itself as pay-as-you-go with no plans.
Consequence: Copying the SDK example raises an attribute error (or prints nothing), and developers are left guessing whether plans are a real unreleased concept or stale copy.
The fix: Align the SDK example with the documented response, or add plan to the Usage page if it actually exists.
What they do well
First-class agent ergonomics: llms.txt, .md versions of every page, an OpenAPI 3.1 spec, a hosted MCP server, and a POST /v1/feedback endpoint designed for agents to report docs mismatches with request IDs.
Error documentation goes far beyond status codes — an exhaustive failureReason catalog sorted by "stop / retry / wait / fix config / spam", with honest notes about which 429s retrying can and cannot clear.
Operational honesty: carrier filtering, 10DLC, iMessage caps, WhatsApp's 24-hour window, and "search results are not reserved" race conditions are all documented with concrete limits instead of marketing gloss.
Top 3 recommendations
Treat the OpenAPI spec as the contract: add phoneNumber to CreateNumberRequest, document (or remove) every spec-only field, and CI-diff guide tables against the spec.
Regenerate every SDK snippet outside the SDK pages (Sub-Accounts, Pagination, Usage) against the published packages so imports, constructor options, and cursor types actually compile and run.
Consolidate to one canonical docs domain and fix the broken in-docs links (/api-reference/numbers/lookup, the Feedback example URL, the .to index pointers).
34 documentation snippets checked against the live AgentPhone API and published SDK packages: 19 PASS, 11 SKIP, 4 FAIL. Core REST examples (agents CRUD, voices, numbers list/search, webhooks CRUD, usage, conversations, sub-account list, MCP auth behavior) work as written. Failures: the number-lookup example rejects the docs' own sample phone number; the project webhook test endpoint demands an undocumented ?agentId= parameter when more than one agent exists; and both Sub-Accounts SDK snippets crash as written (TypeScript at the import line — the package has no default export — and Python at sub.id because the SDK returns a dict). Billable snippets (number purchase, outbound calls, SMS sends) and partial fragments were skipped; all created test resources (2 agents, 1 webhook, 1 sub-account) were deleted afterward.
Notable behavioral observations from passing runs: GET /v1/usage returns no plan object (the Python SDK example reads usage.plan.name); GET /v1/agents returns {data, total} without the hasMore the Pagination page describes as the list response format; GET /v1/numbers/available?areaCode=415 returned area-code 628 numbers with widened: false, contradicting the documented semantics that substitutes set widened: true; voice entries include an undocumented provider field.
Required credentials
AGENTPHONE_API_KEY (requester-provided; sourced from the credentials file, never printed)
200 with {data, widened}. Behavioral mismatch: requested area code 415, received 628 numbers with widened: false — docs state substitutes should set widened: true
11
GET /v1/numbers?limit=10&offset=0
PASS
200, {data, hasMore, total}
12
GET /v1/numbers/lookup?phoneNumbers=%2B14155551234,%2B15551230000
FAIL
400 VALIDATION_ERROR_PHONENUMBERS: "Invalid phone number: +15551230000". The docs' own example input is rejected; the page claims unresolvable numbers return null fields, not a 400. Diagnosis: fictional 555 example number fails server-side validation. Suggested solution: use valid-format example numbers in the docs and document the 400 validation behavior
13
POST /v1/numbers create (country/areaCode/agentId)
SKIP
Billable side effect ($3.00/month number)
14
Buy-a-found-number curl ({"phoneNumber": ...})
SKIP
Billable side effect; also the field is absent from the OpenAPI spec (see audit finding 1)
200, returns null when unconfigured, as documented
17
GET /v1/webhooks/deliveries?limit=10&offset=0
PASS
200, {items, total, offset, limit} as documented
18
POST /v1/webhooks/test
FAIL
400 {"detail": "Multiple agents found. Pass ?agentId=<agent_id> to choose the test payload agent."} — the docs (here, on the Testing page, and in the Welcome flow) show this call with no parameters returning {"success": true, ...}; the required ?agentId= query parameter is undocumented. Diagnosis: endpoint behavior diverges once an account has more than one agent. Suggested solution: document the agentId parameter and the multi-agent 400
200; keys {numbers, stats, periodStart, periodEnd, reportGeneratedAt} — no plan field (the Python SDK example's usage.plan.name would fail); reportGeneratedAt undocumented
TypeScript create snippet (import AgentPhone from 'agentphone'; new AgentPhone({ apiKey }))
FAIL
SyntaxError: The requested module 'agentphone' does not provide an export named 'default' — fails at the import line against agentphone@1.0.23. Diagnosis: snippet targets a nonexistent SDK surface; the real export is the named AgentPhoneClient taking { token } (matches audit finding 2). Suggested solution: rewrite the snippet against the published SDK
The API call succeeds (a sub-account was created, then deleted in cleanup) but the snippet crashes at print(sub.id) with AttributeError: 'dict' object has no attribute 'id' — the SDK returns a dict, not an object. Diagnosis: docs show attribute access on a dict return. Suggested solution: use sub["id"] in the docs or return a typed object from the SDK
25
Full bash workflow (create sub-account → buy number → create agent)