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 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)
Problem: Multiple integration pages still reference the wrong product:
Reddit: "The Notion integration requires an OAuth connection" and "The Notion integration returns Document resources"
Slack: "The Notion integration returns Conversation resources"
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 CalendarEvent resources" — and the Additional Endpoints heading reads "Index a website before querying it" for what's actually GET /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
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)
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/index
Call 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)
<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)
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 named signagture
/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.txt is 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 recurring availble typo 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).
Verified runnable code snippets across 26 Hyperspell documentation pages using the supplied app token against the live api.hyperspell.com API. Installed hyperspell (Python 0.37.0) and both @hyperspell/hyperspell (0.38.0) and hyperspell (0.36.0) npm packages, executed snippets with Node 24 and Python 3.9, and exercised curl and npx commands as written.
40 of 67 snippets pass. 17 are skipped — partial framework fragments (FastAPI/Next.js/Clerk/MCP custom-server), template URLs/HTML, and Go/CLI snippets with no runtime installed. 10 fail. The failures cluster in four pages and stem from three recurring documentation defects:
TypeScript examples mix Python keyword‑argument syntax (sources=[...]) and use unescaped quotes in single‑quoted string literals, producing SyntaxError at parse time (core/integration, integrations/all/reddit).
answer_model is shown as a top‑level argument to memories.search in Python, but is actually a member of options; the Python SDK rejects it with TypeError: search() got an unexpected keyword argument 'answer_model' (core/integration).
The "Using a User Token" examples on usage/permissions reference an undefined hyperspell module instead of the client instance (Python), call curl without a URL (X-As-User example), and omit the JSON body required by POST /auth/user_token (422 missing body).
usage/procedural-memory calls client.traces.add(...); the resource is absent from the published Python and TypeScript SDKs (AttributeError / TypeError: Cannot read properties of undefined). The companion cURL embeds literal ... inside the history JSON string, triggering InvalidTraceFormat.
Required credentials
HYPERSPELL_API_KEY — Hyperspell app token (provided by the user, used as Authorization: Bearer … and as apiKey/api_key in SDKs).
Snippet has --request POST and headers but no URL ⇒ curl: (2) no URL specified. Diagnosis: docs bug — example is missing the --url value.
cURL POST /auth/user_token
FAIL
Snippet omits the request body; the endpoint responds with 422 {"detail":[{"type":"missing","loc":["body"]}]}. Diagnosis: docs bug — needs --data '{"user_id":"..."}'.
Python user_token block
FAIL
response = hyperspell.auth.user_token(...) ⇒ NameError: name 'hyperspell' is not defined. Diagnosis: docs bug — should be client.auth.user_token(...); only the class Hyperspell is imported, not the module.
TypeScript auth.userToken block
PASS
Returns a signed JWT user token.
Frontend new Hyperspell({ apiKey: 'YOUR_USER_TOKEN' }) (js)
Partial fragment: missing imports (pydantic, time, HTTPException, undefined router/HYPERSPELL_JWT_SECRET), and the typo signagture would also raise. Requires a full FastAPI app to host; not runnable as written.
TypeScript Next.js route handler
SKIP
Partial fragment: depends on next/server, uses hmac.compare_digest (Python‑only API) inside JS, and references payload before it is declared. Requires a Next.js app to host; not runnable as written.
Partial fragment with literal ... placeholders (new McpServer(...), missing StdioServerTransport/zodToJsonSchema/z imports, and an extra ) after the handler). Documentation example, not standalone code.
Executes; returns empty documents (no Reddit connection on the test user).
TypeScript memories.search w/ reddit options
FAIL
Same sources=["slack"] Python‑kwarg bug as core/integration ⇒ SyntaxError: Invalid shorthand property initializer. Diagnosis: docs bug — should be sources: ["reddit"].
AttributeError: 'Hyperspell' object has no attribute 'traces'. Diagnosis: docs bug or version drift — no traces resource exists in hyperspell 0.37.0 (available resources are actions, auth, connections, evaluate, folders, integrations, memories, sessions, vaults).
Partial fragment referencing undefined user_message, system_prompt, client.
TypeScript client.traces.add({...})
FAIL
TypeError: Cannot read properties of undefined (reading 'add'). Diagnosis: docs bug — traces resource is not exposed on @hyperspell/hyperspell 0.38.0 either.
TypeScript memories.search with memory_types: ['procedure']
PASS
Executes; empty result.
cURL POST /trace/add
FAIL
Request body has "history": "[...\"the team\"}, ...]" with a literal ... inside the JSON string. API responds {"error":"InvalidTraceFormat","message":"Invalid JSON in trace history"}. Diagnosis: docs bug — the illustrative , ...] ellipsis must be removed/replaced before the cURL example becomes runnable.
TypeError: search() got an unexpected keyword argument 'answer_model'. Diagnosis: per the published Python SDK signature, answer_model lives under options, not at the top level.
TypeScript search w/ answer_model
PASS
JS accepts the extra property (no static checking), call succeeds.
Python search w/ resource_ids
PASS
Executes.
TypeScript search w/ resource_ids
PASS
Executes.
cURL /memories/query w/ resource_ids
PASS
200 OK; reports NoResultsForSource for empty vault (call shape correct).