Diese Seite erscheint auf Englisch und Ukrainisch; unten steht die englische Fassung.
Worked example: your API, from the endpoint to the screen
For developers, in code: building an endpoint the assistant calls, describing it as a tool and an event in the panel, and drawing what it returns on your own page — the plan grid and the shop catalogue on our site, step by step, with the choices that make it work.
Two demos on this site run on exactly what this page describes. Ask the assistant «which plan is the cheapest for three sites?» and a window opens beside the answer with the plans that fit. On the page for shops, ask for «a red dress for a wedding, under 2,000» and the dresses appear in the page’s own storefront. Neither is special code: each is one endpoint, one tool, one event and one listener. Here they are in the order you would build your own.
Custom tools and events are part of the Growth plan and above.
1. The endpoint
The assistant calls an ordinary HTTP endpoint that answers JSON. Ours for the plans is
GET /api/public/plans/ on our panel; it answers:
{
"currency": "USD",
"plans": [
{
"code": "growth",
"name": "Growth",
"price_usd": "29.00",
"first_month_price_usd": null,
"credits": 7000,
"sites": 3,
"pages_per_site": 2000,
"history_days": 90,
"advanced_tools": true
}
]
}
Five choices make an endpoint easy for the assistant to use.
Optional filters the model fills from the question. Every parameter of the plan grid is optional, and they combine:
| The visitor asks | The assistant calls |
|---|---|
| «What plans are there?» | /api/public/plans/ |
| «Which is the cheapest?» | ?cheapest=true |
| «The cheapest one for three sites» | ?min_sites=3&cheapest=true |
| «Something under $50» | ?max_price=50 |
| «I run a shop and want my own API» | ?tools=true |
| «Tell me about Pro» | ?plan=pro |
A value your endpoint cannot read is better ignored than refused: max_price=cheap returns the
whole grid rather than a 400, because the caller is a model filling a URL, and a full answer is
more useful to it than an error.
Filters with listed values for a big catalogue. The shop’s endpoint takes colour, size,
occasion, maker, max_price and a free query — all optional, but a call with none of them
is refused, so every call is a search and a catalogue of thousands is never sent whole. The tool
description lists the values each filter accepts and how the visitor’s words map onto them:
«light» is ivory and pink, «large» is L and XL, «a party» is evening and cocktail.
The model fills the filters from those lists in any language, and a value that is not on a list
is one the shop does not stock. Several values of one filter mean «any of them».
Say how close each result is. Every product carries fits (it matches everything asked)
and differs (the filters it misses). When nothing fits, the endpoint returns the nearest
dresses instead — the ones that miss the fewest filters, the size counting most — and the
assistant says there is no exact match and names how each one differs. Nothing close at all is
an empty list.
The results as one list under one key. plans, products: the tool’s Response path
trims the answer to that list, and that list is what the event carries to your page.
Field names and units that explain themselves. price_usd is US dollars a month,
pages_per_site counts pages across the whole account, a product’s price is whole hryvnias. Say
the same in the tool’s response description (step 2) — the model reads that description before
it reads the data.
Cache-Control: max-age when the data changes slowly. The plan grid answers
Cache-Control: public, max-age=300, so for five minutes the assistant reuses the response
instead of calling again. A balance or an order status sends no such header and is called every
time. Response caching.
A small version of the plan grid, in Node with Express:
app.get("/plans", (req, res) => {
const { cheapest, max_price, min_sites } = req.query;
let plans = PLANS;
const budget = Number(max_price);
if (Number.isFinite(budget)) plans = plans.filter((p) => p.price_usd <= budget);
const sites = Number(min_sites);
if (Number.isFinite(sites)) plans = plans.filter((p) => p.sites >= sites);
if (cheapest === "true" && plans.length) {
const lowest = Math.min(...plans.map((p) => p.price_usd));
plans = plans.filter((p) => p.price_usd === lowest);
}
res.set("Cache-Control", "public, max-age=300").json({ currency: "USD", plans });
});
The address must be https on a public host; redirects are not followed.
When the endpoint needs a token
The plan grid is public on purpose — the same prices are on our pricing page. An endpoint that
is not public takes its token from a secret: save the value under Secrets on the tool’s
form and write Authorization: Bearer {{secret.api_token}} as a header. The value is encrypted,
never shown again and never seen by the model. Keep tokens out of the URL you type into the form;
a header is where your API’s logs expect them. Secrets.
When the data belongs to the visitor
«Where is my order?» needs the signed-in visitor’s own data. Your page hands the widget that
visitor’s token with Shukai.setUser(token) after login, and the tool sends it as
Authorization: Bearer {{user}}. Your endpoint then answers for that person exactly as it would
for your own app. A 401 or 403 is reported to the assistant as «the visitor appears to be signed
out», and it says so. User identity.
2. The tool
In the panel: Tools → New tool. The plan grid as we set it up:
| Field | Value |
|---|---|
| Display name | Plan grid |
| Name | subscription_search |
| When to use it | below |
| Method · URL | GET · https://api.example.com/plans |
| Parameters | plan, cheapest, max_price, min_sites, min_pages, min_credits, tools — all optional, in the query |
| What the response means | below |
| Response path | plans |
| Emits event | subscription_lookup (step 3) |
When to use it is the only thing the model reads to decide whether to call. Write it as the situations, in the visitor’s words, and say what each one means for the parameters:
The live plan grid — the source of truth for plans and prices. Call it for any
question about plans, prices, credits or which plan suits the visitor; never
answer prices from memory. The page shows the visitor exactly the plans this call
returns, so filter to what they asked: «what plans are there?» → no arguments;
«the cheapest» → cheapest=true; «for three sites» → min_sites=3; «under $50» →
max_price=50; «a shop», «my own API» → tools=true.
Two sentences in it do most of the work: «never answer prices from memory» sends every price question through your data, and «the page shows exactly what this call returns» is why the model narrows the call instead of fetching everything.
Each parameter gets a type and one line for the model: cheapest is a boolean, «true: of the
plans that fit, only the lowest price»; min_sites an integer, «how many sites the visitor
needs». The shop’s size is an array, «XS, S, M, L, XL — «small» is XS and S, «large» is L
and XL»; its query a string, «the visitor’s whole sentence, for anything the filters leave
out».
What the response means names every field the model will see, with its unit:
Returns {currency, plans}; each plan has code, name, price_usd (US dollars a month,
as a string), first_month_price_usd (the first month's price, or null when there is
no offer), credits (a month), sites, pages_across_sites (pages across all the
account's sites together), history_days (how long conversations are kept) and advanced_tools
(true when the plan includes your own API tools, events and page actions).
A field the description leaves out is a field the model has to guess about; one it names is one the model quotes correctly.
Language. The plan grid speaks no language, so Languages the API supports stays empty and
no language parameter is sent. The shop’s catalogue answers in English and Ukrainian: it lists
both and names lang as its Language param — the runtime fills it with the visitor’s
language, and the model never chooses it. Both are under Advanced, beside Cache answers,
which stays off for live data like these.
Then Save, and in Test press Run with cheapest=true: the panel makes the real call
and shows the raw response. That is the moment to check the response path and the field names.
3. The event
The tool brings data back to the assistant. An event brings it to your page. Tools → New event:
| Field | Plans | Shop |
|---|---|---|
| Name | subscription_lookup |
show_products |
| Payload fields | plans, an array, required |
products, an array, required |
| Don’t suggest opening a page | off | on |
| Reply briefly instead of in full | off | off |
Then, back on the tool, set Emits event to it. A bound event is sent by the runtime itself on
every non-empty result, with the whole list in its array field — the model does not decide
it, so the page draws every result, every time. Custom events.
The two switches are a choice about where the answer is:
- Reply briefly instead of in full — when your page’s own interface is the whole answer: the assistant says «found four — here they are» and stops, since a written list of what is already on screen would only repeat it.
- Leave it off when the answer has something to say that the page cannot. «Which is the cheapest?» is answered in words, and the plans window beside it shows the grid. The shop keeps it off too: when no dress fits exactly, the answer has to say so and name how the nearest ones differ, and a brief reply would call them a match.
- Don’t suggest opening a page — when the result on screen replaces a page the assistant would otherwise offer. On for the shop: the dresses are here, a link to the catalogue page adds nothing. Off for the plans: the window carries its own link to the pricing page, and «take me to pricing» still goes there.
4. The listener
One listener on your page, on every page the event can fire on. Ours sits in the site’s layout:
window.addEventListener("shukai:action:subscription_lookup", (event) => {
const plans = event.detail.payload.plans;
if (!Array.isArray(plans) || plans.length === 0) return;
showPlansWindow(plans);
});
event.detail is { type, payload }, and payload carries the fields you declared. Three habits
from our own listener:
- Draw with your own components, in your page’s look — the window on this site uses the pricing page’s own card style and words.
- Treat the payload as data. Our window escapes every value before it goes into the markup and builds its links itself: the link to the pricing page comes from the site, never from the payload.
- Close on Escape and on a click outside, and keep the widget visible: the visitor goes on talking while they look.
5. Plans and limits
On Growth and above a site may have 6, 8 or 11 tools available on every screen (Growth, Pro, Business) and many more bound to particular screens. Up to 3 tool calls run in one answer, each within its timeout of at most 10 seconds. The full table is in Custom tools.
The checklist
- One endpoint, https, JSON, a list under one key.
- Optional filters; for a big catalogue, listed values the model maps words onto, and at least one filter in every call.
- When to use it in the visitor’s words, mapped to the parameters.
- What the response means naming every field and unit.
- Response path to the list; Emits event to an event with one
arrayfield. - Reply briefly when the page shows the answer; Don’t suggest opening a page when the result replaces the page.
Cache-Control: max-ageon data that changes slowly.- Tokens as
{{secret.*}}in a header; the visitor’s own data through{{user}}. - Save, then Test → Run with the questions you expect.