Skip to content

Webhooks: give webhooks their own top-level section - #575

Merged
Devon-White merged 3 commits into
mainfrom
Devon/webhooks-top-level-section
Aug 13, 2026
Merged

Webhooks: give webhooks their own top-level section#575
Devon-White merged 3 commits into
mainfrom
Devon/webhooks-top-level-section

Conversation

@Devon-White

Copy link
Copy Markdown
Collaborator

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.mdx on that branch already links /docs/apis/rest/calls/webhooks/ai-swaig-tool-webhook from 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.

Channel Count Pages
AI 8 SWAIG tool + signature request, post-prompt, debug, Bedrock tool + post-prompt, sidecar callback + sidecar tool
Calling 3 inbound call webhook, transcript status, stream status
Messaging 3 inbound message webhook, message status, 10DLC status

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 scattered models/webhooks.tsp files; the 2000-line calling one becomes five topical files under ai/, with common.tsp for 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.yaml are:

  • the per-webhook tags value, and three new tag entries
  • a consistent namespace rename of the 32 webhook schemas (Calling.XWebhooks.AI.X, etc.), every $ref updated to match

The 854 other schemas and all paths are 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 unlike WebhookPayloadSnippet it 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:

  • 14 pages → their new locations
  • 4 per-resource section URLs (/calls/webhooks etc.) → these answer today with a 307 to 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:specs green; generated output in sync with source
  • fern check — no broken links. This is what proves the subpackage_* names resolve: fern check does not validate them directly, and a wrong name silently drops the pages. Confirmed the oracle works by deliberately breaking one ref and watching it report broken link to /docs/apis/rest/webhooks/ai-swaig-tool-webhook
  • fern docs md check — 2806/2806
  • 18/18 redirect coverage, no chains, no duplicate sources, every destination a real page

Not verified locally: fern check's own missing-redirects rule 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 - aIChat for tag "AI Chat". By the naming rule this repo follows (55/55 tags, e.g. "AI Agents: Custom"aiAgentsCustom), that should be aiChat — as written, the section renders empty and fern check won't flag it.

Devon-White and others added 2 commits August 13, 2026 10:15
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.
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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.
@hey-august
hey-august self-requested a review August 13, 2026 15:04
@Devon-White Devon-White linked an issue Aug 13, 2026 that may be closed by this pull request
@hey-august

hey-august commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Review

In progress!

Plan

Asked Claude to plan this review since the diff is quite large.

Click to expand full review plan

Context

The branch (2 commits on top of origin/main, ~49 files) moves every SignalWire REST
webhook reference page out of its per-resource section into one top-level Webhooks
section grouped by channel (AI / Calling / Messaging). It deletes four scattered
models/webhooks.tsp files and rebuilds them as specs/signalwire-rest/webhooks/,
regenerates fern/apis/signalwire-rest/openapi.yaml, rewrites the nav in
fern/products/apis/apis.yml, adds 18 redirects in fern/docs.yml, and repoints
in-repo links.

The PR description makes several strong, checkable claims (schema content untouched,
paths byte-identical, 18/18 redirect coverage, all <Schema type=> refs repointed).
The review is mostly about verifying those claims mechanically rather than reading
26k lines of diff — the interesting risk in a move-shaped PR is silent loss, not bad logic.

