Skip to main content

Integration

Manual API pages

Hand-write an API reference page in MDX when the auto-generator isn't enough. `<ApiMethod>`, `<ParamField>`, `<ResponseField>`, `<RequestExample>`, and `<ResponseExample>` give you full control without losing the shared layout.

Most of the time you want NookDocs to auto-generate API pages from your OpenAPI spec. But sometimes you need to document something the spec can't express — webhook semantics, rate-limit edge cases, partial responses, SSE streams, WebSocket handshakes. For those, author the page in MDX directly using the API primitive components.

When to write manually

No spec exists

Legacy API you're adopting that has no OpenAPI definition. Ship docs first, reverse-engineer the spec later.

Semantics the spec can't express

Retry behaviour, idempotency keys, rate-limit burst vs sustained, eventual consistency windows. Prose > schema.

Custom response formats

Server-sent events, newline-delimited JSON, WebSocket frames. <ResponseExample> accepts any body string.

Specific endpoints with special UX

Your POST /payments has a 3-step flow with customer redirects. Manual page can walk through each step with <Steps> + <RequestExample>.

The five components

<ApiMethod>componentpath

Renders the method + path header at the top of an endpoint page (GET /users/{id}). Colour-coded by method — GET green, POST blue, DELETE red, etc.

<ParamField>componentpath

One row per request parameter — path, query, header, or body field. Props: path (field name), type (TS-ish type), required, default. Children = description.

<ResponseField>componentpath

Same shape as ParamField but for response body fields. Renders in the response panel.

<RequestExample>componentpath

Tabbed code card for the request — cURL, fetch, SDK, whatever you want. Same layout as the auto-generated card.

<ResponseExample>componentpath

Tabbed code card for response examples. One tab per status code or scenario. Status-coloured like auto-gen.

Minimal example

Complete page documenting a POST /payments endpoint from scratch:

---
title: Create payment
description: Charge a card or create a pending bank transfer.
---

<ApiMethod method="POST" path="/v1/payments" />

## Request body

<ParamField path="amount" type="integer" required>
  Amount in smallest currency unit (cents for USD). Min `100`.
</ParamField>

<ParamField path="currency" type="'usd' | 'eur' | 'gbp'" required>
  ISO 4217 currency code. Only these three are supported today.
</ParamField>

<ParamField path="source" type="string" required>
  Payment source token from the Elements SDK (`tok_...`) or a saved
  customer card (`card_...`).
</ParamField>

<ParamField path="description" type="string">
  Appears on the customer's statement. Max 22 characters.
</ParamField>

<ParamField path="idempotencyKey" type="string">
  Unique key to dedupe retries. Same key within 24 hours returns the
  original response without re-charging.
</ParamField>

<RequestExample>
```bash cURL
curl https://api.acme.com/v1/payments \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount": 1000,
    "currency": "usd",
    "source": "tok_visa",
    "description": "Monthly subscription"
  }'
```

```typescript TypeScript
import Acme from "@acme/sdk";
const acme = new Acme({ apiKey: process.env.ACME_KEY });

const payment = await acme.payments.create({
  amount: 1000,
  currency: "usd",
  source: "tok_visa",
  description: "Monthly subscription",
}, {
  idempotencyKey: crypto.randomUUID(),
});
```
</RequestExample>

## Response

<ResponseField name="id" type="string">
  Unique payment identifier. Always starts with `pay_`.
</ResponseField>

<ResponseField name="status" type="'succeeded' | 'pending' | 'failed'">
  `succeeded` — money moved. `pending` — waiting on bank rails (up to 3 business days). `failed` — reason in `failureReason`.
</ResponseField>

<ResponseField name="amount" type="integer">
  Same amount you passed in the request.
</ResponseField>

<ResponseField name="createdAt" type="string (ISO 8601)">
  Server timestamp when the payment was created.
</ResponseField>

<ResponseExample>
```json 200 OK
{
  "id": "pay_abc123",
  "status": "succeeded",
  "amount": 1000,
  "currency": "usd",
  "createdAt": "2026-04-20T10:30:00Z"
}
```

```json 400 Bad Request
{
  "code": "invalid_amount",
  "message": "Amount must be at least 100 for USD.",
  "statusCode": 400
}
```

```json 429 Too Many Requests
{
  "code": "rate_limited",
  "message": "Too many requests. Retry after 60 seconds.",
  "statusCode": 429
}
```
</ResponseExample>

The page renders with the same layout as auto-generated pages — method header, params table, request code card, response field table, response example tabs.

When to use method + path

<ApiMethod> renders a one-line header. If you need the full endpoint treatment (Try-It playground, request tabs, response tabs, status-coloured), you can bind a manual page to an auto-gen'd operation via the openapi frontmatter:

---
title: Create payment — detailed walkthrough
openapi: "POST /v1/payments"
---

This page still renders the Try-It panel + auto-gen'd tabs for
POST /v1/payments. Your prose renders between the method bar and
the generated reference — Callouts, plan banners, setup notes,
any component works here.

## When to use this endpoint

(Your custom narrative, full MDX freedom.)

This hybrid mode is described in the OpenAPI overview. The manual-page-only approach (this page) skips the spec entirely.

Organising manual endpoints

Put them under a sensible path that mirrors your API shape:

docs/
  api/
    payments/
      create.mdx       # <-- manual page
      capture.mdx      # <-- manual page
      refund.mdx       # <-- manual page

Add to nav as a group:

{
  "group": "Payments",
  "pages": [
    "docs/api/payments/create",
    "docs/api/payments/capture",
    "docs/api/payments/refund"
  ]
}

Status badges (GET/POST/etc) appear automatically in the sidebar when the page starts with <ApiMethod>.

Authentication sections

Shared auth blocks belong in a snippet. Today use inline <Snippet> children; Phase 4 file-based snippets will let you include from _snippets/auth.mdx:

<Snippet>
  All endpoints require a `Bearer <api_key>` in the `Authorization`
  header. Test-mode keys start with `sk_test_`; live keys with `sk_live_`.
</Snippet>

See reusable snippets for the full pattern.

Paginated responses

Document cursor pagination with a dedicated field block:

<ResponseField name="data" type="Payment[]">
  Current page of results. Always ordered newest first.
</ResponseField>

<ResponseField name="hasMore" type="boolean">
  Whether more results exist. When `true`, include the last item's `id`
  as the `startingAfter` query parameter to fetch the next page.
</ResponseField>

<ResponseField name="startingAfter" type="string">
  Echoes the cursor from the request, or `null` for the first page.
</ResponseField>

Include a working paginated example in <RequestExample> showing two sequential calls.

Streaming responses (SSE / NDJSON)

<ResponseExample> accepts any body string — use it for non-JSON formats:

<ResponseExample>
```text Server-Sent Events
event: message
data: {"id":"msg_1","content":"Hello"}

event: message
data: {"id":"msg_2","content":"world"}

event: done
data: {"finishReason":"stop"}
```
</ResponseExample>

Document the content-type (text/event-stream / application/x-ndjson) in prose above the example.

Page frontmatter for discovery

Set description so the LLM search + Copy-for-LLMs header has a one-sentence summary. Set icon to give the sidebar entry a glyph. Set hidden: true for internal-only endpoints that you still want routable.

---
title: Rotate API key
description: Generate a new API key and invalidate the old one after a 5-minute grace period.
icon: key-round
---

Related

Was this page helpful?

Last updated August 7, 2026