Hyperspell Documentation Audit
The bones are good — there's an llms.txt, a Stainless-hosted OpenAPI spec, an MCP server, and a Claude Code skill for agent-driven install. But the actual prose and snippets are riddled with copy-paste contamination, broken code examples, and three different casings for the same parameter. An agent pasting these snippets verbatim will not get a working app.
2. Webhook verifier example is broken in every meaningful way (critical)
Location: /usage/webhooks
Problem: This is the canonical security primitive for webhook integrators, and both language examples are full of compile-time errors before you can even reach the cryptography.
Python version (at least four undefined references):
from fastapi import FastAPI, Request
import hashlib
import hmac
def verify_webhook(payload, secret):
hash = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(signagture, hash) # `signagture` is a typo AND never passed in
class WebhookPayload(pydantic.BaseModel): # `pydantic` is never imported
...
app = FastAPI()
@router.post("/webhooks/hyperspell") # `router` is never defined — only `app` is
async def hyperspell_webhook(query: WebhookPayload, request: Request):
signature = request.headers.get("X-Hyperspell-Signature")
if not verify_webhook(query, HYPERSPELL_JWT_SECRET): # global never set or imported
...
Four separate NameErrors: signagture (typo), pydantic (no import), router (only app exists), HYPERSPELL_JWT_SECRET (never sourced from env). Also verify_webhook(query, ...) passes only two arguments, so even if the typo is fixed there is no signature variable available to compare.
TypeScript version is worse still:
function verify_webhook(payload: WebhookPayload, secret: string) {
const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
return hmac.compare_digest(sig, expected);
}
export async function POST(req: Request) {
...
if (!verify_webhook(payload, secret)) return NextResponse.json(...); // payload used before declaration
const payload = JSON.parse(raw.toString("utf8")) as WebhookPayload; // declared after use
}
Three layered bugs: (a) payload is referenced before it's declared; (b) the function signature takes WebhookPayload — a parsed object — and crypto.createHmac().update(payload) will coerce it to the string "[object Object]", so even after fixing the order of declarations the HMAC is computed over garbage and will never match; (c) the hmac.compare_digest call doesn't belong in Node at all.
Consequence: Both examples won't compile, let alone verify a signature. A developer who patches around the surface errors may end up shipping a verifier that HMACs [object Object] and returns false for every legitimate webhook — or, worse, returns true because they swapped in a non-constant-time == comparison. This is the single most consequential snippet in the docs and none of it runs.
The fix: Rewrite both snippets end-to-end. Python: import pydantic, define router (or use app.post), pass signature to verify_webhook as a third argument, fix the typo, and source HYPERSPELL_JWT_SECRET from os.environ. TypeScript: take a Buffer/string raw body (not the parsed object), HMAC the raw bytes, declare payload after verification, and use crypto.timingSafeEqual.
3. Integration pages are copy-paste-contaminated with Notion content (critical)
Location: /integrations/all/reddit, /integrations/all/slack, /integrations/all/google_mail, /integrations/all/google_calendar
Problem: Multiple integration pages still reference the wrong product:
- Reddit: "The Notion integration requires an OAuth connection" and "The Notion integration returns
Documentresources" - Slack: "The Notion integration returns
Conversationresources" - Gmail: "Note that during the OAuth flow, users will be asked to select which pages in their Notion workspace they would like to give access to"
- Google Calendar: "The Web Crawler integration returns
CalendarEventresources" — and the Additional Endpoints heading reads "Index a website before querying it" for what's actuallyGET /integrations/google_calendar/list
The source field documentation on the Reddit page even says: "The provider that fetched the document, which is notion if this integration is used to query or index it." The same template-leak shows up in a recurring typo across every integration page — "is availble in Hyperspell Connect" — reinforcing that nothing here was proofread after duplication.
Consequence: A developer integrating Reddit will not know whether to filter on source == "reddit" or source == "notion". Agents indexing these pages will produce wrong code for every integration except Notion. This is the worst-case for an agent-targeted doc — confidently wrong, structurally identical across pages.
The fix: Audit every integration page for stale Notion/Web Crawler/Slack references — including section headings, not just prose — and substitute the correct source name. Treat the template as a fill-in, not a copy.
4. Three different casings for the user-id SDK parameter (critical)
Location: /core/integration, /usage/permissions
Problem: The TypeScript SDK is initialized with three different field names across the docs:
/core/integration:new Hyperspell({ apiKey: 'API_KEY', userID: 'YOUR_USER_ID' })— userID/usage/permissions(header section):new Hyperspell({ apiKey: 'YOUR_API_KEY', userId: 'user_id' })— userId/usage/permissions(user-token section):client.auth.userToken({ user_id: 'user_id' });;— user_id
Consequence: Only one of these is the real key the SDK accepts. The other two will silently pass an unknown option (TypeScript may infer { userID } as a fine literal type) and the X-As-User header will never get set — every API call runs as the app token instead of the user, returning data the user shouldn't see or no data at all. This is exactly the kind of contradiction agents cannot recover from.
The fix: Pick one casing (Stainless-generated SDKs typically use camelCase, so userId), grep the docs, and replace everywhere. Also remove the doubled ;; on the userToken call.
5. Dashboard hostname flips between hyperspell.ai and hyperspell.com (significant)
Location: /core/integration vs /usage/permissions, /core/quickstart
Problem:
/core/integration: "generate an API Key in your dashboard" — .ai/usage/permissions: "generate an API key in the Dashboard" — .com/core/quickstart: "log into your Hyperspell account at app.hyperspell.com" — .com
Consequence: One of these links 404s or, worse, lands on a parked / typosquatted domain. New developers following the integration page will not find their API key where the docs say to look — and the security implications of pasting credentials into the wrong host depend entirely on what app.hyperspell.ai resolves to today.
The fix: Canonicalize on one TLD and search-replace. If both exist, redirect one to the other and pick one for docs.
6. Private-beta warning contradicts the "no more waitlist" changelog (significant)
Location: /integrations/overview vs /concepts/changelog
Problem: The integrations overview opens with a yellow Warning callout: "Hyperspell is currently in private beta, and not all features may be available to all users. To join the beta, please sign up at hyperspell.com."
But the changelog entry from Feb 24, 2025 (0.5.5) says: "No more waitlist! You can now sign up and start using Hyperspell." A year of changelog entries follow that — 0.30.1 is dated Jan 26 2026.
Consequence: A prospect cannot tell whether they can actually sign up today or need a waitlist invite. The "waitlist" link in the banner is the primary CTA on a page that lists every integration — it may funnel signups into a dead waitlist that no longer exists.
The fix: Remove the private-beta banner, or scope it to specific in-beta integrations (which are already individually flagged with their own Warning callouts — Gmail, Airtable, etc.).
7. Stale /documents endpoint references after migration to /memories (significant)
Location: /integrations/all/slack, /concepts/changelog
Problem: Changelog 0.19.0 (July 18, 2025): "Consolidated our offerings and API endpoints. The old endpoints under /documents are now under /memories."
But the Slack page still documents fields conditional on the old endpoint: "This field is only returned if the resource is returned from the /documents/get endpoint. If the website is returned from the /query endpoint..." — referencing both the deprecated /documents/get and the never-renamed-but-also-stale /query (now /memories/query).
Consequence: Developers reading the Slack resource schema will believe they can call /documents/get to hydrate conversation messages and will get a 404 (or worse, hit a deprecated path that silently changes shape). An agent indexing this page will generate code against an endpoint that no longer exists.
The fix: Search the integration pages for /documents and /query references; rewrite them to /memories/get and /memories/query. Add a redirect from the old paths if any are still live.
8. GET used for an indexing (state-mutating) endpoint (significant)
Location: /integrations/all/web_crawler
Problem: The Additional Endpoints section says:
GET /integrations/web_crawler/indexCall this endpoint to index a website for indexed search. The website will be crawled recursively and added to the search index.
This is a write operation (it triggers crawling and inserts into the index) shaped as a GET.
Consequence: Either (a) the doc is wrong and the actual endpoint is POST — in which case anyone trying it gets 405, or (b) it really is a GET, in which case it's cacheable, retryable by proxies, prefetched by browsers, and a CSRF target. Neither story is good for an agent generating client code.
The fix: Use POST for the indexing trigger; reserve GET for the existing read-only list endpoints like /integrations/google_calendar/list.
9. ParamField and ResponseField components missing names throughout (significant)
Location: /integrations/all/web_crawler, /integrations/all/google_calendar
Problem: Multiple <ParamField> and <ResponseField> components appear with no name attribute or with the name embedded only in surrounding prose:
<ParamField type="string">
The URL of the website to crawl. Trailing slashes are ignored.
</ParamField>
Worse, the Web Crawler index-endpoint parameters block has a fully self-closing, body-less ParamField — no name, no description, nothing to render:
<Expandable title="parameters">
<ParamField type="string" />
...
</Expandable>
The Google Calendar Calendar List Schema has:
<ResponseField name="id" type="calendar[]">
A list of calendars.
</ResponseField>
name="id" but type="calendar[]" — the field name doesn't match the semantics ("id" can't be a list).
Consequence: Rendered output shows untitled "string" / "number" rows in the params table, plus at least one totally blank row, leaving developers to guess what parameter key to actually pass. This is the parameter list — without names, it isn't a parameter list, it's a sentence.
The fix: Add explicit name= attributes to every ParamField/ResponseField, replace the empty self-closing <ParamField type="string" /> with a real declaration, and fix the id: calendar[] mismatch on Google Calendar list response.
10. client = Hyperspell() shown with no arguments on the Reddit page (significant)
Location: /integrations/all/reddit
Problem: The Reddit example initializes the SDK with no arguments:
from hyperspell import Hyperspell
client = Hyperspell()
response = client.memories.search(...)
But every other page (/core/integration, /usage/permissions) shows it requires api_key= and user_id=. The auth docs explicitly say all requests need a Bearer token, and no environment-variable convention (e.g., HYPERSPELL_API_KEY) is documented anywhere on the auth page.
Consequence: Developers copying the Reddit snippet will get a runtime error or an unauthenticated call. Since there's no documented env-var fallback, it's not obvious whether the bare constructor is meant to read env vars or is just a doc error — agents won't know which way to repair it.
The fix: Either document the env-var fallback explicitly on the authentication page (and standardize the bare constructor across integration examples), or always pass api_key= and user_id= in snippets.
11. client vs hyperspell variable mix-up in auth snippet (minor)
Location: /usage/permissions ("Using a User Token" section)
Problem:
client = Hyperspell(api_key="YOUR_APP_TOKEN")
response = hyperspell.auth.user_token(user_id="user_id") # `hyperspell` isn't defined
user_token = response.token
The variable is defined as client but used as hyperspell on the next line.
Consequence: NameError: name 'hyperspell' is not defined. Trivial to fix once spotted, but this is the canonical user-token flow snippet — the one developers will reach for first.
The fix: response = client.auth.user_token(user_id="user_id").
12. Pervasive typos and duplicated words across user-facing copy (minor)
Location: Multiple
Problem:
/usage/permissions: "The/auth/user_tokenendopint is the only endpoint…"/usage/webhooks: variable namedsignagture/concepts/changelog: "Sepember 29, 2025"/advanced/clerk: "left siderbar"/core/quickstart: "configure themlater" (missing space), "You will be be prompted",<Step title="Test your **your** RAG in the sandbox">/integrations/all/slack: "highlights from the conversatoin"/advanced/mcp-overview: "precipice of agentic computing" rendered as "precipise"; "dynamically accesst them"- Recurring on every OAuth integration page (
/reddit,/slack,/google_mail,/google_calendar): "is availble in [Hyperspell Connect]" — the same exact misspelling on each page, confirming the integration template wasn't proofread before duplication
Consequence: Low-severity individually, but the cumulative impression — together with the integration-page copy-paste errors and duplicated-word bugs — is that pages aren't being proofread before publish. For agent parsing, signagture is the only one that breaks code; the rest are cosmetic. The "availble" recurrence in particular reinforces issue #3's template-leak thesis.
The fix: Run a spellcheck pass, plus a duplicated-adjacent-word lint (be be, your your); the variable typo in the webhook example needs to be fixed regardless (see issue #2).
What they do well
- Agent-friendly surface area:
llms.txtis present and lists every doc page with absolute URLs; an OpenAPI spec is hosted at a stable Stainless URL; there's a dedicated "Automatic Integration" page with a Claude Code skill (npx skills add https://docs.hyperspell.com) — this is forward-looking and rare. - MCP server documented end-to-end: tools (
search,add_memory,upload_file,connect_integration, etc.) are listed with clear semantics, and an npm package is referenced. - Security page has substance: at-rest + in-transit encryption, 30-day backups, responsible disclosure contact — actual content, not just a "we care about security" placeholder.
Top 3 recommendations
- Fix the code examples — every TypeScript snippet should be
tsc --noEmit-clean; every Python snippet should at minimum parse and reference variables it defines. The webhook verifier is the most urgent (issue #2): the Python version has four undefined references and the TypeScript version HMACs an object rather than the raw bytes. - De-Notion the integration template — sweep every page under
/integrations/all/and replace stale "Notion" / "Web Crawler" / "Slack" references that leaked from the source template, including section headings on Google Calendar (issue #3). Use the recurringavailbletypo as a marker for which pages were duplicated without review. - Pick one casing for
userId, one TLD for the dashboard, and one canonical endpoint set — agents tolerate spartan docs, but they cannot reconcile contradictions (issues #4, #5, #7).