Skip to main content

API Endpoints

Kickstart's backend runs on Azure Functions v4 (Node.js), deployed as managed functions inside an Azure Static Web App. Endpoints handle LLM conversations, code generation, A2UI action processing, and proxy routes to external APIs.

This guide covers the endpoint pattern, SSE streaming, shared utilities, and how to add a new endpoint.

How Endpoints Work​

Azure Functions v4 Pattern​

Every endpoint follows the same registration pattern using app.http() in packages/web/api/src/functions/:

import { app } from "@azure/functions";
import type {
HttpRequest,
HttpResponseInit,
InvocationContext,
} from "@azure/functions";

app.http("functionName", {
methods: ["POST"],
authLevel: "anonymous",
route: "endpoint-path",
handler: async (
request: HttpRequest,
context: InvocationContext,
): Promise<HttpResponseInit> => {
// Handler logic
},
});
PropertyDescription
"functionName"Unique function name used by the Azure Functions runtime
methodsHTTP methods to accept (["GET"], ["POST"], etc.)
authLevelAlways "anonymous" — SWA handles auth at the edge
routeURL path (relative to /api/)
handlerAsync function receiving the request and invocation context

Existing Endpoints​

Conversation & Session​

EndpointMethodRouteAuthResponse FormatDescription
conversePOST/api/converseNone (rate-limited)SSE or JSONMain LLM conversation proxy — multi-round tool calling
actionPOST/api/actionSession requiredJSONA2UI action event processing
generatePOST/api/generateNone (rate-limited)SSE or JSONCodex-powered code generation
healthGET/api/healthNoneJSONHealth check ({ status: "ok" })

GitHub Integration​

EndpointMethodRouteAuthResponse FormatDescription
github-authGET/POST/api/github-auth/{action}SWA principalHTML / JSONGitHub OAuth flow — login, callback, session state, logout
github-reposGET/POST/api/github/reposGitHub session cookieJSONList or create GitHub repositories
github-pullsPOST/api/github/pullsGitHub session cookieJSONCommit files and open a pull request

Azure Integration​

EndpointMethodRouteAuthResponse FormatDescription
arm-proxyGET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS/api/arm-proxy/{*path}Azure access tokenJSONAzure Resource Manager CORS proxy
azure-targetPUT/api/sessions/{sessionId}/azure-targetAzure access token + SWA sign-in + sessionJSONPersist Azure subscription / resource group for a session
azure-deployments-startPOST/api/sessions/{sessionId}/azure-deploymentsAzure access token + SWA sign-in + sessionJSONKick off an ARM deployment for a session
azure-deployments-statusGET/api/azure-deployments/{runId}Azure access token + SWA sign-inJSONPoll the status of a running ARM deployment
deploy-cost-gatePOST/api/sessions/{sessionId}/deploy-gates/costSWA sign-in + sessionJSONRecord that the user acknowledged estimated deployment costs
cost-estimatePOST/api/sessions/{sessionId}/cost-estimateSession requiredJSONFetch a live Azure cost estimate for the session's resource line-items

Utility​

EndpointMethodRouteAuthResponse FormatDescription
pricing-proxyGET/api/pricing-proxyNoneJSONCORS proxy for the Azure Retail Prices API
inspirationsGET/api/inspirationsNoneJSON / SSELanding page inspiration ideas (LLM-generated or hardcoded fallback)
widget-inspirationsGET/api/inspirations/widgetsNoneJSON / SSEPlayground widget inspiration prompts
playgroundPOST/api/playgroundNoneJSON🔧 Internal — A2UI Playground Create tab; iterates A2UI component designs

Deprecated (410 Gone)​

These routes are registered solely to return 410 Gone to any client still calling them. Do not use them.

EndpointRouteReplacement
github-proxy-legacy/api/github-proxy/{*path}Use /api/github-auth, /api/github/repos, /api/github/pulls
github-oauth-legacy/api/github-oauth/{*path}Use /api/github-auth/login

SSE Streaming Pattern​

Long-running AI operations use Server-Sent Events for real-time streaming. The client opts in by sending Accept: text/event-stream:

