Skip to content

feat: add vapi-webhooks skill - #178

Merged
garethx merged 3 commits into
hookdeck:mainfrom
garethx:feat/vapi-webhooks
Aug 12, 2026
Merged

feat: add vapi-webhooks skill#178
garethx merged 3 commits into
hookdeck:mainfrom
garethx:feat/vapi-webhooks

Conversation

@garethx

@garethx garethx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a webhook skill for Vapi (voice-AI agent platform — the "Server URL"). Collision check clean: no prior vapi-webhooks skill or providers.yaml entry.

Authored from Vapi's canonical docs (server-url, events, server-authentication). This one needed care rather than a stock HMAC template — Vapi has no fixed signature scheme, so the skill is written to avoid fabricating a construction.

Why this skill is unusual

  • Auth is opt-in and per-endpoint. A Server URL has no auth until you attach a Custom Credential. Four types exist: Bearer Token, legacy X-Vapi-Secret, OAuth 2.0, and a fully-configurable HMAC.
  • Primary path = shared secret (Authorization: Bearer <token> or legacy X-Vapi-Secret) — a literal, timing-safe compare, not an HMAC. The examples implement this and read whichever header is present.
  • HMAC is documented generically, not hardcoded. Vapi lets the customer choose the algorithm, header name, optional timestamp header, and payload format, and pins no defaults — so references/verification.md refuses to assert one. OAuth 2.0 is covered as the token-validation path.
  • No fabricated helper. A verifyVapiSignature name in a CLI tutorial is an unimplemented placeholder — the skill explicitly does not use it. No official SDK verify helper exists; no documented source-IP allowlist.
  • Nested envelope. The event type is at message.type, not top-level (a CLI tutorial shows a flatter shape with names like call-started — that's informal example code, not the wire format; the skill follows /server-url/events).
  • Bidirectional request/response protocol. Four message types require a JSON response body, which the handlers implement: assistant-request (hard ~7.5s timeout), tool-calls, transfer-destination-request, knowledge-base-request. All other types get a bare 200.

Tests

  • Express: 19 passed
  • Next.js: 14 passed
  • FastAPI: 18 passed
  • validate-provider.sh vapi-webhooks: passed

Auth tests cover both the Bearer and legacy X-Vapi-Secret headers, wrong/missing secret → 401, and the required response bodies for all four request/response types.

Notes for the reviewer

  • Not live-verified. Authored from docs, not a captured delivery (no Vapi test account in this run). Residual unknowns — exact HMAC construction (undocumented by design), precise response shapes, and the ~7.5s budget — are recorded in skills/vapi-webhooks/TODO.md for a future live pass.

Left as a draft pending sign-off.

🤖 Generated with Claude Code

garethx and others added 3 commits August 5, 2026 14:06
Vapi voice-AI agent platform ("Server URL" webhooks). Authored from the
canonical docs (server-url, server-url/events, server-url/server-authentication)
because Vapi has no fixed signature scheme and needed careful, non-fabricating
treatment.

Key decisions:
- Auth is opt-in and per-endpoint. The skill's primary, fully-specified path is
  the shared secret (Authorization: Bearer <token> or the legacy X-Vapi-Secret
  header) — a literal, timing-safe compare, not an HMAC. OAuth 2.0 and a
  fully-configurable HMAC are documented as secondary options WITHOUT asserting a
  fixed header/algorithm/signed-string, since Vapi pins no defaults.
- The event type is nested at message.type (per /server-url/events), not the
  flatter top-level shape shown in an informal CLI tutorial. The tutorial's
  `verifyVapiSignature` is a placeholder, not a real export — not used.
- Handlers implement the request/response protocol: assistant-request,
  tool-calls, transfer-destination-request, and knowledge-base-request return the
  required JSON body; all other message types get a bare 200.

Tests: Express 19, Next.js 14, FastAPI 18 pass; validate-provider.sh passes.
Residual unknowns (no live account; HMAC construction undocumented) recorded in
skills/vapi-webhooks/TODO.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hookdeck/core added VAPI as a verified source (HMAC controller). Its config
resolves the skill's biggest hedge — the HMAC signed-content construction:

- Signed content = the RAW request body, no timestamp prefix (VAPI omits the
  generic HMAC alias's prefix_data_with_timestamp, so the controller signs body
  as-is).
- Defaults: algorithm sha256, header x-signature, encoding hex; sha1/sha512 and
  base64/base64url also supported; MD5 deliberately excluded.

Vapi's own docs still pin no defaults (re-confirmed 2026-08), so this is framed
as a concrete DEFAULT from Hookdeck's verified integration — not a Vapi spec —
and the customer's dashboard credential still wins. verification.md now ships a
concrete verifyVapiHmac() with these defaults; SKILL.md and TODO.md updated to
match. No handler/test changes; validate-provider.sh passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…struction

Verified the Vapi HMAC scheme by recomputing digests of real sandbox deliveries
(key "test", SHA-256, hex, secret verbatim, x-signature header):

- Payload Format {body}  -> HMAC-SHA256(rawBody, secret). Matches hookdeck/core;
  recommended (self-contained).
- Payload Format {timestamp}.{body} (Vapi default) -> HMAC-SHA256(
  x-timestamp + "." + rawBody, secret), where x-timestamp is Vapi's send-time
  epoch-ms header. Requires the timestamp header ON; with it off the signing
  value is never delivered and the signature can't be verified. The signing
  timestamp is the x-timestamp header, NOT message.timestamp (differ ~40ms).

verification.md now documents both formats + a format-aware verifier + self-
computed known-answer vectors; SKILL.md HMAC bullet updated; TODO marks HMAC
verified. Added a known-answer test to the Express suite locking both digests.
Express 21 pass; validate-provider.sh passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@garethx
garethx marked this pull request as ready for review August 12, 2026 15:13
@garethx
garethx merged commit b9f4d6b into hookdeck:main Aug 12, 2026
8 checks passed
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.

1 participant