> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Endpoints

> api/ holds HTTP endpoints the widget calls from the browser, invisible to the model. well-known/ serves the same kind of handler at the root of the domain.

A widget runs in an iframe on another origin, and it can call its own server without going through the model at all. Booking a slot, loading a calendar, looking up a price, receiving a webhook: `api/` is where those live. The path comes from the file's position, the folder name included, so there is nothing to keep in step with the `fetch()` on the other side:

```text theme={null}
api/cal/slots.ts          →  /api/cal/slots
api/webhooks/stripe.ts    →  /api/webhooks/stripe
api/cal/index.ts          →  /api/cal
```

```ts api/cal/slots.ts theme={null}
import { defineEndpoint } from "@waniwani/kit";
import { fetchCalSlots } from "../../lib/cal.js";

export default defineEndpoint({
  method: "post",
  handler: async (req, res) => {
    const { timeZone } = req.body;
    res.json({ slots: await fetchCalSlots(regionFor(timeZone)) });
  },
});
```

The widget reaches it at the origin the host hands the view, which is the dev port locally and the deployed origin inside ChatGPT or Claude:

```tsx theme={null}
const apiUrl = (path: string) => `${window.skybridge?.serverUrl ?? ""}${path}`;
const response = await fetch(apiUrl("/api/cal/slots"), { method: "POST", body });
```

## What the runtime adds

Four things arrive with every endpoint, so no app writes them:

|                  | What the runtime does                                                                                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **CORS**         | on by default, preflight included, advertising the methods `method` declares and no others                                                                               |
| **JSON body**    | `express.json()`, because the framework installs no parser of its own and `req.body` would be `undefined`                                                                |
| **method guard** | anything outside `method` gets a 405 and an `Allow` header, instead of reaching a handler written for a POST                                                             |
| **errors**       | a handler that throws answers JSON with the message, logged as `[waniwani] endpoint "/api/..." failed`, so a `fetch()` waiting on JSON never receives an HTML error page |

<ResponseField name="method" type="HttpMethod | HttpMethod[]">
  Restrict the endpoint to these methods. Leaving it off accepts every method.
</ResponseField>

<ResponseField name="cors" type="boolean" default={true}>
  `false` opts out of CORS.
</ResponseField>

<ResponseField name="json" type="boolean" default={true}>
  `false` opts out of the JSON body parser.
</ResponseField>

<ResponseField name="handler" type="RequestHandler" required>
  An Express handler. Throwing is safe: the runtime answers 500 and logs.
</ResponseField>

<Note>
  **Reach for a tool instead when the model is the caller.** An endpoint never shows up in `tools/list` and the model cannot see that it was called, so it costs the conversation nothing. That suits a calendar the widget paints for itself, and rules it out for anything the model has to reason about or quote back. See [Tools](/kit/tools).
</Note>

Endpoints share the process with `/mcp`, so `lib/` is one set of modules for both, and the build check prints what it mounted:

```text theme={null}
✓ Build check passed — 1 widget, 1 flow, 3 endpoints
  widget     show-book-call
  flow       demo-qualification
  api        /api/cal/book
  api        /api/cal/slots
  well-known /.well-known/openai-apps-challenge
```

## well-known/ is for the root of the domain

Some paths are not the app's to name. `/.well-known/openai-apps-challenge` proves to OpenAI that a deployment is yours; `security.txt` and `apple-app-site-association` answer to standards of their own. Whoever reads them looks at the root of the domain or nowhere, so `/api/` is not an option and neither is a config key.

`well-known/` is `api/` with a different prefix. It takes the same `defineEndpoint`, gets CORS, the method guard and the error envelope from the runtime, and the path still comes from the file's position:

```text theme={null}
well-known/openai-apps-challenge.ts   →  /.well-known/openai-apps-challenge
well-known/security.txt.ts            →  /.well-known/security.txt
```

The folder on disk has no dot. npm strips a `.`-prefixed directory out of a published tarball and the generator treats dotfiles as tooling rather than source, so a literal `.well-known/` would never reach the build. The dot goes back on when the URL is built.

A handler runs per request, which is what a value that differs per environment needs:

```ts well-known/openai-apps-challenge.ts theme={null}
import { defineEndpoint } from "@waniwani/kit";

export default defineEndpoint({
  method: "get",
  handler: (_req, res) => {
    const token = process.env.OPENAI_APPS_CHALLENGE_TOKEN;
    if (!token) return void res.status(404).json({ error: "not configured" });
    res.type("text/plain").send(token);
  },
});
```

The verifier compares the body byte for byte, and `res.send(string)` on its own would label it `text/html`, hence `res.type("text/plain")`. A 404 when the token is unset makes an unconfigured environment look unclaimed.

<Warning>
  Two names are refused: `oauth-authorization-server` and `oauth-protected-resource`. The framework serves those itself once an app configures OAuth, and an app's endpoints mount ahead of the framework's, so a file at either one would answer a client's discovery request with a body it cannot use. `waniwani check` says so by name.
</Warning>

On Vercel this needs no routing of its own. The reservation that forces a route for `/api/*` does not exist under `.well-known`, and the build writes no static file there, so the request misses the filesystem phase and the catch-all already in the tree carries it to the server. See [Deploy](/kit/deploy).
