# Functions Interfold Functions run server-side TypeScript at `/api/*` on your site. Use them for form handlers, webhooks, small APIs, and any logic that shouldn't live in the browser. No build step, no server to manage — deploy is just uploading a file. ## The contract A function is a single TypeScript file with one rule: export a named `handler` that returns a `Response`. ```ts export async function handler(req: Request, ctx: InterfoldContext) { const url = new URL(req.url) const name = url.searchParams.get('name') ?? 'world' return Response.json({ ok: true, message: `hello ${name}`, siteId: ctx.siteId, }) } ``` - **Source path:** `/api/.ts` — exactly one segment under `/api/`. - **Runtime path:** `/api/` — no `.ts` extension. - **Signature:** `handler(req: Request, ctx: InterfoldContext)`. - **Return:** a standard `Response` (use `Response.json(...)` for JSON). ## Invocation and authorization The runtime endpoint is anonymously callable for both `PUBLIC` and `PRIVATE` Sites. Site access controls only `/web/` static content; it neither exposes function source nor authorizes a function request. If a function protects content or performs a privileged action, its handler must validate its own cookie, token, signature, or other credential and return the appropriate `401` or `403`. Do not rely on a `PRIVATE` Site as an outer function gate. ### v0 limits Functions v0 runs a single self-contained file. It does **not** support: - `import` statements, npm packages, or Node built-ins - nested paths like `/api/foo/bar.ts` Keep each function to one file and the standard web `Request`/`Response` APIs. For scheduled execution, see [Cron Jobs](/cron-jobs). ## The `ctx` object Every handler receives a context object: | Property | Description | | ------------- | ---------------------------------------------------------------------- | | `ctx.siteId` | The id of the site the function belongs to. | | `ctx.data` | Text, JSON, and JSONL under `/data/`. See [Data files](/data-storage). | | `ctx.db` | SQL database queries. See [Databases](/databases). | | `ctx.secrets` | Read site secrets at runtime. See [Secrets](/secrets). | | `ctx.llm` | Call OpenAI models using the site's account usage allowance. | ## Model calls Use `ctx.llm.messages.create` from a function or cron job. No OpenAI API key, SDK, or site secret is needed. ```ts // /api/summarize.ts export async function handler(req: Request, ctx: InterfoldContext) { if (req.method !== 'POST') { return new Response('Use POST', { status: 405 }) } // Validate your application's user/session here before spending account usage. const input = await req.json().catch(() => null) if ( typeof input?.text !== 'string' || !input.text.trim() || input.text.length > 8000 ) { return Response.json( { error: 'Provide 1–8000 characters of text' }, { status: 400 } ) } const result = await ctx.llm.messages.create({ model: 'gpt-6-luna', messages: [ { role: 'system', content: 'Summarize the text in three short bullets.' }, { role: 'user', content: input.text }, ], maxOutputTokens: 4096, }) return Response.json({ summary: result.message.content }) } ``` `model` defaults to `gpt-6-luna`; `gpt-6.1-sol` is also supported. `reasoningEffort` defaults to `low` and accepts `none`, `low`, `medium`, or `high`. Only Luna supports `none`, which is useful for short classifications. `maxOutputTokens` defaults to 4,096 and accepts 16–8,192. It includes internal reasoning and formatting, not just visible text. Avoid tiny budgets with reasoning enabled, even for one-word answers. If the limit is hit, increase the budget, request a shorter answer, or lower the reasoning effort. Send 1–100 text messages using `user` and `assistant` roles, with an optional first `system` message and at least one user message. Each message is limited to 32,000 characters; combined content is limited to 64,000 UTF-8 bytes. Calls time out after 45 seconds. Streaming, images, tools, and provider-managed conversation state are not supported; send the conversation history with each call. Read the answer from `result.message.content`. The result also includes `id`, `model`, and `usage`. Calls share the site's owning account allowance with browser-agent usage. Failed calls can also consume usage. Errors include a stable `code`, an actionable `message`, and an HTTP `status` when available. Preserve the code and message in your application's error response. Uncaught model errors return their status, code, and message as JSON. Calls are not automatically retried. ## Deploying Upload the source under `/api/`: ```sh interfold files put ./hello.ts /api/hello.ts --site --content-type text/typescript ``` Then invoke the runtime path: ```sh interfold sites get curl -i "https://.interfold.site/api/hello?name=agent" ``` Functions accept any HTTP method — read `req.method` to branch: ```ts export async function handler(req: Request, ctx: InterfoldContext) { if (req.method !== 'POST') { return new Response('Method not allowed', { status: 405 }) } const body = await req.json() return Response.json({ received: body }) } ``` ## Cookies and password-protected content Same-origin cookies are available through the standard `Request` headers. A function can also return `Set-Cookie`; the browser stores it and sends it with later requests to the same site. Interfold's own reserved session cookie is not exposed to user functions and cannot be overwritten by them. Keep the protected content behind the function rather than embedding it in the public HTML. For example, configure `CONTENT_PASSWORD` and a separate random `CONTENT_SESSION_TOKEN` as [site secrets](/secrets), then deploy this as `/api/protected.ts`: ```ts function readCookie(req: Request, name: string): string { const cookieHeader = req.headers.get('cookie') ?? '' for (const cookie of cookieHeader.split(';')) { const [cookieName, ...valueParts] = cookie.trim().split('=') if (cookieName === name) { return decodeURIComponent(valueParts.join('=')) } } return '' } const privateResponseHeaders = { 'Cache-Control': 'private, no-store', } export async function handler(req: Request, ctx: InterfoldContext) { const password = await ctx.secrets.get('CONTENT_PASSWORD') const sessionToken = await ctx.secrets.get('CONTENT_SESSION_TOKEN') if (req.method === 'POST') { const body = (await req.json()) as { password?: string } if (body.password !== password) { return new Response('Unauthorized', { status: 401, headers: privateResponseHeaders, }) } return new Response(null, { status: 204, headers: { ...privateResponseHeaders, 'Set-Cookie': `protected_session=${encodeURIComponent(sessionToken)}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400`, }, }) } if (readCookie(req, 'protected_session') !== sessionToken) { return new Response('Unauthorized', { status: 401, headers: privateResponseHeaders, }) } return Response.json( { content: 'The protected content goes here.' }, { headers: privateResponseHeaders } ) } ``` From the page, submit the password and then request the content. Same-origin `fetch` includes the stored cookie automatically: ```ts await fetch('/api/protected', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), }) const response = await fetch('/api/protected') const { content } = await response.json() ``` ## A stateful example Combine `ctx.data` for a hit counter that survives restarts: ```ts export async function handler(req: Request, ctx: InterfoldContext) { const previous = (await ctx.data .readJson('/data/state.json') .catch(() => ({ visits: 0 }))) as { visits: number } const next = { visits: previous.visits + 1, updatedAt: new Date().toISOString(), } await ctx.data.writeJson('/data/state.json', next) return Response.json(next) } ``` ## Troubleshooting a deploy A failed deploy can leave the source saved while the function status is `failed`. Read the CLI response and check: - The path is exactly one segment under `/api/`, e.g. `/api/hello.ts`. - The file exports a named `handler`. - The handler returns a `Response`. - There are no `import`s, npm packages, or Node built-ins. Fix the source and re-upload. Then re-verify the runtime path with `curl -i`. --- Next: [Cron Jobs](/cron-jobs) for scheduled execution · [Databases](/databases) for structured data · [Data files](/data-storage) for file-shaped data · [Secrets](/secrets) for runtime config.