API reference

Base path: /api/ai/ on the swapps.com zone (swapps.com/api/ai/* and www.swapps.com/api/ai/*). All POST bodies must be JSON with Content-Type: application/json. All responses are JSON with Cache-Control: no-store.

Method Path Purpose
POST /api/ai/recommend-plan Recommend one Swapps path with rationale and alternatives.
POST /api/ai/refine Refine free text into a categorized breakdown + field classification.
GET /api/ai/search-test Direct AI Search probe.

Method / path rules

  • POST is required on recommend-plan and refine; GET on search-test.
  • A wrong method on a known path returns 405 with an Allow header (POST, OPTIONS or GET, OPTIONS).
  • Unknown paths under /api/ai/* return 404.
  • OPTIONS returns 204 with CORS preflight headers. If Access-Control-Request-Method is present and is not POST, preflight returns 405.

Error shape

All errors use a single envelope:

{ "error": { "code": "invalid_request", "message": "message is required and must be a string." } }
Code HTTP When
invalid_request 400 Bad Content-Type, malformed JSON, or a validation failure.
method_not_allowed 405 Wrong method on a known path.
not_found 404 Unknown path under /api/ai/*.
ai_request_failed 502 The Workers AI call threw or returned an unusable shape.

POST /api/ai/recommend-plan

Recommends exactly one Swapps path (Build / Launch / Scale / Custom / Technical Review), injects a controlled per-path plan info block, and explains the choice with concrete signals.

Request

{
  "message": "I need a client portal with login, payments and an admin dashboard",
  "locale": "en",
  "source": "homepage-hero",
  "pageUrl": "https://swapps.com/",
  "fields": {
    "initialChoice": "create",
    "service": "build-application",
    "goal": "Internal operations",
    "industry": "finance",
    "audience": "b2b",
    "style": "modernMinimalist",
    "features": ["Auth", "Payments", "Admin dashboard"]
  }
}
Field Type Required Notes
message string yes 1–2000 chars after trim.
locale string no en (default) or es; resolved via locale normalization (es-COes). Anything else falls back to en. ≤ 16 chars.
source string no Free-text metadata. ≤ 64 chars.
pageUrl string no Free-text metadata. ≤ 512 chars.
fields object no Structured input; every sub-field is independently optional. Treated as confirmed facts by the model.

fields sub-fields (all optional): initialChoice, service, goal, industry, audience, style are strings (each ≤ 120 chars); features is an array of strings (each ≤ 120 chars, max 30 items). Present fields are passed to the model as a "treat as confirmed" block.

Response (200)

{
  "summary": "Client portal with login, payments, and an admin dashboard.",
  "recommendedPath": "Scale",
  "plan": {
    "name": "Scale",
    "description": "Engineering depth for products with integrations, complex workflows, operational tooling…",
    "bestFor": "Products requiring third-party integrations, complex business workflows, dashboards, automation…",
    "typicalIncludes": [
      "Multiple third-party integrations",
      "Complex workflows and business rules",
      "Operational dashboards and reporting",
      "Automation and background jobs",
      "Performance, observability, and scaling"
    ]
  },
  "rationale": {
    "summary": "Scale fits because the request combines authentication, payments, an admin dashboard, and likely integrations.",
    "signals": [
      "User mentioned authentication ('login')",
      "User mentioned payments",
      "User mentioned an admin dashboard"
    ]
  },
  "alternatives": [
    { "path": "Launch", "whenItWouldFit": "If integrations and dashboards were dropped from scope." },
    { "path": "Custom", "whenItWouldFit": "If the program required enterprise compliance or a dedicated discovery phase." }
  ],
  "detectedNeeds": ["Authentication", "Payments", "Admin dashboard"],
  "missingInformation": ["Payment provider", "Timeline", "Audience size"],
  "ctaLabel": "Request a Technical Review",
  "ctaUrl": "https://swapps.com/get-swapps/",
  "confidence": "medium",
  "language": "en"
}
Field Type Notes
summary string One short sentence restating the user's request.
recommendedPath enum One of Build / Launch / Scale / Custom / Technical Review.
plan object Server-injected from a controlled table keyed by path + language. { name, description, bestFor, typicalIncludes[] }. The model never writes this.
rationale.summary string One short paragraph (≤ 400 chars) explaining the choice for this user.
rationale.signals string[] 2–6 evidence items (each ≤ 240 chars), each traceable to the user's words. Placeholders are stripped server-side.
alternatives array Up to 2 entries { path, whenItWouldFit }. Duplicates of the primary path, unknown paths, and placeholder text are dropped server-side.
detectedNeeds string[] Short noun phrases the user expressed.
missingInformation string[] Short noun phrases of useful context that is absent.
ctaLabel / ctaUrl string CTA copy; defaults to "Request a Technical Review" / https://swapps.com/get-swapps/.
confidence enum high (explicit matching features), medium, or low (Technical Review / weak signals).
language enum Echoes the resolved locale (en or es).

Fallback behavior

When the model output is unparseable (after trying raw, fenced, and embedded JSON), the endpoint returns a Technical Review recommendation in the requested language with populated plan info, empty alternatives and rationale.signals, a generic missingInformation list, and confidence: "low" — still HTTP 200.

Errors

400 invalid_request for a non-object body, missing/empty/oversize message, malformed JSON, non-application/json Content-Type, or any field exceeding its limit. 502 ai_request_failed when the Workers AI call throws.


POST /api/ai/refine

Refines a user's free-text project description into a structured, categorized breakdown aligned with the /get-swapps/ wizard. Intended for an "Improve with AI" affordance next to the custom-text textarea in swapps-client. Output is itemized — no long paragraphs.

Request

{ "message": "need site, people sign up pay dashboard, company sells courses", "locale": "en" }
Field Type Required Notes
message string yes 1–2000 chars after trim.
locale string no en (default) or es; es-COes; otherwise falls back to en. ≤ 16 chars.

Response (200)

{
  "summary": "Website for selling online courses with sign-up, payments, and a dashboard.",
  "sections": [
    { "key": "service", "items": ["Website"] },
    { "key": "features", "items": ["User sign-up", "Payments", "Dashboard"] },
    { "key": "context", "items": ["Company sells courses online"] }
  ],
  "suggestions": [
    "What kind of courses?",
    "Preferred payment provider?",
    "Do you have a budget range?"
  ],
  "language": "en",
  "fields": {
    "initialChoice": "create",
    "service": "build-website",
    "industry": "education",
    "audience": null,
    "style": null
  }
}
Field Type Notes
summary string One short sentence describing the request. Truncated to 320 chars server-side. Only facts from the user.
sections array Ordered { key, items }. A section is present only when the user touched that dimension; empty sections are dropped.
suggestions string[] Up to 5 short hints (each ≤ 160 chars) about missing information. Empty when nothing is missing.
language enum Echoes the resolved locale (en or es).
fields object Classification into the /get-swapps/ enums. Every key is present; null when the text doesn't clearly indicate a value.

Allowed section keys (de-duped, kept in this canonical display order; unknown keys dropped): service, goal, industry, features, audience, style, constraints, context. Each section's items are short noun phrases / fragments, truncated to 280 chars each and capped at 10 items per section; placeholder items (None mentioned, N/A, TBD, , Spanish variants, etc.) are stripped.

fields ids are restricted to the mirrored client enums; any value not in the allowed list becomes null:

  • initialChoice: help | create | advice
  • service: development-support, infrastructure-scale, seo-services, maintenance-plans, workflow-automation, build-website, build-application, strategic-consultation, technical-advisory, dedicated-team
  • industry: technology, healthcare, finance, education, retail, manufacturing, realEstate, hospitality, nonprofit, other
  • audience: b2b, b2c, youngAudience, specializedProfessionals, generalAudience
  • style: modernMinimalist, boldColorful, elegantSophisticated, funCreative, professionalCorporate

Service / choice safety net

A service id that contradicts the chosen initialChoice (e.g. help with build-website) is dropped to null server-side.

Fallback behavior

On unparseable model output, the endpoint returns { summary: <original message>, sections: [], suggestions: [], fields: <all nulls> } so the caller never sees a broken state.

Errors

400 invalid_request for a non-object body, missing/empty/oversize message, malformed JSON, or non-application/json Content-Type. 502 ai_request_failed when the Workers AI call throws.


GET /api/ai/search-test

Direct AI Search probe — returns the top chunks indexed in swapps-ai-search. Useful to confirm the index is populated.

Request

Query parameter q is required (trimmed, truncated to 500 chars):

curl "https://swapps.com/api/ai/search-test?q=plans"

Response (200)

{
  "query": "plans",
  "count": 1,
  "results": [
    { "source": "https://swapps.com/plans", "text": "…", "score": 0.91 }
  ]
}
Field Type Notes
query string The (trimmed) query that was searched.
count number Number of results returned (0 when the index is empty).
results array { source, text, score? } per chunk. text truncated to 800 chars; source resolved from chunk metadata (urlsourcetitleitem.key → fallback).

An empty index returns count: 0 and does not fail the request.

Errors

400 invalid_request when q is missing or empty.


Limits

Field Limit
message 1–2000 chars (after trim)
source ≤ 64 chars
locale ≤ 16 chars
pageUrl ≤ 512 chars
fields.* strings ≤ 120 chars each
fields.features ≤ 30 items, each ≤ 120 chars
search-test q truncated to 500 chars

CORS

Access-Control-Allow-Origin is echoed only for these origins; others are served without the header (no wildcard):

  • https://swapps.com
  • https://www.swapps.com
  • http://localhost:5173
  • http://localhost:3000

Calling from the frontend

Because the Worker is on the same zone, call the relative path with fetch — no tokens in the browser:

export async function recommendSwappsPlan(message: string) {
  const r = await fetch("/api/ai/recommend-plan", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message }),
  });
  if (!r.ok) throw new Error("Could not get AI recommendation");
  return r.json();
}