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.
| Method | Path | Shape | Status |
|---|---|---|---|
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}/bots | the 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}/config | the 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}/knowledge | POST {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}/bookings | POST 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}/products | the 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}/orders | POST 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/credentials | verified against LINE /bot/info before it is stored | live |
DELETE | /workspaces/{id}/line/credentials | disconnects 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 | /events | POST {type,botInstanceId?,props?}; type must be a known one | live |
GET | /events/summary?days=30 | → {total,byType,byDay} | live |
GET/POST | /leads | POST {name,contact,botInstanceId?,note?} → state new | live |
GET/PATCH | /leads/{id} | PATCH {status} through new → contacted → qualified → won/lost | live |
GET | /industries | mirrors /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.