Review axes

  1. Generated spec — is the move really content-neutral?
  • Extract paths: from openapi.yaml on both sides, normalize, diff → expect byte-identical.
  • Extract the components.schemas key set on both sides; apply the claimed rename map
    (Calling.X → Webhooks.AI.X, etc.) and diff the bodies → expect only key renames
    plus the one net-new Webhooks.Messaging.TenDlcStatusCallbackPayload.
  • Diff the webhooks: object: expect only tags changes, no operationId or payload change.
  • Confirm the 3 new tag entries and that no old tag was orphaned (tag referenced but not declared, or declared but unused).
  1. Generated output actually matches source
  • Run yarn build:specs and confirm git status is clean (no drift between the
    committed tsp-output/openapi.yaml and the .tsp source). Per CLAUDE.md this is
    the only sanctioned way to check generated files.
  1. TypeSpec source — nothing dropped in the 2000-line split
  • Model-by-model set comparison: every model/property/doc in the deleted
    calling-api/calls/models/webhooks.tsp, message-api/messages/models/webhooks.tsp,
    and the removed blocks in swml-webhook / campaign-registry must appear in the new
    webhooks/** tree (this is largely implied by axis 1, but catches doc-comment loss
    that OpenAPI diffing would surface only indirectly).
  • Check webhooks/ai/models/common.tsp extraction: the four shared models are genuinely
    identical across their former copies, not silently unified with differing docs/fields.
  • Check the leftover files still compile cleanly with their webhook models removed —
    no now-unused imports, no models left orphaned in calling-api/calls, and confirm the
    core.tsp/requests.tsp edits in swml-webhook and campaign-registry are only
    reference renames, not behavior changes.
  • Conventions check against .claude/rules/spec-conventions.
  1. Nav
  • yarn fern-check.
  • Verify each subpackage_. name resolves: derive expected subpackage names from
    the tag strings via the repo's 55/55 naming rule ("AI Webhooks" → aiWebhooks) and
    cross-check against every operationId in the generated spec. A wrong name silently
    drops the page, and fern check won't say so — so also confirm the leaf count is
    8 + 3 + 3 = 14 and that no webhook operation in the spec is absent from the nav.
  • Confirm skip-slug on the channel sections yields /docs/apis/rest/webhooks/
    and that no two webhooks collide on a flat slug.
  • Check the emptied parents (- calls, swml-webhook: contents: []) still render — an
    empty contents: [] under a referenced-packages section is the kind of thing that
    renders a bare section.
  1. Redirects
  • Re-derive the source list from the production sitemap (fetch it) rather than trusting
    the PR's list; diff against the 18 entries. Specifically look for published URLs the
    PR missed (e.g. any webhook page under a resource section not enumerated).
  • Confirm every destination is a real leaf in the new nav, no chains (destination that is
    itself a source), no duplicate sources, and no conflict with the ~90 pre-existing redirects.
  • Sanity-check the four section-URL redirects actually 307 today as claimed (curl prod).
  1. In-repo links and MDX
  • Grep the whole repo (all products, not just the ones touched) for
    /docs/apis/rest/(calls|messages|campaign-registry|swml-webhook)/webhooks → expect zero.
  • Grep for referencing any of the 32 renamed schema names → expect zero
    stale; confirm the two repointed pages point at names that exist in the new spec.
  • Also check WebhookPayloadSnippet / EndpointRequestSnippet usages that reference the
    moved operations.
  • yarn fern-md-check.
  1. Editorial / judgment (smaller pass)
  • Tag descriptions and the new nav comments against mdx-style tone rules (they are
    customer-facing).
  • Is the AI/Calling/Messaging split defensible at the boundaries — notably the SWML
    document-fetch webhooks landing in Calling/Messaging, and transcribe-status-callback
    being the destination for the old /calls/webhooks landing.
  • The stated follow-up (docs(ai): document the AI chat API #572's - aIChat vs aiChat): confirm the naming rule from the
    existing 55 tags so the claim is right before endorsing it.

Out of scope

Compatibility API webhooks (explicitly unchanged), and the .claude/rules /
reo.js changes that arrive via merges from main.

Confirmed with August

  • May run yarn build:specs — regenerate into the working tree to prove the committed
    output matches source, then report any drift (I'll leave the tree as I found it).
  • May fetch prod — sitemap + the four old section URLs, to derive the redirect source
    list independently.

Deliverable

A written review in the conversation: correctness findings ranked by severity, each with
file:line and a concrete failure scenario, plus a short verdict on the design call.
No edits to the branch, nothing posted to the PR.


Review — Devon/webhooks-top-level-section

I verified the PR's claims mechanically rather than reading 26k lines; the build, both checks, and production were all in play.

Verified

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 (aiWebhooksaiWebhooksXX) → 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 hey-august left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. #572 is right around the corner.

@Devon-White
Devon-White merged commit b3c21ce into main Aug 13, 2026
2 checks passed
@Devon-White
Devon-White deleted the Devon/webhooks-top-level-section branch August 13, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reorganize webhooks in specs

2 participants