// Check if the client wants SSE
const wantsStream = request.headers
.get("accept")
?.includes("text/event-stream");

if (wantsStream) {
return handleStreaming(/* ... */);
}

// Otherwise return a normal JSON response
return { status: 200, jsonBody: result };

The streaming response uses a ReadableStream with TextEncoder:

function handleStreaming(/* params */): HttpResponseInit {
const encoder = new TextEncoder();

const stream = new ReadableStream({
async start(controller) {
try {
// Stream content chunks
controller.enqueue(
encoder.encode(
`event: chunk\ndata: ${JSON.stringify({ content: "..." })}\n\n`
),
);

// Stream tool calls and results
controller.enqueue(
encoder.encode(
`event: tool_call\ndata: ${JSON.stringify({ name: "tool_name" })}\n\n`
),
);

// Final done event
controller.enqueue(
encoder.encode(
`event: done\ndata: ${JSON.stringify({ sessionId, phase })}\n\n`
),
);

controller.close();
} catch (err) {
controller.enqueue(
encoder.encode(
`event: error\ndata: ${JSON.stringify({ error: "message" })}\n\n`
),
);
controller.close();
}
},
});

return {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
body: stream,
};
}

SSE Event Types​

The converse endpoint defines these event types:

Event TypePayloadDescription
chunk{ content: string }Incremental text chunk from the LLM
tool_call{ name: string }LLM requested a tool call
tool_result{ name: string, result: unknown }Tool execution result
message{ content: string }Final complete message text
a2uiA2UI component JSONStructured UI component for rendering
done{ sessionId, phase, phaseLabel, model, ... }Stream complete — metadata payload
error{ error: string }Stream error — generic message only

Shared Utilities​

Endpoints share a set of utility modules in packages/web/api/src/lib/:

ModuleExportsPurpose
runner.tsRunnerOrchestrates agent turns, guardrails, tool dispatch, and UserAction pause/resume
session-store.tsgetSession(), createSession(), addMessage()In-memory session management with 1-hour TTL
error-response.tssafeErrorResponse(), safeStreamError()Generic error responses — logs details server-side, returns safe messages to clients
rate-limiter.tscheckRateLimit(), rateLimitResponse()Sliding-window rate limiting (30 req/min per client, 300 req/min global backstop)

Endpoint Reference​

Conversation & Session​

POST /api/converse​

Auth: None (rate-limited)
Body: { sessionId?: string, message: string, messages?: ClientMessage[] }
Returns: SSE stream (if Accept: text/event-stream) or JSON { sessionId, phase, message, a2ui?, model?, usage?, ... }
Source: packages/web/api/src/functions/converse.ts

Main LLM conversation endpoint. Manages multi-round tool calling and streams incremental output. sessionId is optional — if omitted or the in-memory session has expired, a new session is created automatically. Pass messages (client-side message history) to rehydrate context across cold starts.

POST /api/action​

Auth: Session required
Body:

{
"sessionId": "string",
"action": { "name": "string", "context": "Record<string, unknown> (optional)" },
"context": { "phase": "string (optional)" },
"messages": "ClientMessage[] (optional, for cold-start rehydration)"
}

Returns: JSON { success: boolean, sessionId, message?, phase?, a2uiMessages?, model? }
Source: packages/web/api/src/functions/action.ts

Processes an A2UI action event — called when the user interacts with a button, picker, or other A2UI component. Routes by action name prefix (navigate: / nav: = navigation, api: = stubbed, default = reply) and re-prompts the LLM.

POST /api/generate​

Auth: None (rate-limited)
Body: { prompt: string, type?: "dockerfile" | "kubernetes" | "pipeline" | "bicep" | "generic", context?: string }
Returns: SSE stream (if Accept: text/event-stream) or JSON { type, code, responseId }
Source: packages/web/api/src/functions/generate.ts

Codex-powered code generation endpoint. Accepts a prompt and optional type hint; returns generated code. No session state — stateless per-request. Streaming is opted into via Accept: text/event-stream.

GET /api/health​

Auth: None
Returns: JSON { status: "ok" }
Source: packages/web/api/src/functions/health.ts

