Webhooks: give webhooks their own top-level section - #575
Conversation
Webhook reference pages were filed under the resource whose OpenAPI tag they inherited, so all ten Calling webhooks sat under Calls. That never matched how the payloads are used: a SWAIG tool webhook fires for voice AI, Amazon Bedrock, sidecar agents, and — with the AI chat API — text conversations, all with the same body. Filing one shared payload under a single channel made every other caller link sideways into Calls. Webhooks now group by channel, in the nav and on disk: AI 8 spans the channels, which is the reason for the split Calling 3 inbound call webhook, transcript status, stream status Messaging 3 inbound message webhook, message status, 10DLC status Source moves to specs/signalwire-rest/webhooks/, a peer of the service dirs, one directory per channel with its own tag. This replaces the four scattered models/webhooks.tsp files; the 2000-line calling one becomes five topical files under ai/, with common.tsp holding the four models that several payloads share. Directory, tag, and nav section now carry the same name. The channel sections are skip-slug, so each webhook keeps one flat URL at /docs/apis/rest/webhooks/<name> that encodes no channel it might outgrow. Payload bodies are emitted inline, so the schema content is untouched: the only changes to the generated spec are the per-webhook tag, three new tag entries, and a consistent namespace rename of the 32 webhook schemas (Calling.X -> Webhooks.AI.X and so on) with every $ref updated to match. The 854 other schemas and all paths are byte-identical. Two pages referenced those schemas by name through <Schema type=...>, which binds by schema name rather than operationId; both are repointed. Redirects cover all 18 published URLs one to one — the 14 pages plus the four per-resource section URLs, which answered with a 307 to their first child and lose that landing along with the section. Each of those four points at the new location of the page it lands on today. Verified: yarn build:specs green, fern check reports no broken links (which is what proves the subpackage names resolve — fern check does not validate them directly), fern docs md check 2806/2806.
The section carried no URL of its own to change — its slug is explicit and the channel sections are skip-slug — so this is ordering only. It also puts the group list back in alphabetical order, matching the api's alphabetized setting.
ReviewIn progress! PlanAsked Claude to plan this review since the diff is quite large. Click to expand full review planContext The branch (2 commits on top of origin/main, ~49 files) moves every SignalWire REST The PR description makes several strong, checkable claims (schema content untouched, Review axes
Out of scope Compatibility API webhooks (explicitly unchanged), and the .claude/rules / Confirmed with August
Deliverable A written review in the conversation: correctness findings ranked by severity, each with Review —
|
| Claim | How | Result |
|---|---|---|
paths byte-identical |
split openapi.yaml by top-level key, compared old vs new |
identical |
| Webhook schema rename is content-neutral | applied the rename map to all 32 old schema bodies, diffed against the 33 new | 0 mismatches; only net-new is Webhooks.Messaging.TenDlcStatusCallbackPayload |
webhooks: object changed only in tags |
per-key block diff | 14 in, 14 out, every diff is exactly one tags line |
| 3 new tags, none orphaned | tag-name set diff | AI/Calling/Messaging Webhooks added, none removed |
| Generated output matches source | yarn install + yarn build:specs |
clean git status — no drift |
| Nav resolves | yarn fern-check |
0 errors; and I re-ran the falsification myself (aiWebhooks → aiWebhooksXX) → broken link to /docs/apis/rest/webhooks/ai-swaig-tool-webhook |
| The oracle actually covers all 14 | counted inbound links per new URL | every one of the 14 is link-referenced ≥1×, so fern check genuinely proves all three subpackage names, not just one |
| MDX | yarn fern-md-check |
2806/2806 |
| No stale links | repo-wide grep for old URL prefixes and for all 32 old schema names | zero outside docs.yml redirect sources |
| Redirect coverage | prod sitemap (3154 URLs) | sitemap contains exactly the 14 moved pages, nothing more |
| The four section URLs | curl prod |
all four 307, each to precisely the page the redirect targets — the destinations are right, not approximated |
| No chains/duplicates | parsed all 74 redirects | no duplicate sources, no destination that is also a source, no self-redirects |
Two additional checks came back clean. Emptying the SWML Webhook nav entry to contents: [] renders correctly — Subscriber Tokens, Subscriber SIP Credentials, and SWML Scripts are all written that way already. And the PR's aiChat follow-up is correct: #572 does have that bug, and aiWebhooks resolving on this branch is live proof that Fern lowercases an acronym-initial tag whole.
Findings
1. The new AI tag description promises a text surface that doesn't exist yet, and the payload docs say the opposite. specs/signalwire-rest/webhooks/tags.tsp:6 claims the payloads apply to "voice calls, Amazon Bedrock agents, sidecar agents, and text conversations." But AICallInfo requires call_id, and four fields document conversation_type as "Always voice" (ai/models/common.tsp:31, swaig.tsp:265, post-prompt.tsp:88,319). AI chat (#572) isn't merged, so nothing on this branch documents a text surface.
The architectural argument for the split is sound and forward-looking; the description states a capability as shipped. Either soften it — "voice calls, Bedrock agents, and sidecar agents today, grouped here because these payloads aren't specific to one channel" — or land it with #572. The same wording is duplicated at fern/products/apis/apis.yml:246 and specs/signalwire-rest/webhooks/main.tsp.
2. The PR description's schema claim is wrong: 21 non-webhook schemas changed, not zero. "The 854 other schemas and all paths are byte-identical" holds for paths only. Brand, Campaign, Order, SWMLWebhook, SWML.Calling.SMSWithBody, Message.CreateMessageRequest and 15 others changed.
Every change is a correct doc-link update to the new webhook URLs, and their absence would be a bug. The issue is the claim, not the code — a reviewer relies on that sentence to skip a 47k-line diff.
Not verified
missing-redirects needs FDR authentication, as the PR states — my sitemap reproduction is the substitute, and it came out 18/18. Rendered appearance is unverified since I don't start the dev server; worth previewing /docs/apis/rest/webhooks/ai-swaig-tool-webhook, /docs/apis/rest/webhooks/inbound-call-webhook, and /docs/apis/rest/swml-webhook (the now-empty parent).
Verdict: approve after fixing the tag description. The move is clean, and the claim that it's content-neutral holds up under exact checking — the only thing that matters in a refactor this size. Working tree is as I found it.
hey-august
left a comment
There was a problem hiding this comment.
Looks good. #572 is right around the corner.
Why
Webhook reference pages are filed under the resource whose OpenAPI tag they inherited, so all ten Calling webhooks sit under Calls. That never matched how the payloads are used: a SWAIG tool webhook fires for voice AI, Amazon Bedrock, sidecar agents, and — with the AI chat API (#572) — text conversations, all with the same body. Filing one shared payload under a single channel makes every other caller link sideways into Calls.
chat.mdxon that branch already links/docs/apis/rest/calls/webhooks/ai-swaig-tool-webhookfrom a page about text conversations.What changed
Webhooks group by channel now, in the nav and on disk. Directory, tag, and nav section all carry the same name.
AI is its own group because it spans the channels — that is the reason for the split. Everything else sits under the channel it serves, including the SWML document-fetch webhooks (a call fetch is Calling, a message fetch is Messaging).
Source layout — new
specs/signalwire-rest/webhooks/, a peer of the service dirs, one directory per channel with its own tag. Replaces the four scatteredmodels/webhooks.tspfiles; the 2000-line calling one becomes five topical files underai/, withcommon.tspfor the four models several payloads share.URLs — channel sections are
skip-slug, so each webhook keeps one flat URL at/docs/apis/rest/webhooks/<name>encoding no channel it might outgrow.Generated spec: what actually changed
Payload bodies are emitted inline, so schema content is untouched. The only changes to
openapi.yamlare:tagsvalue, and three new tag entriesCalling.X→Webhooks.AI.X, etc.), every$refupdated to matchThe 854 other schemas and all
pathsare byte-identical. One net-new schema appears (Webhooks.Messaging.TenDlcStatusCallbackPayload) — it previously sat in the blockless namespace and wasn't emitted at all, so this makes the set consistent.That rename does have one non-obvious consequence: two pages referenced those schemas through
<Schema type="Calling.…">, which binds by schema name, not operationId — so unlikeWebhookPayloadSnippetit does not survive a namespace move. Both are repointed. I swept all 32 renamed names to confirm there were no others.Redirects
All 18 published URLs, one to one, no wildcards:
/calls/webhooksetc.) → these answer today with a307to their first child, and that landing is generated from the section, so it disappears with it. Each points at the new location of the page it lands on today, so the destination is a real page in the same channel.Enumerated from the production sitemap rather than the repo, since prod is the pre-change URL structure and therefore the authoritative list of what breaks.
Verification
yarn build:specsgreen; generated output in sync with sourcefern check— no broken links. This is what proves thesubpackage_*names resolve:fern checkdoes not validate them directly, and a wrong name silently drops the pages. Confirmed the oracle works by deliberately breaking one ref and watching it reportbroken link to /docs/apis/rest/webhooks/ai-swaig-tool-webhookfern docs md check— 2806/2806Not verified locally:
fern check's ownmissing-redirectsrule is skipped here (needs authenticated FDR access, returns 403). The 18/18 above is that check reproduced by hand against the prod sitemap and the nav's leaf refs. CI runs the real one.Out of scope
Compatibility API's 5 webhooks stay nested per-resource — that product's layout mirrors the docs customers port from.
Follow-up spotted
#572 references
- aIChatfor tag"AI Chat". By the naming rule this repo follows (55/55 tags, e.g."AI Agents: Custom"→aiAgentsCustom), that should beaiChat— as written, the section renders empty andfern checkwon't flag it.