API (for developers)

The routes the marketplace front-end calls. Paths are mapped onto the engine in one table — /kit/api.js → ROUTES — so a site never depends on where a route actually lives.

Conventions

JSON in and out. Authorization: Bearer <JWT>. Errors are { "error": { "code", "message" } } with a matching HTTP status.

Base URL is /api/v1 on this origin, because pages are served with connect-src 'self'. Moving the API to another host means widening that CSP in server.js at the same time, or every call fails silently in the browser.

Status column

live — the engine serves it today and these pages use it.

differs — the route answers, but not in a form this marketplace can use. /chat is the important one: the engine’s conversation demo works well, but its twenty industry presets are written in Japanese with no language parameter, and it returns a different message shape. An English site answering a visitor in Japanese is worse than a scripted English demo, so the demos stay scripted. /industries returns the engine’s own twenty Japanese templates, which are a different set from the twenty here — the mapping is the engine_key field in each industry record.

mock — the marketplace calls it and the engine has not shipped it. Both non-live states throw a clear 501 in live mode rather than a confusing 404 or, worse, a plausible answer in the wrong language.

MethodPathShapeStatus
POST/auth/signup{name,email,password} → {token,user,workspace} — creates a workspace too live
POST/auth/login, /auth/login/2fa{email,password,otp?} → {token,user} live
GET/me→ {user, workspaces[]} live
GET/bots→ {bots[]} — the catalogue, no auth needed live
GET/bots/{slug}→ {bot} with its plans from plans.json live
GET/workspaces→ {items[]} — yours, or all of them for platform staff live
POST/workspaces{id,name,plan} → {workspace} — platform staff only live
GET/workspaces/{id}→ {workspace} live
PATCH/workspaces/{id}{name?,brand?,business?,line?} — partial; omitted fields keep their value live
GET/POST/workspaces/{id}/botsthe wizard adds a bot here; re-adding returns the existing one live
GET/bot-instances?status=→ {items[],total,limit,offset} live
POST/bot-instances{botSlug,name?,industrySlug?,planId?} → a draft instance live
GET/PATCH/bot-instances/{id}PATCH merges into config rather than replacing it live
GET/PUT/bot-instances/{id}/configthe builder autosaves here; PUT is idempotent and refuses an empty body live
POST/bot-instances/{id}/publish→ live, or 409 {blockers[]} if LINE is not connected live
POST/bot-instances/{id}/pause→ paused live
GET/bot-instances/{id}/kpis?range=30d→ {empty,tiles[],series[],recent[],usage} counted from /events live
GET/POST/bot-instances/{id}/knowledgePOST {items[]|text|urls[]}; refused URLs come back with a reason live
GET/bot-instances/{id}/knowledge/search?q=→ {hits[],answerable} — says when it cannot answer live
DELETE/bot-instances/{id}/knowledge/{itemId}removes one item live
GET/bot-instances/{id}/bookings/availability?date=→ every slot with its remaining places and, when closed, why live
GET/POST/bot-instances/{id}/bookingsPOST refuses a full slot with 409 and offers free times live
PATCH/bot-instances/{id}/bookings/{bookingId}confirmed → cancelled / done / no_show live
GET/POST/bot-instances/{id}/productsthe catalogue; DELETE hides a product that has order history live
GET/PATCH/DELETE/bot-instances/{id}/products/{productId}partial edits; variant stock is per SKU live
GET/POST/bot-instances/{id}/ordersPOST takes {items:[{id,sku,qty}]}; every price comes from the catalogue live
PATCH/bot-instances/{id}/orders/{orderId}status moves are checked; cancelling returns the stock live
GET/workspaces/{id}/line/status→ {connected,displayName,basicId,plan,…}; secrets are masked live
POST/workspaces/{id}/line/credentialsverified against LINE /bot/info before it is stored live
DELETE/workspaces/{id}/line/credentialsdisconnects and pauses every live bot in the workspace live
GET/workspaces/{id}/line/connect-url→ {available:false, why, fallback} until LINE approves us live
DELETE/bot-instances/{id}archives; the record and its config stay readable live
GET/billing→ {workspacePlan,subscriptions[],monthlyTotal} live
POST/billing/subscribe{botInstanceId,planId?} → a trialing subscription live
POST/billing/cancel{subscriptionId} → canceled live
GET/billing/invoices→ {items[]} live
GET/POST/eventsPOST {type,botInstanceId?,props?}; type must be a known one live
GET/events/summary?days=30→ {total,byType,byDay} live
GET/POST/leadsPOST {name,contact,botInstanceId?,note?} → state new live
GET/PATCH/leads/{id}PATCH {status} through new → contacted → qualified → won/lost live
GET/industriesmirrors /data/industries.json differs
POST/chat{session_id,industry,bot?,instance_id?,message?,postback?} → {messages[],events[],log[],handoff?} differs
PUT/workspaces/{id}/line/richmenu{cells[],layout} → {richmenu_id,image_url} mock
POST/billing/checkout{plan,bots[],options[]} → {url} — needs Stripe mock

Mock mode

Append ?mock=1 to any page to force it, or ?mock=0 to force the real engine. In mock mode every GET resolves from /mock/** and /chat is answered by the scripted engine, so the whole site works with no backend at all.

Webhooks

POST /webhook/line and /webhook/instagram are engine-internal. Signature is verified before processing; front-end code never touches them.