Liveness probe. Returns immediately with no dependencies checked.


GitHub Integration​

GET/POST /api/github-auth/{action}​

Auth: SWA principal (Kickstart sign-in required for login)
Route param: action — one of login, callback, session, logout
Returns: HTML (login/callback) or JSON (session/logout)
Source: packages/web/api/src/functions/github-auth.ts

Manages the GitHub OAuth 2.0 popup flow:

ActionMethodDescription
loginGETRedirects the popup window to GitHub's OAuth authorization page
callbackGETReceives the OAuth code, exchanges it for a token, sets session cookies, closes popup
sessionGETReturns current GitHub session state { login, avatarUrl, connected }
logoutPOSTClears GitHub session cookies (204 No Content)

The login action accepts an optional ?returnTo= query parameter to redirect back after sign-in.

GET /api/github/repos​

Auth: GitHub session cookie
Query params: owner (optional), page (default 1), perPage (default 20)
Returns: JSON { repos: GitHubRepo[] }
Source: packages/web/api/src/functions/github-repos.ts

Lists GitHub repositories accessible to the authenticated user. Refreshes the session cookie on each call.

POST /api/github/repos​

Auth: GitHub session cookie
Body: { owner: string, name: string, description?: string, private?: boolean }
Returns: JSON { repo: GitHubRepo } (201 Created)
Source: packages/web/api/src/functions/github-repos.ts

Creates a new GitHub repository on behalf of the authenticated user.

POST /api/github/pulls​

Auth: GitHub session cookie
Body:

{
"owner": "string",
"repo": "string",
"head": "string",
"base": "string (optional, defaults to default branch)",
"title": "string",
"body": "string (optional)",
"commitMessage": "string (optional)",
"files": [{ "path": "string", "content": "string" }]
}

Returns: JSON { pullRequest, branch, commit } (201 Created)
Source: packages/web/api/src/functions/github-pulls.ts

Commits files to a new branch and opens a pull request in a single call.


Azure Integration​

ANY /api/arm-proxy/{*path}​

Auth: Azure access token (X-Azure-AccessToken header or SWA Azure auth cookie)
Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Returns: Proxied response from management.azure.com
Source: packages/web/api/src/functions/arm-proxy.ts

CORS proxy for Azure Resource Manager. Forwards the request path, query parameters, and body to management.azure.com, injecting the caller's Bearer token. Adds a default api-version=2024-03-01 if the caller omits one. Host-allowlisted — only management.azure.com is accepted.

PUT /api/sessions/{sessionId}/azure-target​

Auth: Azure access token + SWA sign-in (x-ms-client-principal-id header required) + session ownership
Body:

{
"subscriptionId": "string",
"resourceGroup": "string",
"location": "string",
"createIfMissing": "boolean (or use resourceGroupMode: 'new')"
}

Returns: JSON { sessionId, azureContext, deployState }
Source: packages/web/api/src/functions/azure-target.ts

Stores the user's chosen Azure subscription and resource group against the session. Optionally creates the resource group if it doesn't exist.

POST /api/sessions/{sessionId}/azure-deployments​

Auth: Azure access token + SWA sign-in (x-ms-client-principal-id header required) + session ownership
Body:

{
"mainFile": "string",
"files": [{ "path": "string", "content": "string" }],
"deploymentName": "string (optional)",
"parameters": "Record<string, unknown> (optional)",
"appUrlOutput": "string (optional)",
"healthCheckPath": "string (optional)"
}

Returns: JSON { runId, status } (202 Accepted)
Source: packages/web/api/src/functions/azure-deployments.ts

Starts an ARM template deployment. Returns a runId that the client polls with the status endpoint below.

GET /api/azure-deployments/{runId}​

Auth: Azure access token + SWA sign-in (x-ms-client-principal-id header required) + principal ownership
Returns: JSON { runId, status, outputs, error }
Source: packages/web/api/src/functions/azure-deployments.ts

Polls deployment status. The runId is opaque and encodes the principal identifier — a deployment run can only be polled by the user who started it.

