Notra Documentation Audit
The docs are structurally complete (overview, quickstart, API reference, webhooks, SDKs, MCP, CLI) and ship an llms.txt, but the canonical OpenAPI spec is the unedited Mintlify template, auth env-var names contradict one another inside a single page, and unit-of-measure for rate-limit reset drifts between body and headers. Several of these will silently break copy-paste agent workflows.
1. The canonical OpenAPI spec is the unedited Mintlify Plant Store template (critical)
Location: https://docs.usenotra.com/api-reference/openapi.json (linked from /llms.txt under "OpenAPI Specs")
Problem: The OpenAPI document at /api-reference/openapi.json — the one advertised in llms.txt as the machine-readable spec — is the default Mintlify starter:
"info": { "title": "OpenAPI Plant Store", "description": "A sample API that uses a plant store as an example..." },
"servers": [ { "url": "http://sandbox.mintlify.com" } ],
"paths": { "/plants": {...}, "/plants/{id}": {...} }
Meanwhile, individual endpoint pages (e.g. list-posts.md) reference a different spec at https://api.usenotra.com/openapi.json with title: Notra API and real paths (/v1/posts, etc.).
Consequence: Any AI agent (Claude Code, Cursor, etc.) that follows llms.txt to programmatically discover Notra's API will index endpoints for a plant store pointed at sandbox.mintlify.com. The actual API surface is invisible to automated tooling. Humans browsing /api-reference/ see real endpoints, so the issue is invisible to manual QA but lethal to agents.
The fix: Replace /api-reference/openapi.json with the real spec served from api.usenotra.com/openapi.json, or update llms.txt and the Mintlify docs.json to point at the canonical URL. Delete the Plant Store stub.
2. Authentication environment variable conflicts across pages (critical)
Location: /api/authentication vs /api/getting-started (and Tip box on the auth page itself)
Problem: The same page presents two incompatible env var names for the same key:
- The fetch example uses
process.env.NOTRA_API_KEY. - The SDK example two snippets below uses
process.env["NOTRA_BEARER_AUTH"]. - The auth table lists the canonical env var as
NOTRA_BEARER_AUTH. - The Tip says "Store your key in a server-side environment variable such as
NOTRA_BEARER_AUTH." - But
/api/getting-startedusesbearerAuth: process.env.NOTRA_API_KEY ?? "".
Consequence: A developer copy-pasting the fetch sample sets NOTRA_API_KEY; switching to the SDK silently reads undefined and hits the API with Bearer (empty), producing a 401. Agents that scrape one page and trust it will pick whichever they see first.
The fix: Pick one (NOTRA_API_KEY is the more conventional choice for a product called Notra) and replace NOTRA_BEARER_AUTH everywhere — both code samples, the table, the Tip, and the SDK README. The SDK's own default constant should be updated to match.
3. PostUpdateRequest type disagrees between /api/types and /api/common-tasks (significant)
Location: /api/types vs /api/common-tasks
Problem: /api/types defines:
export type NotraPostUpdateRequest = {
title?: string;
slug?: string | null;
markdown?: string;
status?: "draft" | "published";
};
/api/common-tasks defines the same request for PATCH /v1/posts/{postId} as:
type UpdatePostRequest = {
title?: string;
markdown?: string;
status?: "draft" | "published";
};
slug is missing entirely from common-tasks. The Zod schema on /api/types also exposes slug with a regex and length bounds (SLUG_REGEX, min(1).max(160)), confirming slug is a real, validated field — not an oversight.
Consequence: A developer following common-tasks builds a UI that can never set a post slug, even though the API accepts it. An agent generating client code from common-tasks produces an incomplete type; from types, a complete one. Worse: the /api/common-tasks page explicitly warns "If a guide or AI answer suggests…that information is not correct" — yet the two official guide pages are themselves the source of the conflict.
The fix: Single-source these types. Generate them from the same OpenAPI spec used in #1 and inline them on both pages, or keep the canonical version on /api/types and have common-tasks import/embed it.
4. llms.txt advertises endpoints that the Getting Started page denies exist (significant)
Location: /llms.txt vs /api/getting-started
Problem: llms.txt lists routes under chats/ (e.g., "Start a new chat and stream the reply", "List chats") and skills/ (create/delete/get/list/update a skill). The inline OpenAPI snippet on list-posts.md confirms these as tags: Content, Schedules, Chats, Skills. But /api/getting-started enumerates the discoverable surface as REST API / TypeScript SDK / MCP Server only, and references Posts as the example endpoint — Chats and Skills are absent from the page's discoverability story.
Consequence: Developers and agents reading the Getting Started page won't discover the streaming chat or skill endpoints. An agent that crawls llms.txt will see links to surfaces the official getting-started page never introduces.
The fix: Add a "Resources" or "Available Endpoints" listing on /api/getting-started that includes Chats and Skills with links to their reference pages. If either is private/beta, mark it explicitly rather than omitting it.
5. Rate-limit reset unit drifts between response body and header (significant)
Location: /api/rate-limits
Problem: The response-headers table lists RateLimit-Reset as "Seconds until the window resets" (a duration) and X-RateLimit-Reset as "Unix timestamp (seconds) when the window resets" (an absolute time). Separately, the rate-limit JSON body field reset uses milliseconds — a third unit for what reads like the same concept. The section titled "## 429 response" appears empty in the scrape, so there is no worked example anywhere on the page to disambiguate any of these.
Consequence: A developer writing retry logic against the body's reset field (ms) will compute a wake-up 1000× too short if they treat it like the RateLimit-Reset header (seconds), or wildly wrong if they treat it like the X-RateLimit-Reset Unix timestamp. Without a sample 429 response, every client author has to guess. Agents auto-generating retry code from this page have no way to pick the right multiplier.
The fix: Add a concrete 429 example body alongside the headers, with values, and state the unit beside every reset-shaped field. Recommend Retry-After as the single canonical retry signal, and consider unifying the body field to seconds to match the headers.
6. Pagination "soft-clamp" behavior contradicts the explicit error path (significant)
Location: /api/pagination
Problem: The page says values below 1 for limit "default to 1" and non-numeric values "default to 10" — a forgiving soft-clamp. But for page, requesting a non-existent page returns a hard error:
{ "error": "Invalid page number", "details": { "message": "Page 2 does not exist.", "totalPages": 1, "requestedPage": 2 } }
So limit=0 is silently corrected; page=2 on an empty resultset is a thrown error. The inline OpenAPI in list-posts.md declares page: minimum: 1, suggesting page=0 would 400 — but it's never confirmed.
Consequence: Clients can't write a single defensive policy. If a user navigates "next page" past the last record, the client breaks; if they pick a too-small page size, it silently mutates. Agents iterating pages can't tell when they've fallen off the end without a try/catch.
The fix: Either soft-clamp page to totalPages (returning an empty array beyond the end), or hard-validate both limit and page consistently. Document the actual error responses for page=0, page=-1, and limit=0 with status codes.
7. Webhook receivers have no documented signature-verification step (significant)
Location: /api/webhooks/overview and /api/webhooks/events
Problem: The webhook docs describe a GitHub-sourced delivery to POST https://api.usenotra.com/api/webhooks/{provider}/{organizationId}/{integrationId}/{repositoryId} and walk through deduplication (X-GitHub-Delivery, 24h cache) and event filtering, but never mention a signature header, shared secret, or verification routine (no X-Hub-Signature-256, no HMAC step, no secret rotation). The "Webhook Logging" section logs payloads but doesn't gate by signature.
Consequence: A reader implementing or auditing a receiver has no way to know whether deliveries are signed. Self-hosted setups and security reviewers will assume the worst (silent forgery risk), and customer security teams will block the integration until someone in support confirms it. If signing exists, it's invisible; if it doesn't, that's a security gap that should be called out, not omitted.
The fix: Add a "Verifying webhook signatures" section to /api/webhooks/overview with the exact header name, algorithm, secret-storage guidance, and a copy-pasteable verification snippet. If verification isn't implemented yet, document that explicitly with a roadmap note.
8. Webhook docs leak a third-party vendor (Supermemory) into customer-facing examples (significant)
Location: /api/webhooks/events → "Memory Storage" section
Problem: The "Memory Entry Creation" code sample shows a literal fetch('https://api.supermemory.ai/v3/documents', …) call with Bearer ${apiKey} — implying customers must know about and integrate Supermemory directly. This is an internal implementation detail of Notra's pipeline, exposed in a section titled as if it's customer guidance.
Consequence: Confused developers try to obtain a Supermemory API key, ask in support, or worse: code against api.supermemory.ai because the docs imply it's part of the contract. It also leaks Notra's tech-stack choices to competitors and locks Notra in by making the vendor switch a breaking-doc change.
The fix: Either remove the code sample entirely (the prose "Processed events are stored in Notra's memory system for AI context" is sufficient) or replace it with a Notra-side endpoint like GET /v1/memory/entries that customers can actually call.
9. Webhook paths sit under /api/webhooks/, but the documented API base is /v1/ (minor)
Location: /api/webhooks/overview vs /api/getting-started
Problem: Getting Started declares the base URL as https://api.usenotra.com/v1/:resource. The webhook receiver lives at https://api.usenotra.com/api/webhooks/{provider}/{organizationId}/{integrationId}/{repositoryId} — no /v1/, with an /api/ prefix that no other endpoint uses.
Consequence: Developers can't deduce the webhook URL from the base URL convention. Anyone setting up a self-hosted GitHub App against Notra (or migrating) has to find this one page to learn the exception. It also signals an unversioned webhook contract — what happens to existing receivers when payload shapes change?
The fix: Move webhooks under /v1/webhooks/... for consistency, or document the versioning policy for the webhook surface explicitly and explain why it lives at a different prefix.
10. Quickstart's first code sample is missing imports and dangles an undefined authClient (minor)
Location: /quickstart → "1. Create your organization"
Problem: The very first runnable snippet in a page that promises "your first AI-powered content in under 5 minutes" is:
await authClient.organization.create({
name: "Acme Inc",
slug: "acme-inc",
websiteUrl: "https://example.com"
});
There is no import line, no instantiation of authClient, and no link to wherever authClient comes from. An agent extracting this snippet to run will hit ReferenceError: authClient is not defined immediately.
Consequence: Copy-paste is broken on step 1. Developers have to leave the page and search for whatever package or auth.ts file exposes authClient. Agents will silently fail on the very first instruction.
The fix: Add the import (or a one-line "before you start" instantiation) above the snippet, or replace the example with a curl/REST call that the rest of the page is consistent with.
11. Base URL uses Rails-style :resource placeholder syntax that's easy to misread as literal (minor)
Location: /api/getting-started
Problem: The Base URL is rendered as:
https://api.usenotra.com/v1/:resource
with prose ":resource is the endpoint path (e.g., posts, posts/{postId})." The page then immediately switches to {postId}-style curly placeholders for path params. Two placeholder syntaxes coexist for the same concept, and :resource is unusual in JSON-API-style REST docs.
Consequence: Readers (and especially agents tokenising the URL) may treat :resource as a literal path segment or path-parameter to substitute via colon-prefix routing — particularly when the very next examples use {postId} curly-brace convention.
The fix: Pick one placeholder syntax ({resource} is more consistent with the rest of the page) and use it everywhere, or drop the placeholder and write https://api.usenotra.com/v1/... with concrete examples beneath.
12. Rust SDK is linked from llms.txt and the API reference with no warning that field naming diverges from the canonical API (minor)
Location: /api/rust-sdk and /llms.txt
Problem: /api/rust-sdk's page body in the scrape ends at ## Installation — there is no documented mapping between the Rust crate's request shapes and the canonical Notra API's request fields. The page is labelled "Community" but is linked at peer level with the official TypeScript SDK from llms.txt. No "examples from the TypeScript SDK don't translate field-for-field" warning appears.
Consequence: A developer (or agent) translating a TypeScript example into Rust by structural analogy may produce payloads the API rejects, and bug reports land on the community maintainer rather than Notra. The lack of an explicit naming-policy note also means anyone evaluating the SDK can't tell at a glance how far it has drifted.
The fix: Add a short "Field naming" section to /api/rust-sdk mapping crate types to canonical API parameters (or stating that field names match), and add a one-line warning that community-maintained SDKs are not guaranteed to track the API on the same schedule.
What they do well
- An
llms.txtexists and is reasonably complete — most companies still don't ship one. /api/common-tasksproactively warns about an incorrect endpoint (POST /v1/posts) that AI assistants commonly hallucinate. That kind of "we know what wrong answers exist in the wild" defensive doc is rare and valuable.- Rate-limit table is concrete per-endpoint, with numbers — not vague "fair use" prose.
Top 3 recommendations
- Replace the Plant Store OpenAPI stub at
/api-reference/openapi.jsonwith the real spec served fromapi.usenotra.com/openapi.json, sollms.txtactually points agents at the Notra API. - Pick one auth env-var name (
NOTRA_API_KEYorNOTRA_BEARER_AUTH) and purge the other from every code sample, table, and SDK default — including the inconsistencies that appear inside a single page. - Make the rate-limit and webhook contracts complete and verifiable: a worked 429 body with consistent units for
reset, and a documented signature-verification routine for webhook receivers.