POST /api/sessions/{sessionId}/deploy-gates/cost​

Auth: SWA sign-in (x-ms-client-principal-id header required) + session ownership
Body: { estimatedMonthlyTotal: number, currency?: string, source?: string }
Returns: JSON { sessionId, deployState }
Source: packages/web/api/src/functions/deploy-cost-gate.ts

Records that the user has acknowledged the estimated monthly cost before deployment. Updates the session's deployState.costGatePassed flag. The frontend uses this as a gate before calling the start-deployment endpoint.

POST /api/sessions/{sessionId}/cost-estimate​

Auth: Session ownership
Rate limit: 12 requests per minute (stricter than the default 30/min)
Body: { region: string, lineItems: CostLineItem[] } (see packages/web/api/src/lib/cost-estimate.ts for the full schema)
Returns: JSON cost estimate with live prices, cache metadata, and fallback info
Source: packages/web/api/src/functions/cost-estimate.ts

Fetches a cost estimate from the Azure Retail Prices API for the resource types in lineItems. Results are cached per region/SKU combination. Falls back to static prices if the upstream API is unavailable.


Utility​

GET /api/pricing-proxy​

Auth: None
Query params: Any OData $filter / $orderBy supported by the Azure Retail Prices API
Returns: Proxied JSON from prices.azure.com/api/retail/prices
Source: packages/web/api/src/functions/pricing-proxy.ts

Public CORS proxy for the Azure Retail Prices API. No authentication required — the pricing API is public. Responses are cached for 5 minutes (Cache-Control: public, max-age=300).

Example: GET /api/pricing-proxy?$filter=serviceName eq 'Azure Kubernetes Service'

GET /api/inspirations​

Auth: None
Query params: stream=true (optional) — stream a single idea token-by-token
Returns: JSON InspirationIdea[] (array of { title, subtitle, prompt }) or SSE stream
Source: packages/web/api/src/functions/inspirations.ts

Returns carousel inspiration ideas for the landing page. Requires Azure OpenAI to be configured — returns 503 Service Unavailable if AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, and a deployment name are not set. When configured, generates creative app ideas via LLM (AZURE_OPENAI_INSPIRE_DEPLOYMENT is used if set, otherwise falls back to the chat deployment).

GET /api/inspirations/widgets​

Auth: None
Query params: stream=true (optional) — stream a widget prompt token-by-token
Returns: JSON WidgetIdea[] (array of { title: string, subtitle: string, prompt: string }) or SSE stream
Source: packages/web/api/src/functions/widget-inspirations.ts

Returns Playground widget inspiration prompts. If Azure OpenAI is configured, generates AKS operational widget ideas via LLM. Otherwise returns a shuffled fallback idea from a built-in list. Used by the Playground Create tab to seed the prompt field.

POST /api/playground​

🔧 Internal — used exclusively by the A2UI Playground Create tab. Not part of the public API contract.

Auth: None (rate-limited)
Body: { message: string, sessionId?: string }
Returns: JSON { sessionId: string, message: string, a2ui?: object[] }
Source: packages/web/api/src/functions/playground.ts

Dedicated A2UI component design endpoint. Uses a specialised system prompt focused on iterating over A2UI component structures. Maintains its own lightweight in-memory session store (separate from the main conversation sessions, 1-hour TTL).


How to Add an Endpoint​

Step 1 — Create the function file​

Create a new file in packages/web/api/src/functions/. The filename doesn't matter for routing — the route parameter in app.http() controls the URL path.

packages/web/api/src/functions/my-endpoint.ts

Step 2 — Implement the handler​

Use the standard Azure Functions v4 pattern. Apply rate limiting and error handling:

import { app } from "@azure/functions";
import type {
HttpRequest,
HttpResponseInit,
InvocationContext,
} from "@azure/functions";
import { checkRateLimit, rateLimitResponse } from "../lib/rate-limiter.js";
import { safeErrorResponse } from "../lib/error-response.js";

interface MyEndpointRequest {
query: string;
}

app.http("myEndpoint", {
methods: ["POST"],
authLevel: "anonymous",
route: "my-endpoint",
handler: async (
request: HttpRequest,
context: InvocationContext,
): Promise<HttpResponseInit> => {
// Rate limit check
const rateCheck = checkRateLimit(request);
if (!rateCheck.allowed) {
return rateLimitResponse(rateCheck.retryAfterMs!);
}

try {
const body = (await request.json()) as MyEndpointRequest;

if (!body.query?.trim()) {
return { status: 400, jsonBody: { error: "query is required" } };
}

// Your logic here
const result = await processQuery(body.query);

return { status: 200, jsonBody: result };
} catch (err) {
return safeErrorResponse(err, context, "MyEndpoint error");
}
},
});

Step 3 — Add SSE streaming (if applicable)​

If your endpoint calls the LLM or performs long-running work, support SSE streaming. Check the Accept header and return a ReadableStream:

const wantsStream = request.headers
.get("accept")
?.includes("text/event-stream");

if (wantsStream) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
// Stream your data
for (const item of results) {
controller.enqueue(
encoder.encode(
`event: chunk\ndata: ${JSON.stringify(item)}\n\n`
),
);
}

controller.enqueue(
encoder.encode(
`event: done\ndata: ${JSON.stringify({ complete: true })}\n\n`
),
);
controller.close();
} catch (err) {
const safeMsg = safeStreamError(err, context, "Stream error");
controller.enqueue(
encoder.encode(
`event: error\ndata: ${JSON.stringify({ error: safeMsg })}\n\n`
),
);
controller.close();
}
},
});

return {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
body: stream,
};
}

Step 4 — Build and verify​

The API uses esbuild and auto-discovers function files — no manual registration needed:

npm run build -w @kickstart/api

Start the development server and test your endpoint:

# JSON response
curl -X POST http://localhost:7071/api/my-endpoint \
-H "Content-Type: application/json" \
-d '{"query": "test"}'

# SSE streaming
curl -X POST http://localhost:7071/api/my-endpoint \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"query": "test"}'

Step 5 — Add SWA route config (if needed)​

If your endpoint needs specific routing rules (e.g., CORS headers, auth requirements), update swa-cli.config.json or add route rules to staticwebapp.config.json. The default wildcard /api/* proxies all API calls, so new endpoints under /api/ typically work without changes.


Patterns to Follow​

Error Handling​

Always use safeErrorResponse() for non-streaming endpoints and safeStreamError() for SSE streams. These log the full error server-side but return only a generic message to clients — preventing internal details from leaking.

// JSON endpoint — 3 args: error, context, label
return safeErrorResponse(err, context, "MyEndpoint error");

// SSE stream — 3 args: error, context, label — returns a safe string
const safeMsg = safeStreamError(err, context, "Stream error");

Rate Limiting​

Apply checkRateLimit() at the top of every public handler. Pass the request object directly — the limiter extracts the client identifier from SWA principal headers or falls back to IP headers:

const rateCheck = checkRateLimit(request);
if (!rateCheck.allowed) {
return rateLimitResponse(rateCheck.retryAfterMs!);
}

Limits: 30 requests per minute per client, 300 per minute global backstop.

Content Safety​

For endpoints that accept user-generated text, run checkContentSafety() before processing:

const safetyResult = await checkContentSafety(body.message);
if (!safetyResult.safe) {
return { status: 400, jsonBody: { error: safetyResult.error } };
}

Key Files​

FilePurpose
packages/web/api/src/functions/converse.tsMain LLM conversation endpoint with SSE streaming and tool calling
packages/web/api/src/functions/generate.tsCode generation endpoint (Codex) with SSE streaming
packages/web/api/src/functions/health.tsMinimal health check — good template for simple endpoints
packages/web/api/src/functions/action.tsA2UI action event processing (JSON only)
packages/web/api/src/lib/openai-client.tsAzure OpenAI client with streaming async generator
packages/web/api/src/lib/session-store.tsIn-memory session management with TTL
packages/web/api/src/lib/error-response.tssafeErrorResponse() and safeStreamError() utilities
packages/web/api/src/lib/rate-limiter.tsSliding-window rate limiter (per-client + global backstop)