Endpoints: 28,729MCP servers: 18,413Payout addresses: 2,071Paid calls: 1,551Letters: 14Defects: 1,323counted 4 min ago
teppi

Server definition

Hash
sha256:510a1f1cc54055e31e42a6570b90fdc81ef945f9d8573776b9b4bb885d5b9e31
What it is
What a remote MCP server returned when asked what it offers: 37 tools

The blob, as servednamed by its sha256

{ "instructions": "WHICH TOOLS TO REACH FOR\n• Who am I / which company / what do Zooza's words mean → whoami, get_terminology, negotiate_terminology, explain_data_model\n• What we offer — programmes, classes, schedules, venues, billing periods (term blocks) → classes_* (resolve ids first: classes_find_courses for a PROGRAMME → course_id; classes_find_classes for a CLASS/group by name → schedule_id; VENUES, BILLING PERIODS (term blocks), TRAINERS and trainer PAY RATES all resolve through classes_find_resource with kind: place|billing_period|trainer|trainer_rate_type; NEW programme (genuinely new product/offering, not a rerun of an existing one) → classes_add_course, then a class inside it via the create flow below; create flow: classes_preview_schedule → classes_preview_events → classes_commit_class; edit an existing class — name/price/capacity/make-up extra capacity (\"počet miest navyše pre náhradné hodiny\" = the extra_capacity/extra_capacity_usage fields, NOT registrations_cap, which caps the NUMBER OF REGISTRATIONS for multi-seat bookings)/billing period, or instructor/venue/duration/rate: classes_update, called TWICE — first without a token to preview, then again with the token and confirmed:true to apply (to change trainer PAY RATE resolve trainer_rate_type_id via classes_find_resource kind:\"trainer_rate_type\" first). For instructor/venue/duration/rate you MUST set session_scope: 'upcoming' (usual) / 'all' / 'class_only' — 'class_only' re-advertises the class but LEAVES existing sessions unchanged, so default operators toward upcoming/all; changing PROGRAMME settings — pricing, online booking, make-ups, trial, auto-enrolment, attendance, feedback, archive: resolve course_id with classes_find_courses first, then classes_update_course_settings WITHOUT a token → show the diff + warnings, get confirmation → call it again with the token and confirmed:true)\n• This week's sessions, attendance, session notes → sessions_* (resolve event ids first: sessions_find_events — with NO filters it returns sessions from today onward; pass a schedule_id to get a class's FULL history incl. past, or from/to for a window — there is no past flag); edit SPECIFIC sessions — reschedule a date/time or change a hand-picked session's instructor/venue/block: sessions_update with event_ids+changes; ADD new sessions to a class (\"add one more at the end\", a make-up date) → sessions_update with schedule_id+sessions (resolve the last session via sessions_find_events for the next date). Either mode is called TWICE — first without a token to preview, then again with the token and confirmed:true to apply (set notify:true on the FIRST call to email clients). To change an attribute across ALL/upcoming sessions of a class in one action, use classes_update with session_scope instead; to CANCEL sessions (a session that will not take place — \"the pool is closed Friday\", \"Martina is ill all week\") → sessions_cancel, scoped to ONE entity per call (event_ids, one date, one trainer_id+range, one schedule_id+range, or one place_id+date) and also dual-phase; it needs an internal reason, and a public_reason whenever notify is on. Cancelling a SESSION is not the same as cancelling one CLIENT's booking on it (sessions_mark_attendance with attendance:'canceled' — that is the path that issues make-up credits)\n• Who is enrolled / who hasn't paid / who's on the waiting list / find a client → bookings_find (filter by schedule_id, course_id, name/email, user_id, status; payment_status:[\"unpaid\",\"partially_paid\"] for the unpaid roster; status:[\"waitlist\"] for the waiting list — it is EXCLUDED by default, so pass it explicitly; created_from/created_to (YYYY-MM-DD) for new sign-ups, e.g. this week's registrations; distinct:true to collapse to one row per client → user_id). Yields registration_id / user_id to chain into comms.\n• Put a client into a DIFFERENT class — \"also add her to Tuesday\" (copy, original stays) or \"move him to Wednesday\" (move, booking relocates with its debt) → bookings_copy_booking, called TWICE: first without a token to preview both class prices, what the client owes, free places and any blockers, then again with the token and confirmed:true to apply. payments has NO default — ask the operator whether to price it from the target class or leave payments alone. Every figure in the preview's money block is a TOTAL for the whole booking (the per-session figure is separate, target_unit_price_per_session); quote target_will_owe_total, which already includes the target class's registration fee. Cannot do instalment plans, term-block picking, or a custom amount owed: send the operator to the Copy/Transfer wizard in the Zooza app for those\n• ADDITIONAL lecturers — a second instructor / assistant / helper working ALONGSIDE the main instructor (\"put Martin on Mondays and Peter on Tuesdays\", \"add an assistant to the Junior classes\", \"who is helping on Wednesday?\") → trainers_add_helpers to CHANGE them; sessions_find_events / classes_find_classes to SEE them. Two levels, and they are NOT the same fact: a class carries an eligibility ROSTER (who may work it) and each session carries the ACTUAL assignment. Registering someone on a class normally means they work EVERY session — restricting them to weekdays is the exception. So a lecturer with no weekdays needs an explicit session_scope (upcoming/all/class_only); one with weekdays only touches those days. Reading: a session's additional_trainers = who works it, class_additional_trainers = who is merely eligible; never answer \"who is helping on Wednesday\" from the roster. Roles are per CLASS and cannot vary by day: secondary (\"Secondary instructor\", default), assistant (\"Assistant\"), helper (\"Assistant instructor\"), trainer (\"Instructor\"). Called TWICE — first without a token to preview every class + session count, then again with the token and confirmed:true. This is NOT how you change the MAIN instructor (classes_update, or sessions_update for a one-off substitution)\n• Trainers / instructors → classes_find_resource kind:\"trainer\" (virtual placeholder trainers included — use one for \"TBD\"/\"unassigned\"/\"guest\"); trainer PAY RATES → classes_find_resource kind:\"trainer_rate_type\" (the ONLY way to resolve a named rate like \"hourly\"/\"per class\" → trainer_rate_type_id for classes_/sessions_ edits — never guess that id)\n• Messaging clients — templates, merge variables, sending email → comms_* (comms_list_templates for what exists, comms_list_merge_vars for *|TAGS|*; resolve specific recipients with bookings_find → registration_id / user_id, and pass the whole LIST of registration_ids as audience.registration_id to message an ad-hoc cohort (e.g. the unpaid roster) without a saved segment; for a company-wide blast use audience.whole_company:true — active_only defaults true (registered bookings), ASK the operator before setting active_only:false since that also emails cancelled/inactive clients; send flow: comms_send_message WITHOUT a token → show the plan, get explicit confirmation → call it again with the token and confirmed:true; a large send comes back requires_second_confirmation — show the recipient COUNT, get a separate yes, then call once more adding confirm_large_send:true)\n• Putting a BOOKING on a payment plan → payments_add_plan. A plan on a programme or class is NOT inherited by bookings — until it is applied per booking the client owes nothing and sees no schedule. Pass total_price as the WHOLE amount for that booking (opposite of unit_price on classes_add_course, which is per session); omit it to price from the class. Two calls: without a token to see the actual instalment dates+amounts, then again with the token and confirmed:true\n• WRONG payment plans on a programme (Zooza auto-attaches templates when price type or payment collection changes, sometimes dozens) → setup_update_course_templates: pass the COMPLETE list of template ids the programme should keep and everything else is detached; empty array detaches all. Two calls, preview then token+confirmed:true\n• PRICE: Zooza charges PER SESSION, but operators quote TOTALS (\"300 for the term\"). Always pass what they said as total_price on classes_add_course — do NOT divide it yourself and do NOT put a total in unit_price. The session count does not exist yet at that point; classes_commit_class divides the total once the sessions are real. unit_price is ONLY for a per-session figure the operator actually quoted. Sending both is refused. And if they say \"unit price\" / \"jednotkova cena\", that is AMBIGUOUS — ask \"is that per lesson, or for the whole course?\" before choosing a field; the tool refuses a bare unit_price on instalment programmes for exactly this reason.\n• Setting up INSTALMENT billing → a programme set to instalments bills NOTHING until a payment plan template is attached. setup_add_payment_template creates one (pass course_id to attach it in the same call). The template holds the CADENCE, never the price — \"€200 in 4 × €50\" = programme price 200 + template frequency:\"absolute\", value:4; the 50 is derived, never put it in value\n• Capturing a website/enquiry LEAD as a trackable record → bookings_add_lead (minimal registration on a lead_collection schedule; no customer email, no payment). Trial classes to offer come from classes_find_classes with in_trial:true, active_only:true — each returns a registration_url (the public booking link a prospect clicks).\n• Reading a customer's REPLY to a Zooza email, or triaging replies → comms_find_replies (filter by registration_id / from_email / state unread|todo|resolved; pass mark_reply_id + mark_state to flag a reply todo/resolved). Replies only appear for leads whose email was sent through Zooza tied to that registration.\n• Tagging records / pipeline state — label a lead converted, flag one todo → labels_mark (object_type course|schedule|registration, object_id, label, present:true to attach / false to detach; labels on a SCHEDULE can be customer-visible on the booking widget)\n• Operator TO-DO items — escalate something a human must action → todos_add (message + to_user_id assignee, optionally entity_type+entity_id to link a registration); change a todo's status → todos_mark (open/done/cancelled)\n• Sending feedback or feature requests to Zooza → submit_feedback\nWrites that commit real changes are ALWAYS two steps, and the preview step is not optional. Two shapes exist: (a) DUAL-PHASE tools — classes_update, classes_update_course_settings, sessions_update, comms_send_message — call the SAME tool twice: first without a token to preview, then again with the returned token plus confirmed:true to apply. confirmed:true asserts you SHOWED the user the preview and they approved it, so never set it on a call the user has not seen the preview for. Send nothing but the token and the confirmation flags on the second call. (b) Separate preview/commit tools — e.g. classes_preview_schedule before classes_commit_class. Either way: show the preview, get confirmation, then apply.\n\nSHOWING A CLASS'S SESSIONS (its timetable) — DEFAULT to a weekly GRID, never a flat date list. Any time you display the sessions of a class — a preview, after creating it, when viewing or COPYING an existing class, or a whole-period overview — render a markdown week grid: days across the top (Mon–Sun), time down the left, the class in its slot, exactly like the Zooza app calendar. Show ONE representative week and carry the run range + session count in a one-line caption (one caption per season when comparing an original against a copy). Draw a date list / timeline ONLY if the user explicitly asks to see every individual session date.\n\n---\n\nZOOZA TERMINOLOGY — read before using any tool.\n\nHIERARCHY: Programme → Class → Session → Booking\n Programme = The top-level container that defines an activity type\n Class = A scheduled group within a Programme, typically differentiated by day/time, leve\n Session = A single scheduled meeting within a Class, with a specific date and time\n Booking = A client's formal commitment to attend a Class\n\nTERM RESOLUTION — when a user says X, Zooza means:\n \"termín / hodina / lekcia / lekce / óra / lesson\" → Session\n \"Kurz\" (CZ/SK) / \"Curs\" (RO) / \"Kurzus\" (HU) → Programme (NOT deprecated in those markets)\n \"skupina / Gruppe\" → Class (not a Programme; Class lives inside a Programme)\n \"prihlásenie / prihláška / Buchung\" → Booking/Enrolment\n\nDO NOT CONFUSE:\n Class ≠ Programme (Class lives inside a Programme)\n Session ≠ Class (Session = one meeting; Class = the recurring schedule)\n Transfer ≠ Copy (Transfer moves booking; Copy duplicates it)\n Make-up ≠ Free credits (Make-up = from cancellation; Free credits = granted)\n\n---\n\nThis MCP server exposes Zooza operational tools plus (optionally) a small set of **skills** (playbooks) that teach you how to compose those tools well.\n\n## Session bootstrap (do this before any operational tool)\n1. Call `whoami` once per conversation. The response includes `available_companies` — the Zooza companies this user can operate on.\n2. Pick the company you'll operate against:\n - If `available_companies.length === 1`, you can omit `company_id` from every tool call — the server will default to it. Briefly tell the user which company you're working in.\n - If multiple AND the user has unambiguously named one (e.g. *\"in the Bratislava studio\"*), match by name and pass `company_id` explicitly.\n - If multiple AND the user hasn't specified, **ask them** before any other tool — render the options as a table (`id | name`).\n3. When `available_companies.length > 1`, **every** operational tool call needs an explicit `company_id`. You may use different `company_id` values in the same conversation to operate across companies (e.g. comparisons).\n4. If you already have a `whoami` response in your context from earlier this conversation, don't re-call it.\n\n## Reference tools (call on demand — no API required)\nThese tools return hardcoded Zooza knowledge instantly. Call them when the user asks a direct question or when you need to resolve a value before calling an operational tool.\n- `explain_data_model` — entity hierarchy, valid status values, do-not-confuse rules. Use when the user's request is ambiguous about which entity they mean.\n- `classes_list_schedule_patterns` — valid cadences (weekly/biweekly/monthly/daily), weekday keys (mon/tue…), time_minutes format, payment schedule types. Use when building a class schedule from scratch without a skill.\n- `comms_list_merge_vars` — all valid *|MERGE_VAR|* tags for email/SMS templates. Use when the user asks to write or edit a message template.\n- `get_terminology` — multilingual term lookup (e.g. 'hodina' → Session). Use when the user's language is ambiguous.\nDo NOT call these proactively on every request — only when genuinely needed.\n\n## Available skills\n- `business-model-validator` — Use when a prospect, new customer, or existing operator wants to know whether Zooza fits their business, or which features map to how they operate. Pure knowledge interview — no Zooza account or API access required.\n- `class-management` — Guided flow for creating a new Zooza class — interview the user, accumulate session patterns, commit. Used in conjunction with the classes_preview_schedule, classes_preview_events, and classes_commit_class MCP tools.\n- `communication` — Guided flow for emailing clients — figure out who the message targets, suggest the right template or compose a custom one, preview with real recipient counts, then send after explicit confirmation. Used with the comms_send_message, comms_send_message, comms_list_templates, and comms_list_merge_vars MCP tools.\n- `feedback-nudge` — Use after a write/commit tool succeeds (rate-limited via whoami.last_feedback_at, ~once per week) OR whenever the user explicitly asks to report a bug / file feedback / \"tell the engineering team\". Drafts an anonymized title+body, confirms with the user, then calls submit_feedback.\n- `negotiate-terminology` — Use when the user is new to Zooza MCP, says \"you're using the wrong words\", or runs /zooza-setup. Conducts a short vocabulary interview and saves a personalised terminology profile to Claude memory.\n- `report-compose` — Build a focused, single-question report artifact for an activity-brand client using their REAL data. Use whenever a client asks to see / show / build a report, dashboard, chart, or visual of their business numbers (occupancy, unpaid, churn, attendance, trials, retention, revenue, \"how are we doing\"). The client should feel they created this report — compose it live around their data and brand, one question per page. Never show the full multi-tab dashboard; never invent numbers.\n- `report-discovery` — Routing interview for the Zooza client reports app. Clients rarely ask for a metric by name — they describe a symptom (\"feels like fewer kids coming\", \"money seems tight\"). This skill maps symptom → metric → report page, opens the right page, and handles the case where the data doesn't exist yet. Use whenever an activity-brand operator asks a business question about their numbers, performance, or \"how are we doing\".\n- `report-page-new` — Author one new page in the Zooza client reports artifact (business-dashboard.html) when a client's question has no existing page but the capability manifest says the data exists. Walks the strict recipe — capability check, registry descriptor, render function, landing-menu entry, structural verification. Use when the user asks for a report/chart/view the reports app doesn't have yet.\n- `schedule-optimization` — Guided flow for building a whole weekly timetable OVERVIEW for a new billing period (school year / term). Two modes — optimise from scratch (forecast + constraint solver) or roll over the current period (copy as-is, advance courses by a client progression map, or apply small edits), with a shared validator and repair. Produces a reviewable overview only; a separate tool creates the classes. Uses predict_demand, build_timetable, and the find_* tools.\n\nWhen the user's request matches a skill's purpose, call the `get_skill` tool with the skill name **before** invoking the underlying operational tools — it returns the full playbook with the interview steps, mapping rules, and confirmation flow for that scenario. Skill content is stable within a session; do not re-fetch the same skill more than once per session.\n\n---\n\nREPORTS — composing a custom report a client asks to SEE.\nWhen an activity-brand operator asks to see / show / build a report, dashboard, chart, or\nvisual of their business numbers (occupancy, unpaid, churn, attendance, trials, retention,\nrevenue, \"how are we doing\", per-programme / venue / instructor performance — and make-up /\nreplacement credit pressure: \"unused/expiring make-ups\", \"credits\", \"náhrady\", \"are we\noverloaded on make-ups\" → reports_get_data view=\"replacements\". Zooza HAS make-up credits;\nnever tell a user credits don't exist):\n\n1. Get the skill: get_skill(\"report-compose\") — the playbook for building a focused report\n the client owns. (Vague question → get_skill(\"report-discovery\") to find the view first.)\n2. Get the REAL numbers: reports_get_data (view + optional from/to). Its headline/rows/note\n are the only legitimate source of figures.\n3. Compose a focused, single-question report as an ARTIFACT in the conversation (it\n renders in the side panel), branded as the client's own, charts in inline SVG/CSS\n (no CDN/library). NEVER hand the user a link or open a browser page. One question\n per report — never the full multi-tab dashboard.\n\nHARD RULES:\n• Every figure you show MUST come from reports_get_data verbatim. NEVER invent, estimate,\n or recompute numbers, and never draw a chart before calling it. No data → say so; do not\n fabricate a report.\n• Show only what the client asked. The full multi-tab dashboard\n (artifacts/business-dashboard.html) is an internal EXAMPLE + component library, not the\n client deliverable — compose a focused, single-question report instead.\n• For raw data to REASON over (not show), use the find_*/get_* tools.", "tools": [ { "description": "Create a LEAD — a lightweight registration on a lead-collection schedule — for a prospective customer, from their name and email. Use this to capture an inbound enquiry as a trackable Zooza record you can later label, message, and check for conversion. It does NOT enrol the person in a real class, take payment, or email the customer (the server authenticates with an App-type key, which sends no customer communication). It works ONLY against schedules whose type is `lead_collection`; for a genuine class booking, or anything that should charge or notify the customer, do NOT use this tool. Not idempotent — calling twice creates two leads, so the caller must guard against re-processing the same enquiry. Returns the new registration id (the `order_id` that `comms_find_replies` and `labels_mark` consume downstream).", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Parent programme id. Optional — derived from the schedule when omitted; only pass it to skip the lookup.", "exclusiveMinimum": 0, "type": "integer" }, "email": { "description": "Lead's email. Required — the key downstream conversion detection matches on.", "minLength": 1, "type": "string" }, "first_name": { "description": "Lead's first name. Required.", "minLength": 1, "type": "string" }, "last_name": { "description": "Lead's last name. Required.", "minLength": 1, "type": "string" }, "phone": { "description": "Lead's phone, free-text, if the enquiry included one.", "type": "string" }, "schedule_id": { "description": "The lead-collection schedule (the lead 'pipeline') to attach the lead to. Must be a schedule_type='lead_collection' schedule — the tool refuses any other.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "schedule_id", "first_name", "last_name", "email" ], "type": "object" }, "name": "bookings_add_lead", "outputSchema": null }, { "description": "Copy or move a client's existing booking into a different class. COPY creates a second booking and leaves the original in place — use it when the client is continuing into a new term or adding a class alongside their current one. MOVE relocates the booking itself, carrying its payment history and debt, and the client is no longer in the old class — use it when they are switching.\n\nTWO CALLS. First WITHOUT `token`: returns both class prices, what the client currently owes, free places in the target class, and any blockers, plus a single-use token. Show that to the operator — especially the `money` line — and get explicit approval. Then call again with `token` + `confirmed: true` to apply; send nothing else, the plan is frozen.\n\n`payments` has no default on purpose: pricing the booking from the target class is what operators most often get wrong, so ask rather than assume.\n\nMONEY FIGURES ARE TOTALS. Every amount in the `money` block is for the WHOLE booking, never per session — Zooza's per-session figure is `target_unit_price_per_session`, shown separately, and quoting it as the price is how a booking ends up quoted ~20x too cheap. Read `target_will_owe_total` to the operator: it is the class price PLUS the target class's registration fee, which Zooza charges on top of it.\n\nSIMPLE CASE ONLY. This tool cannot set up instalment plans, pick specific term blocks, or set a custom amount owed. If the operator needs any of those, do not improvise with other tools — tell them to use the Copy/Transfer wizard in the Zooza admin app (open the booking, then Transfer or Copy booking).", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "action": { "description": "Required on the FIRST call. See the tool description for which to pick.", "enum": [ "copy", "move" ], "type": "string" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "payments": { "description": "Required on the FIRST call — NO default, ask the operator. from_target_class = price it by the target class. do_not_change = on copy, no payments on the new booking; on move, leave existing payments untouched (Zooza's help calls this the safest option).", "enum": [ "from_target_class", "do_not_change" ], "type": "string" }, "registration_id": { "description": "Required on the FIRST call. The existing booking. Resolve with bookings_find.", "exclusiveMinimum": 0, "type": "integer" }, "send_confirmation": { "description": "Default false. true emails the client — confirm intent with the operator first.", "type": "boolean" }, "start": { "description": "Enrolment start, YYYY-MM-DD. Default today. Affects price and session count.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "status": { "description": "Omit to use what Zooza computes for the target class (usual choice).", "enum": [ "registered", "waitlist", "late", "trial_started", "trial_not_started" ], "type": "string" }, "target_event_id": { "description": "One-off (single-session) programmes only. Resolve with sessions_find_events.", "exclusiveMinimum": 0, "type": "integer" }, "target_schedule_id": { "description": "Required on the FIRST call. The class it goes into. Resolve with classes_find_classes.", "exclusiveMinimum": 0, "type": "integer" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "bookings_copy_booking", "outputSchema": null }, { "description": "Find this company's bookings — a client's enrolment in a class (registration; \"prihláška\"/\"Buchung\") — and resolve them to a `registration_id`, or a client to a `user_id`. Use for \"is X enrolled?\", \"who's in this class?\", \"who hasn't paid?\" (set `payment_status:[\"unpaid\",\"partially_paid\"]`), and \"find client X\". Filter by `search` (loose: name/email/phone) or `name`, by `course_id`/`schedule_id` (resolve via classes_find_courses / classes_find_classes), `billing_period_id` (a term/season), `user_id`, `registration_id` (one exact booking by its id), `status`, `payment_status`, or booking date with `created_from`/`created_to` (the \"new registrations this week\" lever). `distinct:true` returns one row per client (→ `user_id`) for person lookups. Chain a result's `registration_id` or `user_id` straight into comms_send_message (`audience.registration_id` / `audience.user_id`). Class/programme NAMES aren't returned — resolve the ids via classes_find_* if you need them. Defaults to active enrolments; guest, waitlist, canceled and deleted are excluded unless you pass `status`. Read-only — does not create or change bookings.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "billing_period_id": { "description": "Bookings belonging to this billing period (term/season). Resolve the id with classes_find_resource (kind:'billing_period'); never guess it. To cover several periods, call once per period and merge the ids.", "exclusiveMinimum": 0, "type": "integer" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Bookings in this programme. Resolve the id with classes_find_courses; never guess it.", "exclusiveMinimum": 0, "type": "integer" }, "created_from": { "description": "Only bookings CREATED on/after this date (YYYY-MM-DD, inclusive). The \"new registrations\" lever — e.g. created_from=<Monday> for this week's sign-ups. You supply the literal date; the api does no relative-date parsing.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "created_to": { "description": "Only bookings CREATED on/before this date (YYYY-MM-DD, inclusive). Pair with created_from for a window.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "distinct": { "description": "true → one row per CLIENT (deduped by account-holder user_id), person fields only — use to find a person or resolve a name to a single user_id. Default false → one row per booking.", "type": "boolean" }, "include_inactive": { "description": "Default false. Set true to also include inactive customers.", "type": "boolean" }, "name": { "description": "Enrolled person's name (substring, accent-insensitive). If it draws a blank for a kids' class, try `search` (also matches the account-holder parent).", "type": "string" }, "page": { "description": "0-based page index (default 0).", "minimum": 0, "type": "integer" }, "page_size": { "description": "Number of results per page (max 200).", "minimum": 1, "type": "integer" }, "payment_status": { "description": "Payment state — the \"who hasn't paid\" lever, e.g. [\"unpaid\",\"partially_paid\"].", "items": { "enum": [ "paid", "unpaid", "partially_paid", "overpaid" ], "type": "string" }, "type": "array" }, "registration_id": { "description": "Fetch ONE exact booking by its registration id. Use this to confirm a specific registration exists or read who it is — unlike `search`, which substring-matches the id (search:45 also matches 145, 450). An exact registration_id lookup returns that booking whatever its status (only truly deleted rows are hidden).", "exclusiveMinimum": 0, "type": "integer" }, "schedule_id": { "description": "Bookings in this class (schedule). Resolve the id with classes_find_classes; never guess it.", "exclusiveMinimum": 0, "type": "integer" }, "search": { "description": "Broad freetext: matches the enrolled person's or account holder's name, email, phone, or id (substring, accent-insensitive). Best for a loose term. Use `name` instead to match only the enrolled person's name.", "type": "string" }, "status": { "description": "Enrolment statuses to include (piped to the api). Omit → confirmed enrolments only (registered, late, trial_*); guest, waitlist, canceled and deleted are excluded — pass them to widen. `auto_unenrolled` = canceled by the unpaid automation.", "items": { "enum": [ "registered", "guest", "waitlist", "canceled", "late", "trial_not_started", "trial_started", "trial_ended", "trial_won", "trial_lost", "auto_unenrolled" ], "type": "string" }, "type": "array" }, "user_id": { "description": "All bookings of one client, by their user id.", "exclusiveMinimum": 0, "type": "integer" } }, "type": "object" }, "name": "bookings_find", "outputSchema": null }, { "description": "Create a new programme (course) — the top-level container in Zooza that holds pricing, payment settings, and booking-form configuration. Classes and sessions are added inside it afterwards; a programme cannot accept bookings until it has at least one class. IMPORTANT routing rule: only create a programme for a genuinely NEW product or offering. If the user is re-running an existing programme — new term, new time slot, new venue, new instructor — do NOT create a programme; create a class inside the existing programme instead (classes_preview_schedule → classes_commit_class; the class inherits all programme settings). This tool asks only the essentials; Zooza defaults everything else, and settings can be changed later with classes_update_course_settings. The new programme is created public with online booking enabled. Summarise name, kind, and price to the user and get their OK before calling.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "allow_duplicate_name": { "description": "Default false. Set true ONLY after the user confirms they want a second programme with the same name.", "type": "boolean" }, "audience": { "description": "Default 'groups'. 'individuals' = 1-to-1 programme. Capacity is a class-level concern; nothing is auto-set to 1 here.", "enum": [ "groups", "individuals" ], "type": "string" }, "color": { "description": "Optional admin/calendar colour.", "type": "string" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "for_children": { "description": "Default false (deliberate deviation from the app's default of true). true adds a child profile to the booking form (auto-activates date-of-birth + child-name fields). Ask the user when the vertical suggests kids (baby swim, kids dance, …).", "type": "boolean" }, "name": { "description": "Programme name as clients will see it. Required, non-empty.", "minLength": 1, "type": "string" }, "payment_collection": { "description": "full_duration only. Default 'one_off'.", "enum": [ "one_off", "installments" ], "type": "string" }, "price_type": { "description": "full_duration + installments only. Default 'course_fee'.", "enum": [ "course_fee", "membership" ], "type": "string" }, "programme_kind": { "description": "Default 'full_duration'. 'one_off_event' = single occurrence — lecture, workshop, open day. 'full_duration' = clients book all sessions for the whole period (terms). 'pay_as_you_go' = enrol once, book sessions individually (drop-in).", "enum": [ "one_off_event", "full_duration", "pay_as_you_go" ], "type": "string" }, "registration_fee": { "description": "Default 0.", "minimum": 0, "type": "number" }, "total_price": { "description": "The whole price the client pays for the run — what operators normally quote (\"300 for the term\"). REQUIRED for one_off_event and for full_duration with one_off collection, and the RECOMMENDED input for full_duration with installments too. For instalments Zooza charges per session, so this total is stored on the programme and classes_commit_class divides it by the sessions you create — you do NOT need to know the session count now. Never send both this and unit_price.", "minimum": 0, "type": "number" }, "unit_price": { "description": "Price PER SESSION. Only use this when the operator quoted a per-session figure — for a price covering the whole run use total_price instead, which works for instalment programmes too and is the usual case. REQUIRED for pay_as_you_go. Never send both this and total_price.", "minimum": 0, "type": "number" }, "unit_price_is_per_session": { "description": "Only for instalment programmes, and only alongside unit_price. Asserts you ASKED the operator whether their figure is per session or for the whole run, and they said PER SESSION. \"Unit price\" / \"jednotkova cena\" is ambiguous in everyday speech — never assume it means per session.", "type": "boolean" } }, "required": [ "name" ], "type": "object" }, "name": "classes_add_course", "outputSchema": null }, { "description": "Writes a class to api-v1 in one shot: creates the schedule, attaches any selected payment templates (bundled inline), and posts the assembled events array. Call this only after the user has confirmed the class shell (from `classes_preview_schedule`) and the full event list (accumulated from one or more `classes_preview_events` calls). For lead-collection classes, pass `events: []`. Returns the created schedule's id and url plus the list of created event ids. If api-v1 silently skips any events (a known quirk), the tool surfaces the mismatch as an error so the caller knows the partial state.\n\n`schedule.name` is OPTIONAL — omit unless the user explicitly asked for a custom class name. api-v1 auto-renders `{course_name} {class_name} {session_dates}` end-user-facing when name is absent.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "events": { "description": "The full list of sessions to create, accumulated from one or more classes_preview_events calls. Pass [] for lead_collection classes.", "items": { "additionalProperties": false, "properties": { "date_string": { "description": "Date of this session, YYYY-MM-DD.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "duration": { "description": "Session length in minutes.", "exclusiveMinimum": 0, "type": "integer" }, "time_minutes": { "description": "Session start time as minutes past midnight (0-1439, e.g. 540 = 09:00).", "maximum": 1439, "minimum": 0, "type": "integer" }, "trainer_id": { "description": "Optional per-session instructor override. Resolve with trainers_find; defaults to the schedule's trainer_id when omitted.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "date_string", "time_minutes", "duration" ], "type": "object" }, "type": "array" }, "payment_schedule_template_ids": { "description": "Ids of the payment schedule templates to attach to the class. Omit to attach every template the course offers (the same default classes_preview_schedule marks selected_by_default). Pass the ids explicitly to attach a subset.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "type": "array" }, "schedule": { "additionalProperties": false, "description": "The confirmed class shell (course, venue, trainer, capacity, prices, billing period) as returned by classes_preview_schedule.", "properties": { "all_day": { "description": "When true, the session has no fixed start time (an all-day session).", "type": "boolean" }, "billable_events": { "description": "Number of billable sessions used to compute what clients owe.", "minimum": 0, "type": "number" }, "billing_period_id": { "description": "Term block (billing period) this class belongs to. Resolve with classes_find_billing_periods.", "exclusiveMinimum": 0, "type": "integer" }, "capacity": { "description": "Basic maximum number of seats per session — the ordinary class size.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Parent programme (course) this class belongs to. Resolve with classes_find_courses.", "exclusiveMinimum": 0, "type": "integer" }, "course_name": { "description": "Display name of the parent programme, carried through from classes_preview_schedule for labelling.", "type": "string" }, "duration_minutes": { "description": "Session length in minutes.", "exclusiveMinimum": 0, "type": "integer" }, "name": { "description": "OPTIONAL custom class name — omit unless the user explicitly asked for one. When blank, api-v1 auto-renders `{course_name} {class_name} {session_dates}` for end users.", "type": "string" }, "online_registration": { "description": "Whether clients can self-register for this class online — true publishes it on the public website.", "type": "boolean" }, "place_id": { "description": "Venue (place) where the class runs. Resolve with classes_find_places.", "exclusiveMinimum": 0, "type": "integer" }, "place_name": { "description": "Display name of the venue, carried through from classes_preview_schedule for labelling.", "type": "string" }, "price": { "description": "Total price for the class/period, used when the programme prices by total.", "minimum": 0, "type": "number" }, "registration_fee": { "description": "One-time enrollment fee charged on top of the class price.", "minimum": 0, "type": "number" }, "room_id": { "description": "Room within the venue. `0` = no specific room.", "minimum": 0, "type": "integer" }, "schedule_type": { "description": "What kind of class this is. 'fixed_period' = a real class with concrete dates (sessions get created on commit). 'lead_collection' = interest-gathering placeholder (no events; pass events: []).", "enum": [ "fixed_period", "lead_collection" ], "type": "string" }, "total_price": { "description": "The TOTAL price for the whole run, when the programme is priced in instalments. Pass it through from classes_preview_schedule; unit_price is then derived here as total / billable sessions. Do NOT also pass a non-zero unit_price — the operator quoted one number, not two.", "minimum": 0, "type": "number" }, "trainer_id": { "description": "Instructor assigned to the class. Resolve with trainers_find.", "exclusiveMinimum": 0, "type": "integer" }, "trainer_rate_type_id": { "description": "Trainer PAY-RATE type (what the instructor is paid, not what clients pay). Resolve with trainers_find_rate_types. `0` = none.", "minimum": 0, "type": "integer" }, "unit_price": { "description": "Per-session price, used when the programme prices per session.", "minimum": 0, "type": "number" } }, "required": [ "course_id", "course_name", "place_id", "place_name", "room_id", "trainer_id", "trainer_rate_type_id", "capacity", "duration_minutes", "all_day", "online_registration", "schedule_type", "unit_price", "price", "registration_fee", "billable_events" ], "type": "object" } }, "required": [ "schedule", "events" ], "type": "object" }, "name": "classes_commit_class", "outputSchema": null }, { "description": "Search this company's CLASSES — the scheduled groups inside a programme (a \"class\" / \"group\" / \"skupina\"; internally a *schedule*) — by name (substring) and resolve them to a `schedule_id`. Reach for this whenever the user names a specific group rather than a whole programme (\"the Nejaké class\", \"the Monday 5pm group\", \"her Wednesday ballet class\"), or whenever a downstream tool needs a `schedule_id` — most importantly `comms_send_message` targeting everyone in one class (`audience.schedule_id`). This is the missing middle rung between `classes_find_courses` (finds the PROGRAMME → `course_id`) and `sessions_find_events` (finds individual dated SESSIONS → `event_id`): a class is one recurring group within a programme, made of many sessions. Optionally narrow by `course_id` (classes inside one programme), `trainer_id`, `place_id`, `day` of week, `registration_type`, `in_trial: true` (only classes currently offering a TRIAL), `active_only: true` (exclude classes whose schedule has ENDED), or `lead_only: true` (only lead-collection pipelines). To answer \"the latest classes that actually have sessions\" in ONE call, combine `with_sessions: true` (only classes whose schedule has ≥1 session) with `sort: \"created_desc\"` and a `page_size` — no need to scan `sessions_find_events`. `sort` also takes created_asc / date_asc / date_desc / name_asc / registrations_desc. Returns a slim list — `{schedule_id, name, course_id, start, end, time, trainer_id, trainer_name, place_id, place_name, room_id, duration_minutes, trainer_rate_type_id, capacity, registrations_count, sessions_count, status, in_trial, registration_url, schedule_type}` — enough to disambiguate when several classes share a name, never enough to mutate. When COPYING an existing class, pass its `room_id`, `duration_minutes` and `trainer_rate_type_id` into `classes_preview_schedule` explicitly — otherwise the preview falls back to no room, the 60-minute default and no pay rate. `room_id` 0 = no specific room; `trainer_rate_type_id` 0 = none set (or hidden from your role). `sessions_count` is the class's number of sessions (a stored/materialised count — fine for overview and \"how many\", may lag a very recent edit; chain `sessions_find_events` for an exact live count). `schedule_type` tells a real class (`fixed_period`) from a lead pipeline (`lead_collection`) — use `lead_only: true` to find the pipeline `bookings_add_lead` needs. `registration_url` is the public link a prospect clicks to book that specific class (empty when the class isn't publicly bookable or the company has no registration widget). Combine filters in ONE call — e.g. `{place_id, in_trial: true, active_only: true}` returns the bookable trial classes at a venue in a single query; do not split them across separate calls. `course_id` is returned but not the course name (resolve it with `classes_find_courses` if you need it). `additional_trainers` lists the class's ADDITIONAL lecturers — people who work it alongside the main instructor in `trainer_id`/`trainer_name` — as `{trainer_id, trainer_name, role}`, `[]` when there are none. At class level this is the eligibility roster: it says who may work the class, NOT which sessions each one actually works — for that call `sessions_find_events` with the `schedule_id` and read each session's own `additional_trainers`. Roles render as `secondary` = \"Secondary instructor\", `assistant` = \"Assistant\", `helper` = \"Assistant instructor\", `trainer` = \"Instructor\". To change the roster use `trainers_add_helpers`. By default returns active + paused (inactive) classes; pass `include_archived: true` to search archived classes instead. Does NOT create or change classes (that is `classes_preview_schedule` → `classes_commit_class`) and does NOT list a class's sessions (use `sessions_find_events` with the `schedule_id`).", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "active_only": { "description": "Set true to exclude classes whose schedule has already ENDED (keeps not-yet-started and in-progress classes). Omit to include ended classes too.", "type": "boolean" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Only classes inside this programme. Resolve the course_id first with classes_find_courses; never guess it.", "exclusiveMinimum": 0, "type": "integer" }, "day": { "description": "Day-of-week the class falls on (its start day): 1=Sunday, 2=Monday, … 7=Saturday (MySQL DAYOFWEEK convention).", "maximum": 7, "minimum": 1, "type": "integer" }, "in_trial": { "description": "Set true to return only classes that currently have a TRIAL enabled (the schedule in_trial flag). Omit to include all classes regardless of trial.", "type": "boolean" }, "include_archived": { "description": "Default false → returns active + paused (inactive) classes. Set true to search ARCHIVED (retired) classes instead.", "type": "boolean" }, "lead_only": { "description": "Set true to return only LEAD-COLLECTION schedules (schedule_type='lead_collection' — the lead pipelines that bookings_add_lead attaches leads to). Every row also carries schedule_type, so you can tell real classes apart from lead pipelines without this filter.", "type": "boolean" }, "name": { "description": "Substring match on the class (schedule) name, e.g. \"Nejaké\". Case- and accent-insensitive (DB collation utf8mb4_unicode_ci) — \"nejake\" matches \"Nejaké\", so you need not reproduce diacritics.", "type": "string" }, "page": { "description": "0-based page index (default 0).", "minimum": 0, "type": "integer" }, "page_size": { "description": "Number of results per page (max 200).", "maximum": 200, "minimum": 1, "type": "integer" }, "place_id": { "description": "Only classes at this venue. Resolve with classes_find_resource (kind:'place').", "exclusiveMinimum": 0, "type": "integer" }, "registration_type": { "description": "Filter by the parent course's registration model: 'single' = drop-in / per-session, 'full2' = full-course enrollment, 'open' = open-ended / membership.", "enum": [ "single", "full2", "open" ], "type": "string" }, "sort": { "description": "Result ordering. created_desc = newest class first (the default), created_asc = oldest first, date_asc/date_desc by schedule start date, name_asc alphabetical, registrations_desc most-enrolled first. Use created_desc for \"the latest classes\".", "enum": [ "created_desc", "created_asc", "date_asc", "date_desc", "name_asc", "registrations_desc" ], "type": "string" }, "trainer_id": { "description": "Only classes this trainer is assigned to. Resolve with classes_find_resource (kind:'trainer').", "exclusiveMinimum": 0, "type": "integer" }, "with_sessions": { "description": "Set true to return only classes that HAVE at least one session (schedule total_events > 0). Use this for \"classes that actually have sessions\" instead of scanning sessions_find_events.", "type": "boolean" } }, "type": "object" }, "name": "classes_find_classes", "outputSchema": null }, { "description": "Search the company's courses by name (substring match) and optionally by registration_type. Returns a slim list of matches — `{id, name, registration_type, target_audience, price, schedules_count, ...}` — enough to disambiguate, not enough to act. Use this whenever the user names a course in natural language; never demand a raw course_id. Archived courses are excluded by default (pass `include_archived: true` to opt in). Pagination defaults to page 0, page_size 25 (max 200); `truncated: true` is returned when more matches exist than the current page reveals.\n\n`registration_type` business meanings (when filtering, AND when surfacing results to the user — always translate to these terms, never show the raw enum value):\n- `single` — drop-in / per-session: customer books one event at a time.\n- `full2` — full-course enrollment: customer signs up for the entire course/schedule in one go.\n- `open` — open-ended / membership: no fixed enrollment window; customer joins and stays.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "include_archived": { "description": "Default false → archived programmes are excluded. Set true to also include archived (retired) programmes.", "type": "boolean" }, "name": { "description": "Substring match on the programme (course) name, e.g. \"Ballet\". Matches any programme whose name contains this text.", "type": "string" }, "page": { "description": "0-based page index (default 0).", "minimum": 0, "type": "integer" }, "page_size": { "description": "Number of results per page (max 200).", "maximum": 200, "minimum": 1, "type": "integer" }, "registration_type": { "description": "Registration model. 'single' = drop-in / per-session booking (customer books one event at a time). 'full2' = full-course enrollment (customer signs up for the entire course at once). 'open' = open-ended / membership (no fixed enrollment window).", "enum": [ "single", "full2", "open" ], "type": "string" } }, "type": "object" }, "name": "classes_find_courses", "outputSchema": null }, { "description": "Resolve a NAME the operator said into an id, for four kinds of company-level records. Pick `kind`:\n\n- `place` — venues. Returns `{id, name, city, street, rooms: [{id, name, capacity}]}`. Rooms are inlined because picking a venue is usually followed by picking a room. Filters: name, city.\n- `billing_period` — term blocks (e.g. \"Autumn 2026\"). Returns `{id, name, active, period_start, period_end}`; either date may be null, an open-ended period is valid. Filter: name. Match on the DATES, not just the name — a period covering the term you want may be named anything.\n- `trainer` — team members assignable to classes. Returns `{id, full_name, email, active, virtual}`. Filters: name, place_id, course_id. **Virtual trainers** are always included regardless of place/course filters — they are system-wide placeholders with `virtual: true`, a synthetic id (>= 9000000000000) and no email. Pick one when the operator says \"we'll decide later\", \"no trainer yet\", \"TBD\", \"unassigned\", \"guest\", \"external speaker\". Three ship by default: 'To be decided', 'Trainer unassigned', 'Guest trainer'.\n- `trainer_rate_type` — named pay rates (e.g. \"Hourly\", \"Per class\"). Returns `{id, name, minutes, type}`. This is the ONLY way to turn a rate the operator names into the `trainer_rate_type_id` that classes_update and sessions_update need — NEVER guess that id.\n\n`include_inactive` (place: n/a) defaults false. Sending a filter that does not apply to the chosen kind returns the list of filters that do. For PROGRAMMES use classes_find_courses, for CLASSES classes_find_classes, for people enrolled use bookings_find — those are separate, richer tools.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "city": { "description": "kind=place only.", "type": "string" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "kind=trainer only — narrow to trainers associated with this programme.", "exclusiveMinimum": 0, "type": "integer" }, "include_inactive": { "description": "kind=trainer or billing_period. Default false — include former staff / deactivated periods.", "type": "boolean" }, "kind": { "description": "Which kind of record to resolve. 'place' = venue, 'billing_period' = term block, 'trainer' = team member, 'trainer_rate_type' = named pay rate.", "enum": [ "place", "billing_period", "trainer", "trainer_rate_type" ], "type": "string" }, "name": { "description": "Substring match on the record's name. Applies to every kind.", "type": "string" }, "page": { "description": "kind=place or trainer. Default 0.", "minimum": 0, "type": "integer" }, "page_size": { "description": "kind=place or trainer. Default 25, max 200. billing_period and trainer_rate_type are small bounded sets and are returned in full.", "maximum": 200, "minimum": 1, "type": "integer" }, "place_id": { "description": "kind=trainer only — narrow to trainers associated with this venue.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "kind" ], "type": "object" }, "name": "classes_find_resource", "outputSchema": null }, { "description": "Returns all valid field values for building class schedules and payment plans in Zooza. No Zooza API call — hardcoded from Events_Preview.php and Payment_Schedule.php. Call this BEFORE classes_preview_schedule or classes_commit_class to avoid validation errors. Critical: weekdays must be 3-letter lowercase (mon/tue/wed...), NOT 'monday' or '1'. Critical: until_date and count are mutually exclusive — sending both causes an API error. Examples: domain=\"event_generation\" → cadences, weekdays, time format; domain=\"payment_schedule\" → schedule types and billing frequencies; empty call → both sections.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "domain": { "description": "Filter to one section. Omit to return both event generation and payment schedule references.", "enum": [ "event_generation", "payment_schedule" ], "type": "string" } }, "type": "object" }, "name": "classes_list_schedule_patterns", "outputSchema": null }, { "description": "Expands one or more recurrence patterns and/or ad-hoc dates into the concrete list of class sessions, honouring holiday-skip flags. Stateless — performs no writes. Call this once per pattern the user describes during class creation. Accumulate the returned sessions across multiple calls (Claude side) until the user says they're done, then pass the full list to `classes_commit_class`. Each block must carry EXACTLY ONE of `count` (stop after N sessions) or `until_date` (stop on a fixed date) — count mode is preferred when the user says \"X sessions\". A top-level `to_date` acts as a fallback `until_date` for any block that omits both. `place_id` is required so api-v1 can apply the correct subdivision-scoped school-holiday calendar. When you SHOW the expanded sessions to the user, render them as a weekly GRID (days across the top, time down the left — the Zooza app calendar layout): one representative week with the run range + session count in a caption, NOT a flat date list — unless the user explicitly asks to see every date.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "additional_dates": { "description": "One-off sessions on specific dates, added on top of any recurrence blocks (e.g. an extra makeup date).", "items": { "additionalProperties": false, "properties": { "date_string": { "description": "Date of this ad-hoc session, YYYY-MM-DD.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "duration": { "description": "Session length in minutes.", "exclusiveMinimum": 0, "type": "integer" }, "time_minutes": { "description": "Session start time as minutes past midnight (0-1439, e.g. 540 = 09:00).", "maximum": 1439, "minimum": 0, "type": "integer" }, "trainer_id": { "description": "Optional per-session instructor override. Resolve with trainers_find.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "date_string", "time_minutes", "duration" ], "type": "object" }, "type": "array" }, "blocks": { "description": "Recurrence patterns to expand into concrete sessions. Each block describes one repeating rhythm; add multiple blocks for classes with several patterns.", "items": { "additionalProperties": false, "properties": { "all_day": { "description": "When true, the session has no fixed start time (an all-day session).", "type": "boolean" }, "cadence": { "description": "How often the pattern repeats: daily, weekly, biweekly, or monthly. Defaults to weekly.", "enum": [ "daily", "weekly", "biweekly", "monthly" ], "type": "string" }, "count": { "description": "Stop after generating this many sessions. Provide EXACTLY ONE of count or until_date; preferred when the user says 'X sessions'.", "maximum": 500, "minimum": 1, "type": "integer" }, "duration": { "description": "Session length in minutes.", "exclusiveMinimum": 0, "type": "integer" }, "time_minutes": { "description": "Session start time as minutes past midnight (0-1439, e.g. 540 = 09:00).", "maximum": 1439, "minimum": 0, "type": "integer" }, "trainer_id": { "description": "Optional per-block instructor override. Resolve with trainers_find.", "exclusiveMinimum": 0, "type": "integer" }, "until_date": { "description": "Stop generating sessions on this date (inclusive), YYYY-MM-DD. Provide EXACTLY ONE of count or until_date.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "weekdays": { "description": "Days of the week this recurring pattern runs on (e.g. ['mon','wed']). Omit for a single-day cadence.", "items": { "enum": [ "mon", "tue", "wed", "thu", "fri", "sat", "sun" ], "type": "string" }, "type": "array" } }, "required": [ "time_minutes", "duration" ], "type": "object" }, "type": "array" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "from_date": { "description": "Start of the window to expand sessions into, YYYY-MM-DD. Recurrence blocks begin generating on or after this date.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "place_id": { "description": "Venue (place) the sessions run at. Required so api-v1 applies the correct subdivision-scoped school-holiday calendar. Resolve with classes_find_places.", "exclusiveMinimum": 0, "type": "integer" }, "skip_custom_holidays": { "description": "When true, skip sessions on dates the company itself has defined as holidays/closures — company-specific, independent of the shared public/school-holiday calendars.", "type": "boolean" }, "skip_holidays": { "description": "When true, skip generating sessions that fall on public / state-wide holidays. Holiday calendars are system-wide (shared across all companies) and applied per the venue's region: a regional (non-country-wide) holiday only applies when the venue's location has a region set — otherwise only country/state-wide holidays are skipped.", "type": "boolean" }, "skip_school_holidays": { "description": "When true, skip sessions that fall within school-holiday periods. Same system-wide, region-distributed calendar as skip_holidays — the venue's location must have a region set for region-specific school holidays to apply; otherwise only country-wide ones are skipped.", "type": "boolean" }, "to_date": { "description": "Optional end of the window, YYYY-MM-DD. Acts as a fallback until_date for any block that supplies neither count nor until_date.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" } }, "required": [ "place_id", "from_date" ], "type": "object" }, "name": "classes_preview_events", "outputSchema": null }, { "description": "Resolves a new class's *schedule shell* — the course, venue, trainer, capacity, prices, billing period, and default payment templates — and returns the result alongside any warnings. Performs no writes. Use this first in a class-creation flow to confirm the basic class settings with the user before collecting session dates via `classes_preview_events` and committing via `classes_commit_class`. Defaults are copied from the parent course where the caller hasn't specified them (capacity from `target_audience`, prices from the course's pricing fields). Always surface the `warnings[]` array to the user — entries about `online_registration` and `billing_period_id` are real decisions to confirm, not noise. For lead-collection classes (`schedule_type: lead_collection`), the events step is skipped entirely after this preview.\n\n`name` is OPTIONAL — do NOT pass it unless the user explicitly asked for a custom class name. End-user-facing display is auto-rendered by api-v1 as `{course_name} {class_name} {session_dates}`, so leaving it blank gives users the most informative label by default. Only set `name` when the user says something like 'call it \"Morning Yoga Group A\"'.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "all_day": { "description": "When true, the session has no fixed start time (an all-day session).", "type": "boolean" }, "billable_events": { "description": "Number of billable sessions used to compute what clients owe. Copied from the parent course when omitted.", "minimum": 0, "type": "number" }, "billing_period_id": { "description": "Term block (billing period) this class belongs to. Resolve with classes_find_billing_periods. Falls back to the most recent active period when omitted.", "exclusiveMinimum": 0, "type": "integer" }, "capacity": { "description": "Basic maximum number of seats per session — the ordinary class size. Defaults from the course's target_audience when omitted.", "exclusiveMinimum": 0, "type": "integer" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Parent programme (course) the new class belongs to. Resolve with classes_find_courses. Defaults (capacity, prices) are copied from this course.", "exclusiveMinimum": 0, "type": "integer" }, "duration_minutes": { "description": "Session length in minutes. Defaults to 60 when omitted.", "exclusiveMinimum": 0, "type": "integer" }, "name": { "description": "OPTIONAL — leave unset unless the user explicitly asked for a custom class name. api-v1 auto-renders `{course_name} {class_name} {session_dates}` for end users when name is blank, which is almost always what you want.", "type": "string" }, "online_registration": { "description": "Whether clients can self-register for this class online — true publishes it on the public website. Defaults to true.", "type": "boolean" }, "payment_schedule_template_ids": { "description": "Ids of the payment schedule templates to attach. Omit to select the course's default templates.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "type": "array" }, "place_id": { "description": "Venue (place) where the class will run. Resolve with classes_find_places.", "exclusiveMinimum": 0, "type": "integer" }, "price": { "description": "Total price for the class/period, used when the programme prices by total. Copied from the parent course when omitted.", "minimum": 0, "type": "number" }, "registration_fee": { "description": "One-time enrollment fee charged on top of the class price. Copied from the parent course when omitted.", "minimum": 0, "type": "number" }, "room_id": { "description": "Room within the venue. Defaults to 0 (no specific room) when omitted.", "minimum": 0, "type": "integer" }, "schedule_type": { "description": "What kind of class this is. 'fixed_period' = a real class with concrete dates the trainer will run — sessions get created and customers register for them. 'lead_collection' = a pre-launch interest-gathering placeholder (no dates yet); customers can express interest, and the operator converts it to a fixed_period class once dates are decided. For lead_collection, the events step is skipped entirely after preview.", "enum": [ "fixed_period", "lead_collection" ], "type": "string" }, "trainer_id": { "description": "Instructor assigned to the class. Resolve with trainers_find.", "exclusiveMinimum": 0, "type": "integer" }, "trainer_rate_type_id": { "description": "Trainer PAY-RATE type (what the instructor is paid, not what clients pay). Resolve with trainers_find_rate_types. Defaults to 0 (none).", "minimum": 0, "type": "integer" }, "unit_price": { "description": "Per-session price, used when the programme prices per session. Copied from the parent course when omitted.", "minimum": 0, "type": "number" } }, "required": [ "course_id", "place_id", "trainer_id" ], "type": "object" }, "name": "classes_preview_schedule", "outputSchema": null }, { "description": "Edit one or more existing classes (a \"class\"/\"timetable\" is the recurring group within a programme) — name, price, registration fee, capacity, make-up/replacement extra capacity (\"počet miest navyše pre náhradné hodiny\" → extra_capacity/extra_capacity_usage, NOT registrations_cap, which caps the NUMBER OF REGISTRATIONS), registration-count cap, billing period, online-registration, status — and/or instructor, venue, or session duration.\n\nTWO CALLS. First WITHOUT `token`: returns a preview of exactly what changes and how many sessions are affected, plus a single-use token. Show it to the operator and get explicit approval. Then call again with `token` + `confirmed: true` to apply — send nothing else, the change is frozen in the plan. To alter anything, run the first call again.\n\nChanging instructor/venue/duration forces a `session_scope` choice about existing sessions: \"upcoming\" (this class + its future sessions — usually what people mean), \"all\" (every session incl. past), or \"class_only\" (re-advertise the class but leave existing sessions on their old value — the #1 cause of \"I changed it but the sessions still show the old value\", so only pick it deliberately). This scope is the OPERATOR's call, not yours: when they haven't stated one, PRESENT the three options and their consequences and let them choose — do NOT silently pick a scope, and do NOT stall. If the operator asks what a cascade edit will do before applying, explain this same taxonomy.\n\nHandles one class or many at once. To edit specific individual sessions (move one date, change one session's room), use sessions_update instead. To cancel sessions, use the cancellation tools.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "changes": { "additionalProperties": false, "description": "Required on the FIRST call. The fields to change — at least one.", "properties": { "billing_period_id": { "description": "Term block (billing period) this class belongs to. Resolve with classes_find_billing_periods.", "exclusiveMinimum": 0, "type": "integer" }, "capacity": { "description": "Basic maximum number of seats per session. This is the ordinary class size; it does NOT include the make-up buffer (see extra_capacity) and is unrelated to the registration-count limit (see registrations_cap).", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Move the class to a different PROGRAMME (course). GUARDED — rewrites ALL sessions AND all registrations (irreversible); requires confirm_course_change: true.", "exclusiveMinimum": 0, "type": "integer" }, "description": { "description": "Public description of the class shown to clients on the registration page.", "type": "string" }, "duration": { "description": "Session length in minutes. CASCADE field — changing it requires session_scope.", "exclusiveMinimum": 0, "type": "integer" }, "extra_capacity": { "description": "Extra seats reserved for make-up / replacement (SK/CZ: náhradné/náhrady) sessions — the \"Extra capacity for make-up sessions\" setting. Lets make-up attendees be booked ABOVE the class's basic `capacity`. `0` = no per-class make-up buffer. How it combines with the company-wide global make-up default is controlled by extra_capacity_usage. This is the correct field for \"počet miest navyše pre náhradné hodiny\" — NOT registrations_cap.", "minimum": 0, "type": "integer" }, "extra_capacity_usage": { "description": "How extra_capacity combines with the company-wide global make-up capacity default. `add` = the per-class buffer is ADDED on top of the global (make-up ceiling = capacity + extra_capacity + global); `replace` = the per-class buffer REPLACES the global (global ignored). Mirrors the make-up panel's \"Add to global setting\" / \"Replace global settings\" radios. When a class has never had it set, Zooza treats it as `add`.", "enum": [ "add", "replace" ], "type": "string" }, "name": { "description": "Class (schedule) display name — the label operators and clients see for this recurring group.", "type": "string" }, "note": { "description": "Internal, staff-only note on the class. Not shown to clients.", "type": "string" }, "online_registration": { "description": "Whether clients can self-register for this class online. `false` removes it from public registration.", "type": "boolean" }, "place_id": { "description": "Venue (place) for the class. Resolve with classes_find_places. Must be changed together with room_id. CASCADE field — changing it requires session_scope.", "exclusiveMinimum": 0, "type": "integer" }, "price": { "description": "Total price for the class/period, used when the programme prices by total (full2/total_price or single registration types). Stored verbatim; `0` is saved as-is with no inheritance from the parent programme.", "minimum": 0, "type": "number" }, "registration_fee": { "description": "One-time enrollment fee charged on top of the class price. Stored verbatim.", "minimum": 0, "type": "number" }, "registrations_cap": { "description": "Cap on the NUMBER OF REGISTRATIONS (bookings) this class accepts — NOT seats, and NOT make-up capacity. One registration can occupy several seats (e.g. a birthday party = 1 registration holding 7 seats), so this limits how many separate bookings exist, independent of seat count. `0` = OFF, no limit on the number of registrations — this is the normal default. Setting it to 1 restricts the class to a single registration; setting it to N caps it at N registrations. It does NOT by itself stop registrations or reduce capacity, and it has NOTHING to do with make-up/replacement (náhrady) sessions — for those use extra_capacity. Only set this for genuine multi-seat-per-registration scenarios.", "minimum": 0, "type": "integer" }, "room_id": { "description": "Room within the venue. Must be changed together with place_id.", "minimum": 0, "type": "integer" }, "status": { "description": "Class lifecycle state: `active` (live/bookable), `inactive` (hidden/paused), `archive` (retired).", "enum": [ "active", "inactive", "archive" ], "type": "string" }, "trainer_id": { "description": "Instructor assigned to the class. Resolve with trainers_find. CASCADE field — changing it requires session_scope.", "exclusiveMinimum": 0, "type": "integer" }, "trainer_rate_type_id": { "description": "Trainer PAY-RATE type for this class (what the instructor is paid, not what clients pay). Resolve with trainers_find_rate_types. CASCADE field — changing it requires session_scope.", "minimum": 0, "type": "integer" }, "unit_price": { "description": "Per-session price, used when the programme prices per session (full2/unit_price or open registration types). Stored verbatim; `0` is saved as-is with no inheritance from the parent programme.", "minimum": 0, "type": "number" } }, "type": "object" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirm_course_change": { "description": "REQUIRED true to change changes.course_id — reprogramming a class rewrites ALL its sessions AND all registrations (irreversible). Only set after the operator explicitly asked to move the class to a different programme.", "type": "boolean" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "schedule_ids": { "description": "Required on the FIRST call. One or many class (schedule) ids. Resolve by name with classes_find_classes.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "session_scope": { "description": "REQUIRED when changing instructor/venue/duration. 'upcoming' = the class + its future sessions (usual); 'all' = every session; 'class_only' = re-advertise the class only, existing sessions keep their old value (warn the operator — usually NOT what they want).", "enum": [ "class_only", "upcoming", "all" ], "type": "string" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "classes_update", "outputSchema": null }, { "description": "Change the settings of an existing programme (course) — pricing, online booking, make-up sessions, trial, auto-enrolment, attendance, feedback, basic info, or archiving. Works one section at a time, like the settings tiles in the Zooza app.\n\nTWO CALLS. First call WITHOUT `token`: returns a diff of current → proposed values, warnings, and a single-use token. Show that diff to the user and get their approval. Second call with `token` + `confirmed: true`: applies it. Send nothing else on the second call — the token already carries the change. The token expires in 15 minutes; if it is expired or used, run the first call again.\n\nResolve the programme first with `classes_find_courses` (needs `course_id`). This tool edits the PROGRAMME level — rules inherited by all its classes. To change one class/group (capacity, venue, instructor, time), use the classes tools instead. Some sections only apply to \"booking for full programme duration\" programmes: trial, make-up sessions, auto-enrolment.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "changes": { "additionalProperties": {}, "description": "Field → new value, required on the FIRST call. Keys limited to the chosen section's whitelist (an invalid field returns the allowed list). Booleans as true/false, enums as their string value. Example: {\"online_registration\": false}.", "type": "object" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "course_id": { "description": "Programme (course) id — required on the FIRST call. Resolve names with classes_find_courses; never guess ids.", "exclusiveMinimum": 0, "type": "integer" }, "section": { "description": "Which settings tile to change — required on the FIRST call. One section per call, mirroring the Zooza app's settings dashboard. 'trial', 'makeup_sessions' and 'auto_enrolment' only exist for 'booking for full programme duration' (full2) programmes.", "enum": [ "basic_info", "price_and_payment", "online_booking", "booking_form_labels", "makeup_sessions", "trial", "auto_enrolment", "attendance", "feedback" ], "type": "string" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "classes_update_course_settings", "outputSchema": null }, { "description": "Read inbound replies a customer has sent back to Zooza emails, and optionally mark a reply handled. Use it to see whether a lead responded and what they said — filter by the lead's registration id, sender email, state (unread / todo / resolved), or date. To act on a reply, pass `mark_reply_id` + `mark_state` to flag it `todo` (needs a human) or `resolved` (handled), or `read`. Replies only appear here if the original email went out through Zooza tied to that registration. This does NOT send anything — use comms_send_message to reply. The idempotency pattern: read `unread` replies, act, then mark `resolved` so the same reply isn't handled twice.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "from_email": { "description": "Filter by sender email (partial match).", "type": "string" }, "mark_reply_id": { "description": "If set, MARK this reply's state instead of listing. Requires mark_state.", "exclusiveMinimum": 0, "type": "integer" }, "mark_state": { "description": "Required with mark_reply_id: 'read', 'todo', or 'resolved'.", "enum": [ "read", "todo", "resolved" ], "type": "string" }, "registration_id": { "description": "The lead's registration id (the inbound reply's order_id). The usual filter — a lead's replies.", "exclusiveMinimum": 0, "type": "integer" }, "since": { "description": "Only replies on/after this date (YYYY-MM-DD).", "type": "string" }, "state": { "description": "Which replies to return: 'unread' (default), 'todo', 'resolved', or 'all'.", "enum": [ "unread", "todo", "resolved", "all" ], "type": "string" } }, "type": "object" }, "name": "comms_find_replies", "outputSchema": null }, { "description": "Returns all valid merge variables for Zooza message templates (email, SMS, WhatsApp). Format: *|VARIABLE_NAME|* (MailChimp-compatible). No Zooza API call — hardcoded from Merge_Vars::merge_vars() in api-v1. Use this BEFORE writing any message template to get correct variable names. Claude must not invent variable names — only variables listed here are valid. Examples: category=\"financial\" → payment and balance vars; medium=\"sms\" → SMS-safe vars with HTML warnings; empty call → full catalogue grouped by category.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "category": { "description": "Filter by category. Omit to return all variables.", "enum": [ "client", "booking", "programme", "session", "financial", "place", "online_meeting", "extra_fields", "system", "notification_urls" ], "type": "string" }, "medium": { "description": "Include medium-specific notes (e.g. HTML variables that do not render in SMS).", "enum": [ "email", "sms", "whatsapp" ], "type": "string" } }, "type": "object" }, "name": "comms_list_merge_vars", "outputSchema": null }, { "description": "Lists the automated email templates Zooza sends to this company's clients — registration confirmations, trial follow-ups, cancellation notices, session reminders, loyalty/discount emails, and custom templates. For each template returns its trigger `type`, subject line, and whether the company uses the stock Zooza default or has customized it. Use this to see which automated emails exist, check what has been customized, or look up a template's `type` before previewing or sending it (comms_send_message accepts that `type`). This tool only lists email templates — for the merge variables (*|FIRST_NAME|* etc.) usable inside template bodies, use comms_list_merge_vars instead. Read-only; sends nothing. Bodies are full HTML and large — only set include_body when the user asks to see template content.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "include_body": { "description": "Default false. When true, includes the full HTML body of each returned template (large — combine with `type`).", "type": "boolean" }, "source": { "description": "Default 'all'. 'customized' = only templates this company has overridden; 'default' = only untouched stock templates.", "enum": [ "all", "default", "customized" ], "type": "string" }, "type": { "description": "Exact template type, e.g. \"registration_cancellation\" — returns just that template. Omit to browse all.", "type": "string" } }, "type": "object" }, "name": "comms_list_templates", "outputSchema": null }, { "description": "Email clients of this company. Describe the audience (a course/programme, a class schedule, a specific booking, one client, a saved segment, an ad-hoc cohort, or course-level labels) and the content (an existing template `type` from comms_list_templates, or a custom subject + body which may use *|MERGE_VAR|* tags from comms_list_merge_vars).\n\nTWO CALLS. First WITHOUT `token`: sends NOTHING. Returns the estimated recipient count, a sample of recipients, the content as it will be sent, warnings (unknown merge tags, zero recipients), and a single-use token. Show that plan to the operator and get explicit confirmation. Then call again with `token` + `confirmed: true` to actually send. Calling the first form again with adjusted filters is free and repeatable — refine the audience that way rather than guessing.\n\nLARGE SENDS need a SECOND confirmation. If the recipient count exceeds the approval threshold, the sending call returns `requires_second_confirmation: true` with the count and job id and sends NOTHING yet. Show the operator the exact recipient count and ask again (e.g. \"Send to all 105 clients?\"). Only after they explicitly agree, call once more with the SAME token, `confirmed: true`, and `confirm_large_send: true`. If they decline, send nothing.\n\nResolve names to ids first: classes_find_courses for a course/programme → course_id, classes_find_classes for a class/group by name → schedule_id, sessions_find_events for a single session → event_id; never guess ids. When the operator names an ad-hoc cohort rather than the whole company — \"everyone who hasn't paid\", the unpaid roster, the waitlist, this week's sign-ups — resolve it with bookings_find and pass the resulting registration_id LIST as audience.registration_id. Reserve audience.whole_company for genuinely company-wide sends; do NOT use it as a shortcut for a named subset, or you email far more people than the operator asked for.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "audience": { "additionalProperties": false, "description": "Who receives the message. At least one targeting field is required.", "properties": { "active_only": { "description": "Only meaningful with whole_company. true (DEFAULT) = only clients with an ACTIVE (registered) booking — the safe choice. false = literally EVERYONE incl. cancelled/inactive/past clients (spammy) — use only when the operator has explicitly asked for that. When whole_company is set, ASK the operator which they mean and state plainly that the default skips cancelled/inactive people.", "type": "boolean" }, "course_id": { "description": "Everyone registered in this course/programme.", "exclusiveMinimum": 0, "type": "integer" }, "exclude": { "description": "Registration ids to leave out.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "type": "array" }, "guests": { "description": "Default false. Also send to guest registrations (added at send time; not in the count estimate).", "type": "boolean" }, "inactive_customers": { "description": "Default false. Include inactive registrations.", "type": "boolean" }, "labels": { "description": "Registrations in COURSES labeled with any of these label ids — labels attach at course level, not per person.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "type": "array" }, "registration_id": { "anyOf": [ { "exclusiveMinimum": 0, "type": "integer" }, { "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" } ], "description": "One booking, or a LIST of bookings — pass a single id or an array (e.g. the registration_ids from a bookings_find result set, so you can message an ad-hoc cohort like the unpaid roster without a saved segment)." }, "schedule_id": { "description": "Everyone in this class (schedule).", "exclusiveMinimum": 0, "type": "integer" }, "segment_id": { "description": "A saved registration segment.", "exclusiveMinimum": 0, "type": "integer" }, "user_id": { "description": "One client (all their registrations).", "exclusiveMinimum": 0, "type": "integer" }, "whole_company": { "description": "Broadcast to the ENTIRE company — every booking, one email per client. The only audience needing no id. ONLY for genuinely company-wide sends. For a NAMED subset — the unpaid roster, the waitlist, one class — do NOT use this; resolve the cohort with bookings_find and pass its registration_id list to registration_id instead. This can reach a LOT of people, so ALWAYS confirm scope with the operator first, and make the all-vs-active choice explicit (see active_only) — do NOT silently email everyone. Pair with active_only.", "type": "boolean" } }, "type": "object" }, "bcc": { "description": "Comma-separated BCC addresses.", "type": "string" }, "channel": { "description": "Only 'email' is implemented. WhatsApp is specced and coming — do not promise it yet.", "enum": [ "email" ], "type": "string" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirm_large_send": { "description": "Different from `confirmed`. Set true ONLY on the follow-up call after a send came back requires_second_confirmation: true AND the operator explicitly approved the recipient COUNT. Never set it on the first sending call, and never without that separate approval.", "type": "boolean" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "content": { "additionalProperties": false, "description": "EITHER template_type OR subject+body — not both.", "properties": { "body": { "description": "Custom email body (HTML or text); may contain *|MERGE|* tags.", "type": "string" }, "subject": { "description": "Custom email subject; may contain *|MERGE|* tags.", "type": "string" }, "template_type": { "description": "Existing email template type from comms_list_templates, e.g. \"registration_cancellation\".", "type": "string" } }, "type": "object" }, "marketing": { "description": "REQUIRED on the FIRST call. true = promotional content (consent rules apply; say so to the operator). false = operational (schedule changes, payment reminders, session info).", "type": "boolean" }, "schedule_at": { "additionalProperties": false, "description": "Omit to send immediately on commit.", "properties": { "date": { "description": "Local send date, YYYY-MM-DD.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "hour": { "description": "Local send hour, 0-23 (24-hour clock).", "maximum": 23, "minimum": 0, "type": "integer" }, "minute": { "description": "Local send minute, 0-59.", "maximum": 59, "minimum": 0, "type": "integer" } }, "required": [ "date", "hour", "minute" ], "type": "object" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "comms_send_message", "outputSchema": null }, { "description": "Returns a structured description of Zooza's domain entities — hierarchy, roles, valid field values (enums), parent/child relationships, and disambiguation rules. No Zooza API call is made — purely hardcoded domain knowledge. Call this before any class-creation, booking, or attendance tool to avoid entity confusion. Examples: entity=\"programme\" → registration_type enums; entity=\"booking\" → status values; entity=\"credit\" → make-up / replacement entitlements (the \"unused/expired make-ups\" and \"credits\" concept); empty call → full hierarchy with all entities.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "entity": { "description": "Return full detail for this entity only. Omit to get the complete hierarchy summary.", "enum": [ "programme", "class", "session", "booking", "attendance", "credit", "trainer", "place" ], "type": "string" } }, "type": "object" }, "name": "explain_data_model", "outputSchema": null }, { "description": "Returns the full markdown playbook for one of the registered skills. Call this BEFORE starting a flow named in the server's instructions — the playbook contains the interview steps, mapping rules, and confirmation pattern for that scenario. Available skills: business-model-validator, class-management, communication, feedback-nudge, negotiate-terminology, report-compose, report-discovery, report-page-new, schedule-optimization.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "name": { "description": "Exact skill name from the available list.", "type": "string" } }, "required": [ "name" ], "type": "object" }, "name": "get_skill", "outputSchema": null }, { "description": "Search the Zooza domain glossary. Returns canonical term names, definitions, cross-language synonyms, disambiguation rules, and AI guidance notes. No Zooza API call is made — purely local lookup against the compiled glossary. Use this to resolve ambiguous user input before calling operational tools. Examples: query=\"hodina\" → Session; query=\"kurz\", language=\"sk\" → Programme; category=\"product-hierarchy\" → all hierarchy terms; empty call → full index.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "category": { "description": "Filter by glossary category.", "enum": [ "product-hierarchy", "programme-types", "bookings", "attendance", "payments", "clients", "communication", "platform", "settings", "scheduling", "client-management" ], "type": "string" }, "language": { "description": "Restrict intent_keyword matching to this language. Also highlights translations for the given language in the response.", "enum": [ "en", "sk", "cz", "de", "pl", "ro", "hu", "it", "fr" ], "type": "string" }, "query": { "description": "Term, synonym, or keyword to look up. Matched against canonical_en, synonyms, deprecated terms, and intent_keywords (all languages). Case-insensitive substring match.", "type": "string" } }, "type": "object" }, "name": "get_terminology", "outputSchema": null }, { "description": "Attach or detach a label (a named tag) on a Zooza course, schedule, or registration. Set `present: true` to attach (the label is created automatically if it doesn't exist yet — attach is idempotent), `present: false` to detach. Use it to tag records for grouping or pipeline state — e.g. mark a lead registration `converted`, or flag one `todo`. Works ONLY on courses, schedules, and registrations. NOTE: labels on a SCHEDULE can be customer-visible on the public booking widget (output flags this as `public_facing`); labels on courses and registrations are internal. Does not send anything.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "label": { "description": "Label name (the whole identity — labels have no colour). Created on first attach.", "minLength": 1, "type": "string" }, "object_id": { "description": "Id of the course/schedule/registration to tag.", "exclusiveMinimum": 0, "type": "integer" }, "object_type": { "description": "What kind of thing to tag: 'course', 'schedule', or 'registration'. Zooza labels attach only to these three.", "enum": [ "course", "schedule", "registration" ], "type": "string" }, "present": { "description": "true = attach the label, false = detach it.", "type": "boolean" } }, "required": [ "object_type", "object_id", "label", "present" ], "type": "object" }, "name": "labels_mark", "outputSchema": null }, { "description": "Free tool — no Zooza API call, no company_id required. Two modes:\n \"start\" → returns the 8-question interview template for Claude to conduct conversationally.\n \"build\" → validates answers against the Zooza glossary, returns a TerminologyProfile JSON\n plus a /remember instruction so Claude saves the profile to memory.\nRun once per user. The saved profile is auto-loaded in every future Zooza session — no\nre-configuration needed. Call get_skill('negotiate-terminology') before starting the interview.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "action": { "description": "\"start\" — returns the 8-question interview template (call this first, then ask the user the questions conversationally). \"build\" — validates collected answers and returns TerminologyProfile JSON + /remember instruction.", "enum": [ "start", "build" ], "type": "string" }, "answers": { "additionalProperties": false, "description": "Required when action is \"build\". Omit for action \"start\".", "properties": { "billing_period_term": { "description": "What the user calls a Billing Period.", "type": "string" }, "booking_term": { "description": "What the user calls a Booking.", "type": "string" }, "class_term": { "description": "What the user calls a Class.", "type": "string" }, "client_term": { "description": "What the user calls a Client / Parent.", "type": "string" }, "locale": { "description": "Primary language code of this business, e.g. 'sk', 'cz', 'en', 'de', 'pl', 'ro', 'hu'.", "type": "string" }, "notes": { "description": "Any additional terminology notes or unusual vocabulary.", "type": "string" }, "programme_term": { "description": "What the user calls a Programme.", "type": "string" }, "session_term": { "description": "What the user calls a Session.", "type": "string" }, "trainer_term": { "description": "What the user calls a Trainer / Instructor.", "type": "string" } }, "required": [ "locale", "programme_term", "class_term", "session_term", "booking_term", "trainer_term", "billing_period_term", "client_term" ], "type": "object" } }, "required": [ "action" ], "type": "object" }, "name": "negotiate_terminology", "outputSchema": null }, { "description": "Put a booking on a payment plan — the instalment calendar the client actually pays against. A plan attached to a programme or class is NOT inherited by bookings; each booking has to have it applied, and until then the client owes nothing and sees no payment schedule.\n\nTWO CALLS. First WITHOUT `token`: writes nothing and returns the TOTAL plus the instalment dates and how many sessions each one covers. Zooza's preview does not expose per-instalment amounts before the plan exists — divide the total by the instalment count when telling the user, and say it is the expected split. Show that to the operator, then call again with `token` + `confirmed: true` to apply.\n\n`total_price` is the WHOLE amount for this booking, not a per-session price. Say \"EUR 200 for the term split into 4\" and pass total_price: 200 — Zooza does the division. Omit it to let Zooza price the booking from the class instead. (This is the opposite of `unit_price` on classes_add_course, which IS per session.)\n\nYou do not need a plan id: the tool lists the plans available on the booking's own class and picks the only one automatically. WARNING — if the booking already has a plan, applying another REPLACES it and rebuilds the ledger; the preview says so.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "include_sessions_in_first_payment": { "description": "Rolls sessions already elapsed into the first instalment instead of billing them separately.", "type": "boolean" }, "payment_schedule_id": { "description": "Usually omit. The id of a plan ON THE BOOKING'S CLASS — NOT a payment template id. Leave it out and the tool lists what the class offers and auto-selects a single one; only pass it when several exist and the user picked one.", "exclusiveMinimum": 0, "type": "integer" }, "registration_id": { "description": "Required on the FIRST call. The booking, from bookings_find (`registration_id`).", "exclusiveMinimum": 0, "type": "integer" }, "start": { "description": "YYYY-MM-DD. Anchors the instalment dates. Omit to use the class start.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" }, "total_price": { "description": "The TOTAL for this booking — the whole sum the client pays, which Zooza splits across the instalments. Not a per-session price. Omit to let Zooza calculate it from the class.", "minimum": 0, "type": "number" } }, "type": "object" }, "name": "payments_add_plan", "outputSchema": null }, { "description": "Return the REAL, pre-aggregated numbers for ONE business question about an activity brand — and the basis for SHOWING it. This is how you show an operator a report / dashboard / chart of their business numbers (occupancy, unpaid, churn, attendance, trials, retention, revenue, \"how are we doing\", per programme / venue / instructor): call this, then COMPOSE a focused report as an ARTIFACT in the conversation that renders in the side panel — do NOT hand the user a link or open a browser page. Views: occupancy, unpaid, churn, attendance, trials, retention, clients_by_location, replacements, summary. Use view=\"replacements\" for ANY question about make-up / replacement credits — \"unused make-ups\", \"expiring make-ups\", \"credits\", \"náhrady / náhradné hodiny\", \"are we overloaded on make-ups\", make-up demand vs available slots per programme (this IS the credits report; Zooza HAS make-up credits even though they are not in the business_dashboard views). The result has `headline` (computed key figures), `rows` (chart/table-ready, named, capped), `note` (a data-aware caption), `currency`, and `period`. RULES: every number you show the user MUST come from this result verbatim — never invent, estimate, or recompute figures, and never draw a chart without calling this first. Render charts with inline SVG/CSS — no external CDN or chart library (the artifact sandbox blocks them). If a view returns no rows, say so plainly. Follow get_skill(\"report-compose\").", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Resolved from the session when omitted.", "type": "integer" }, "from": { "description": "First month YYYY-MM-01. Both from+to or neither (default: last 6 months).", "type": "string" }, "to": { "description": "Last month YYYY-MM-01.", "type": "string" }, "view": { "description": "Question to pull data for. One of: occupancy, unpaid, churn, attendance, trials, retention, clients_by_location, replacements, summary. Default \"summary\".", "type": "string" } }, "type": "object" }, "name": "reports_get_data", "outputSchema": null }, { "description": "Write a post-session summary on one event. Two independent fields:\n\n- `public_summary` — visible to attendees / parents via their in-app Zooza feed. Use when the user says \"write a summary for the parents,\" \"send a recap,\" \"note for the families,\" etc. After write, every attendee's Person_Feed gets a `SUMMARY_PUBLIC` entry — parents see it in their client portal.\n- `internal_summary` — admin / team only. Use when the user says \"add a note for the team,\" \"private note,\" \"reminder for next week,\" etc. Not visible to parents.\n\nAt least one of the two must be provided. Both can be written in one call — the tool fans out the two PUTs api-v1 requires (the upstream endpoint dispatches on which field is in the body, so they cannot be combined). The tool checks the caller's role (**owner / assistant only** — trainers (`member`) cannot write summaries) and the event's `summary_public_locked` flag before writing; refuses cleanly when blocked. Returns the post-write state so you can confirm to the user what's now visible to whom.\n\n**Pairs naturally with `sessions_mark_attendance`.** After marking attendance for a session, offer to write a summary (always optional in V1; no api-v1 rule makes it mandatory). Don't volunteer a summary for an event that already has one (`summary.public_set=true` in the sessions_get_attendance / sessions_mark_attendance result) unless the user explicitly asks to update it.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "event_id": { "description": "Target event id (one session of a class). Required.", "exclusiveMinimum": 0, "type": "integer" }, "internal_summary": { "description": "Admin-only note on the event. Not visible to attendees. At least one of public_summary / internal_summary required.", "maxLength": 4000, "type": "string" }, "override_locked": { "description": "When the event's public summary is locked AND the caller is owner, set this true to write anyway. Refused for non-owners regardless.", "type": "boolean" }, "public_summary": { "description": "Parent-visible note. Delivered to each attendee's in-app Person_Feed. At least one of public_summary / internal_summary required.", "maxLength": 4000, "type": "string" } }, "required": [ "event_id" ], "type": "object" }, "name": "sessions_add_summary", "outputSchema": null }, { "description": "Cancel one or more scheduled SESSIONS of a class so they do not take place — \"cancel Tuesday's sessions\", \"the pool is closed on Friday\", \"Martina is ill all week, cancel her classes\". A cancelled session stays visible in Zooza with a cancelled status; it is NOT deleted.\n\nScope the call to exactly ONE entity: `event_ids`, a single `date`, a single `trainer_id` with `from`/`to`, a single `schedule_id` with `from`/`to`, or a single `place_id` with a `date`. Two instructors or two classes in one call are refused by design — cancel them one at a time so each blast radius is reviewed on its own. At most 7 days, 20 sessions and 150 affected clients per call.\n\nTWO CALLS. First WITHOUT `token`: returns every affected session, the client count, how many emails would be sent, and what will NOT happen — plus a single-use token. Show that to the operator and get explicit approval. Then call again with `token` + `confirmed: true` to apply; send nothing else, the plan is frozen.\n\n`reason` is required and is recorded for staff only. `public_reason` is what clients read and is required when `notify` is true — one email per session per client, so five sessions in a class of 25 is 125 messages; the preview states the exact count either way.\n\nOptionally create a make-up session with `replacement_date`, which moves the attendees onto the new date. Allowed ONLY when exactly one session is in scope, because a make-up date belongs to one session. Attendees who already have attendance marked (attended / no-show) and anyone waitlisted do NOT move — the preview counts them.\n\nSessions that already happened are left out unless you pass `include_past: true`. Only owners and assistants can cancel; other roles are refused with an explanation.\n\nThis tool does NOT un-cancel a session, does NOT delete sessions (cancelling and deleting are different verbs in Zooza), does NOT cancel ONE client's booking on a session — that is `sessions_mark_attendance` with `attendance: 'canceled'`, and that is the path that issues make-up credits — and does NOT reschedule anything (`sessions_update`). Cancelling a session issues no make-up credits to anyone.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "date": { "description": "SCOPE. One whole day, YYYY-MM-DD, company-wide unless place_id narrows it.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "event_ids": { "description": "SCOPE. Exact sessions, at most 7 days apart. Resolve with sessions_find_events. The only scope that may cross classes and instructors.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "from": { "description": "Inclusive range start, YYYY-MM-DD. Only with trainer_id or schedule_id.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "include_past": { "description": "Default false — past sessions are skipped and reported.", "type": "boolean" }, "notify": { "description": "Default false. Set it on the FIRST call; it is frozen into the plan.", "type": "boolean" }, "place_id": { "description": "SCOPE. ONE venue, with date. Resolve with classes_find_resource kind:'place'.", "exclusiveMinimum": 0, "type": "integer" }, "public_reason": { "description": "Client-facing email text. Required when notify is true.", "type": "string" }, "reason": { "description": "REQUIRED. Internal, staff-only; Zooza cannot clear it later.", "minLength": 1, "type": "string" }, "replacement_date": { "description": "Make-up session date, \"YYYY-MM-DD HH:MM:SS\" — time defaults to the cancelled session's own. One session in scope only.", "pattern": "^\\d{4}-\\d{2}-\\d{2}([ T]\\d{2}:\\d{2}(:\\d{2})?)?$", "type": "string" }, "replacement_place_id": { "description": "Make-up venue. Defaults to the cancelled session's.", "exclusiveMinimum": 0, "type": "integer" }, "replacement_room_id": { "description": "Make-up room; needs replacement_place_id. 0 = no room.", "minimum": 0, "type": "integer" }, "schedule_id": { "description": "SCOPE. ONE class, with from/to. Resolve with classes_find_classes.", "exclusiveMinimum": 0, "type": "integer" }, "to": { "description": "Inclusive range end, YYYY-MM-DD.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" }, "trainer_id": { "description": "SCOPE. ONE instructor, with from/to. Resolve with classes_find_resource kind:'trainer'.", "exclusiveMinimum": 0, "type": "integer" } }, "type": "object" }, "name": "sessions_cancel", "outputSchema": null }, { "description": "List **events** (scheduled sessions of classes) in the caller's company. Use this whenever you need to resolve an `event_id` from natural language (\"my next class,\" \"Monday's ballet,\" \"all swim sessions this week,\" \"Sarah's classes tomorrow\") before chaining into another tool like `sessions_get_attendance` or `sessions_mark_attendance`. With no filters at all, returns the company's **upcoming** scheduled sessions (from today onward, earliest first) — not just the caller's — so a bare call stays near-term instead of dumping years of history. **Any** filter you add returns the FULL matching set, including PAST sessions: pass a `schedule_id` to get a class's entire history (past + future), or use `from`/`to` for an explicit window. There is no `past` flag — past sessions are just a range with `from` set early (or omitted alongside another scope). Filters cover date window, course, schedule, trainer, place, room, segment, billing period, status, and event-type (over-capacity, substituted, cancelled, etc.). Each returned row includes denormalised names (trainer, place, event-number), the event's date and duration, `capacity`, `free_spots` (remaining places = capacity − going, or null for open/unlimited events — use this to answer \"which sessions still have space\"), and an `attendance_counts` object (`going`, `attended`, `noshow`, `canceled`, `canceled_late`, `waitlist`). Read-only — does not modify events.\n\n**Additional lecturers.** Two separate fields, and they mean different things. `additional_trainers` = who is actually working THAT session alongside the main instructor. `class_additional_trainers` = the parent class's roster of people ELIGIBLE to work it, who are not necessarily on that session. Answer \"who is helping on Wednesday?\" from `additional_trainers`, never from the roster. Both are always arrays (`[]` = nobody), and neither includes the main instructor, who stays in `trainer_id`/`trainer_name`. Each entry is `{trainer_id, trainer_name, role}`; `role` is the raw enum — show it to operators as `secondary` = \"Secondary instructor\", `assistant` = \"Assistant\", `helper` = \"Assistant instructor\", `trainer` = \"Instructor\". `trainer_name` can be null if the lookup failed — that is not proof the trainer is gone. To CHANGE any of this, use `trainers_add_helpers`.\n\n**Critical: \"my sessions\" / \"what am I teaching\" / \"my classes today\".** When the user is asking for THEIR OWN sessions (any first-person framing), you MUST pass `trainer_id` matching `whoami.identity.user_id`. Without it, this tool returns every trainer's events in the company — which is almost never what the user meant when they said \"my.\" The only exception: when the caller's role is `member` or `external_member`, the server silently auto-scopes to their assignments anyway; `meta.scoped_to` in the response flags when this has happened.\n\nFilter notes:\n- `trainer_id` matches across FIVE trainer relationships including pre-substitution and schedule-level extras. Treat it as \"events trainer X is connected to,\" not strictly \"events trainer X currently teaches.\"\n- `status` uses raw db terms: `scheduled` (default — only state attendance can be tracked on), `unplanned` (includes cancelled events), `finished`, or `any`.\n- `segment_id=[0]` is a sentinel matching events with NO segment assignment.\n- Counters in `attendance_counts` may be sub-second-stale; for real-time counts on one event, chain into `sessions_get_attendance`. DISPLAYING A CLASS'S TIMETABLE: when the user wants to SEE a class's sessions (e.g. viewing or COPYING a class), render them as a weekly GRID — days across the top (Mon–Sun), time down the left, like the Zooza app calendar — collapsed to the weekday+time pattern with the run range + session count in a one-line caption; list individual dates only if the user explicitly asks. (Display only — ignore when you are merely resolving an event_id to chain into another tool.)", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "billing_period_id": { "$ref": "#/properties/course_id", "description": "Restrict to sessions in one or more billing periods (term blocks). Resolve with classes_find_billing_periods." }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "anyOf": [ { "exclusiveMinimum": 0, "type": "integer" }, { "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" } ], "description": "Restrict to sessions of one or more programmes (courses). Resolve with classes_find_courses." }, "date": { "description": "YYYY-MM-DD, exact-day match.", "type": "string" }, "from": { "description": "YYYY-MM-DD, inclusive lower bound on event date. To see PAST sessions, set this (e.g. from a schedule's start) — there is no `past` flag; past + future is simply an unbounded-below range.", "type": "string" }, "ids": { "description": "Specific event (session) ids to fetch. Bypasses the upcoming/scheduled defaults so the requested rows come back as-is.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "page": { "description": "0-based page index (default 0).", "minimum": 0, "type": "integer" }, "page_size": { "description": "Number of results per page (max 200).", "maximum": 200, "minimum": 1, "type": "integer" }, "place_id": { "$ref": "#/properties/course_id", "description": "Restrict to sessions at one or more venues (places). Resolve with classes_find_places." }, "room_id": { "$ref": "#/properties/course_id", "description": "Restrict to sessions held in one or more specific rooms within a venue." }, "schedule_id": { "description": "Restrict to sessions belonging to one class. Resolve with classes_find_classes.", "exclusiveMinimum": 0, "type": "integer" }, "segment_id": { "description": "Schedule-segment id(s). Pass [0] to match events with NO segment assignment (sentinel).", "items": { "minimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "sort": { "description": "Result ordering. date_asc/date_desc sort by session date; event_no_asc/event_no_desc by event number; created_asc/created_desc by record creation time. Default date_asc.", "enum": [ "date_asc", "date_desc", "event_no_asc", "event_no_desc", "created_asc", "created_desc" ], "type": "string" }, "status": { "description": "Event lifecycle status. Default \"scheduled\" (matches dashboard; attendance can only be tracked on scheduled events). \"unplanned\" covers cancelled events. \"any\" expands to (scheduled, unplanned).", "enum": [ "scheduled", "unplanned", "finished", "any" ], "type": "string" }, "to": { "description": "YYYY-MM-DD, inclusive upper bound on event date.", "type": "string" }, "trainer_id": { "$ref": "#/properties/course_id", "description": "Restrict to sessions an instructor is connected to (one or more). Resolve with trainers_find. See the trainer filter note above — this matches across five trainer relationships." }, "type": { "description": "Event-shape filter. \"cancelled\" surfaces events explicitly cancelled (server-side maps to status=unplanned). Other values target dashboard cases: oversold, undersold, ad-hoc replacements, etc.", "enum": [ "over_capacity", "under_capacity", "custom_replacement", "rescheduled", "substituted", "cancelled" ], "type": "string" } }, "type": "object" }, "name": "sessions_find_events", "outputSchema": null }, { "description": "Read who's enrolled in **one event** (a single session of a class) and their current attendance, so you can show the list and then mark it. Pass an `event_id`; the tool returns each enrolled attendee, their current attendance value (if already marked), and per-row context the LLM needs to mark attendance correctly: `allowed_statuses[]` (the statuses the **current caller** is permitted to set for THIS attendee), `is_trial` / `is_last_trial_session` flags, warnings about cross-company or cascade-sensitive (full2) cases, and — for open-type registrations only — `entrance_voucher` info (how many unused vouchers the attendee has, and whether one is already spent on this event). Use this **before** `sessions_mark_attendance` whenever the user has not already dictated the full list of attendees and marks — typically: \"open attendance for X,\" \"who's enrolled in tomorrow's class,\" \"show me Monday's attendance.\" If the event's course has attendance tracking disabled, the tool returns an `attendance_tracking_disabled` error rather than an empty list. This tool is read-only — it never writes attendance, notes, or summaries.\n\n**Talking to the user — vocabulary.** Zooza's customers are activity brands — dance, swim, language, sport, STEAM schools. Call this **\"attendance,\" \"the attendance list,\" \"the class list,\" or \"who's coming.\"** Don't expose the tool name or use sports/HR jargon (\"roster\") — it reads as foreign to these businesses. When the user asks to \"see attendance\" / \"open the register\" / \"who's in Monday's class,\" just call this tool and render the list directly.\n\n**Attendee vs client (critical for children's-class programmes).** Each row carries TWO people:\n- `attendee` — who actually shows up to the session. Often a child (Zooza data-model name: `customer`). May have `user_id: 0` when they aren't a registered account holder, which is normal for children. `attendee.date_of_birth` is available.\n- `client` — the account holder / payer (Zooza data-model name: `buyer`). Usually the parent. Has a real `user_id`. Contact info (`email`, `phone`) lives on the client when the attendee is a child; copy from client when speaking to / messaging the family.\n- `display_name` — a pre-formatted one-line label. When attendee == client (adult attending themselves), just the one name. When they differ, `attendee_name (client_name)` — e.g. `\"Jozko Jozko (Martin Rapavy)\"`. Use this when listing attendees; the LLM doesn't need to compose it from scratch.\n\nResponse shape notes:\n- `allowed_statuses[]` already factors in the caller's role, `company.trainer_attendance_management`, and the row's cross-company state. Do not propose a status not in this array — refuse locally and explain instead of calling `sessions_mark_attendance` to discover the constraint.\n- `is_last_trial_session` is currently `null` in V1 (derivation requires either a new api-v1 field or extra per-row lookups; deferred). Treat `is_trial=true` as the trigger for caution — a future enrichment will tighten this.\n- `entrance_voucher` is non-null only when `course.registration_type=\"open\"`. Check it before setting `sessions_mark_attendance`'s `use_voucher=true` on a `going` write.\n- `summary` block at the top level surfaces whether this event already has a public / internal session summary (`public_set` / `internal_set`), whether the public one is locked, and whether the caller's role is permitted to write summaries (`writable_by_caller`). After the user has marked attendance, the LLM can use this to offer `sessions_add_summary` as a follow-up when appropriate.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "event_id": { "description": "Target event id (one session of a class). Required.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "event_id" ], "type": "object" }, "name": "sessions_get_attendance", "outputSchema": null }, { "description": "Record per-attendee attendance for **one event** (a single session of a class — e.g. \"Monday Ballet on 2026-06-03 at 09:00\"). Pass an `event_id` and a list of attendees, each with their own attendance value (`attended`, `noshow`, `canceled`, `going`, `ignore`). Each value is set on **that one attendee for that one event**, never on the event as a whole. The tool writes each attendee individually and returns a per-row outcome. Use this **after** you already know the event and the attendees you want to mark — typically because the user dictated them or because you previously called `sessions_get_attendance`. If you don't yet know which event or who's enrolled, call `sessions_find_events` or `sessions_get_attendance` first. This tool does **not** cancel or reschedule the event itself or handle trialist follow-ups — those are separate tools.\n\n**Follow-up chaining.** The response includes a top-level `summary` block with `public_set` / `internal_set` / `writable_by_caller` flags. After a successful mark, if `summary.public_set=false` AND `summary.writable_by_caller=true`, proactively offer the user the option to write a parent-visible recap via `sessions_add_summary`. If `writable_by_caller=false`, don't offer (the caller's role can't write summaries). If `public_set=true`, don't volunteer an update unless asked.\n\n**Trial follow-ups.** A per-row `pending_action: \"trial_followup\"` (with `todo_id`) means that attendee just completed their trial by being marked `attended` — a follow-up (parent feedback + continuing-class recommendation) is now pending. Tell the user it's waiting and offer to handle it; the attendance skill resolves it against the todo. This tool only surfaces the hint — it does not orchestrate the follow-up. If the field is absent, there's nothing pending.\n\nAttendance value semantics:\n- `attended` — attendee was present.\n- `noshow` — attendee did not show up and did not warn.\n- `canceled` — attendee cancelled (admin-recorded). Triggers server-side make-up credit creation automatically when the programme allows it; do not call any other tool to issue credits.\n- `going` — pre-event RSVP / \"planning to attend.\" Restricted for member/receptionist roles under `trainer_attendance_management=\"limited\"`.\n- `ignore` — hide this event from the attendee's history (Zooza-specific; rare).\n\n`use_voucher` is a tentative V1 design: only meaningful when `attendance=\"going\"` AND `course.registration_type=\"open\"`. Check the attendee's `entrance_voucher.unused_entrance_vouchers > 0` (from `sessions_get_attendance`) before setting it to true; the server silently downgrades to cash debt when no voucher is available.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "attendees": { "description": "Per-attendee marks. At least one item required. Each item targets ONE attendee on the event by registration_id; the attendance value is set per attendee, not on the event as a whole.", "items": { "additionalProperties": false, "properties": { "attendance": { "description": "This attendee's attendance for this one event: 'attended' = was present; 'noshow' = absent without warning; 'canceled' = admin-recorded cancellation (auto-issues make-up credit when the programme allows); 'going' = pre-event RSVP/planning to attend; 'ignore' = hide this event from the attendee's history (rare).", "enum": [ "attended", "noshow", "canceled", "going", "ignore" ], "type": "string" }, "cancellation_reason": { "description": "Free-text reason accompanying a cancellation; only meaningful when attendance='canceled'.", "type": "string" }, "registration_id": { "description": "Identifies the enrolled attendee to mark — one registration row per attendee on this event (from sessions_get_attendance). Not the event id, not the client's user id.", "exclusiveMinimum": 0, "type": "integer" }, "use_voucher": { "description": "Tentative V1. Only meaningful when attendance='going' AND the course.registration_type='open'. Set true to spend an entrance voucher instead of accruing cash debt; check the attendee's entrance_voucher.unused_entrance_vouchers > 0 first (the server silently downgrades to cash debt when none is available).", "type": "boolean" } }, "required": [ "registration_id", "attendance" ], "type": "object" }, "minItems": 1, "type": "array" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "event_id": { "description": "Target event id (one session of a class). Required.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "event_id", "attendees" ], "type": "object" }, "name": "sessions_mark_attendance", "outputSchema": null }, { "description": "Edit specific individual sessions (events) of a class, OR add new sessions to a class. Two modes, one tool.\n\nEDIT-MODE — pass `event_ids` + `changes`: reschedule a session's date/time, or change a hand-picked session's instructor, venue/room, block, or duration. Works on one session or a chosen set.\n\nADD-MODE — pass `schedule_id` + `sessions`: CREATE one or more new sessions on an existing class (e.g. \"add one more session at the end\", \"add a make-up class on 2026-05-04\"). Each new session needs a `date`; its time, duration, trainer, venue and room default from the class. To append after the last session, first resolve the class's latest session with sessions_find_events, then pass the next date. New sessions are created billable so a priced class keeps charging.\n\nThe two modes are mutually exclusive — send event_ids/changes OR schedule_id/sessions, never both.\n\nTWO CALLS either way. First WITHOUT `token`: returns a preview (per-session before→after for edits, or the list of sessions to be created for adds) plus a single-use token. Show it to the operator and get explicit approval (and, if `notify` is set, confirm that clients will be emailed). Then call again with `token` + `confirmed: true` to apply — send nothing else, the plan is frozen.\n\nUse EDIT-MODE when the user points at particular sessions (\"move next Tuesday's class to Wednesday 5pm\", \"give Friday's session to Jana\"). To change an attribute across ALL or all upcoming sessions of a class in one go, use classes_update with session_scope instead. To cancel sessions, use the cancellation tools — this tool does not cancel.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "changes": { "additionalProperties": false, "description": "EDIT-MODE. The edits to apply to `event_ids`.", "properties": { "duration": { "description": "New session length in minutes.", "exclusiveMinimum": 0, "type": "integer" }, "place_id": { "description": "Move the session(s) to a different venue (place); must be sent together with room_id. Resolve with classes_find_places.", "exclusiveMinimum": 0, "type": "integer" }, "reschedule": { "anyOf": [ { "additionalProperties": false, "properties": { "date": { "description": "New session date, YYYY-MM-DD.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "mode": { "const": "set", "description": "Reschedule mode: move the session(s) to an explicit date (and optional time).", "type": "string" }, "time": { "description": "New start time, HH:MM. Omit to keep each session's existing time.", "pattern": "^\\d{2}:\\d{2}$", "type": "string" } }, "required": [ "mode", "date" ], "type": "object" }, { "additionalProperties": false, "properties": { "mode": { "const": "unify_time", "description": "Reschedule mode: set every selected session to the same time on its existing date.", "type": "string" }, "time": { "description": "Start time, HH:MM, applied to every selected session.", "pattern": "^\\d{2}:\\d{2}$", "type": "string" } }, "required": [ "mode", "time" ], "type": "object" }, { "additionalProperties": false, "properties": { "days": { "description": "Days to shift each session by; negative moves it earlier.", "type": "integer" }, "minutes": { "description": "Minutes to shift each session's time by; negative moves it earlier.", "type": "integer" }, "mode": { "const": "shift", "description": "Reschedule mode: move each session by a relative offset (days and/or minutes).", "type": "string" } }, "required": [ "mode" ], "type": "object" } ], "description": "Move the selected session(s) to a new date/time. Pick a mode: set (explicit date), unify_time (same time on existing dates), or shift (relative offset)." }, "room_id": { "description": "Room within the venue for the session(s); must be sent together with place_id.", "minimum": 0, "type": "integer" }, "segment": { "anyOf": [ { "minimum": 0, "type": "integer" }, { "type": "string" } ], "description": "Block: existing segment id (int), a new block name (string, auto-created), or 0 to clear." }, "trainer_id": { "description": "Reassign the session(s) to a different instructor. Resolve with trainers_find.", "exclusiveMinimum": 0, "type": "integer" }, "trainer_rate_type_id": { "description": "Set the instructor pay-rate type for the session(s). Resolve with trainers_find_rate_types.", "minimum": 0, "type": "integer" } }, "type": "object" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "event_ids": { "description": "EDIT-MODE. Existing session ids to change; pair with `changes`. Resolve with sessions_find_events. Not with schedule_id/sessions.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "notify": { "description": "Default false. true emails enrolled clients about the change — confirm intent with the operator first. Set it on the FIRST call; it is frozen into the plan.", "type": "boolean" }, "schedule_id": { "description": "ADD-MODE. Class to append NEW sessions to; pair with `sessions`. Resolve with classes_find_classes. Not with event_ids/changes.", "exclusiveMinimum": 0, "type": "integer" }, "sessions": { "description": "ADD-MODE. New sessions to create on `schedule_id`. Each needs a date; time/duration default from the class. For \"one more at the end\", get the last session via sessions_find_events and pass the next date.", "items": { "additionalProperties": false, "properties": { "date": { "description": "New session date, YYYY-MM-DD.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "duration": { "description": "Length in minutes. Omit → class default.", "exclusiveMinimum": 0, "type": "integer" }, "time": { "description": "Start time HH:MM. Omit → class default.", "pattern": "^\\d{2}:\\d{2}$", "type": "string" } }, "required": [ "date" ], "type": "object" }, "minItems": 1, "type": "array" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "sessions_update", "outputSchema": null }, { "description": "Create a company-level payment plan template (\"splátková šablóna\") — the object that defines HOW a programme's price is collected: in how many instalments, how often, with what discount and rounding. A programme set to instalment collection produces NO instalment schedule until a template is attached, so this is the step that makes instalment billing actually happen.\n\nCRITICAL — the template does NOT carry the price. The amount always comes from the programme/class; the template only says how to split it. So \"€200 in 4 × €50\" is: programme price 200 (set via classes_add_course or classes_update_course_settings) PLUS this template with frequency: 'absolute', value: 4. The €50 is derived. Never put 50 in `value`.\n\nWhat `value` means depends on `frequency`:\n- `absolute` → the TOTAL NUMBER of instalments (4 = four payments). This is the usual choice for \"split into N\".\n- `after_events` → number of sessions per instalment (charge every N sessions).\n- `monthly` / `quarterly` / `half_yearly` / `yearly` → `value` is NOT used for dates; set `value_date` to the day of month to bill on (0 = anchor to the start date).\n- With `schedule_type: 'pay_as_you_go'` → `value` is a UNIT MULTIPLIER, not money: the client is charged value × the programme's unit_price. Keep it a small count.\n\n`schedule_type` must match the programme's price type: 'in_advance', 'single_payment' and 'by_attendance' work with a normal course fee; 'pay_as_you_go' is for recurring membership pricing. A template is NOT a class pass: a fixed bundle (\"5 classes for €X, valid 6 weeks\") cannot be modelled here — tell the operator it is not available through these tools rather than approximating it with a discount. Pass `course_id` to attach the template to a programme immediately — Zooza validates the combination and rejects a mismatch with the reason. Without `course_id` the template is created but attached to nothing (still fine — attach it later or in the app). Requires the edit_company permission.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "course_id": { "description": "Optional. Attach the new template to this programme right away. Zooza validates it against the programme's price type and rejects a mismatch. Resolve with classes_find_courses.", "exclusiveMinimum": 0, "type": "integer" }, "discount": { "description": "Default 'none'. A plan-level discount, e.g. to reward paying in one go.", "enum": [ "none", "absolute", "relative" ], "type": "string" }, "discount_value_absolute": { "description": "Used when discount is 'absolute'.", "minimum": 0, "type": "number" }, "discount_value_relative": { "description": "Percent, used when discount is 'relative'.", "maximum": 100, "minimum": 0, "type": "number" }, "frequency": { "description": "How often instalments fall. 'absolute' = a fixed TOTAL COUNT of instalments (see `value`). 'after_events' = every N sessions. The periodic ones bill on `value_date` each period.", "enum": [ "monthly", "quarterly", "half_yearly", "yearly", "after_events", "absolute" ], "type": "string" }, "name": { "description": "Operator-facing name, e.g. \"4 monthly instalments\". Strongly recommended — it appears in pickers.", "minLength": 1, "type": "string" }, "rounding_method": { "description": "Default 'none'. 'bata' is .99-style pricing.", "enum": [ "none", "round_down", "round_up", "round_half_up", "round_half_down", "bata" ], "type": "string" }, "schedule_type": { "description": "'in_advance' = pay ahead on a cadence (the usual instalment plan). 'single_payment' = one payment. 'by_attendance' = charged from attendance. 'pay_as_you_go' = membership pricing, where `value` becomes a unit multiplier on the programme's unit_price.", "enum": [ "single_payment", "in_advance", "by_attendance", "pay_as_you_go" ], "type": "string" }, "skip_empty_period": { "description": "Default false. true skips periods that contain no sessions.", "type": "boolean" }, "value": { "description": "Meaning depends on frequency — see the tool description. absolute → number of instalments; after_events → sessions per instalment; periodic → unused; pay_as_you_go → unit multiplier. NEVER a money amount.", "minimum": 0, "type": "number" }, "value_date": { "description": "Day of month to bill on, for the periodic frequencies. 0 (default) anchors to the start date.", "maximum": 31, "minimum": 0, "type": "integer" } }, "required": [ "schedule_type", "frequency" ], "type": "object" }, "name": "setup_add_payment_template", "outputSchema": null }, { "description": "Choose which payment plan templates a programme offers clients — attach new ones, and DETACH ones that should not be there. Detaching is the point: Zooza attaches templates by itself when a programme's price type or payment collection changes, and on a company with many templates that can silently put dozens of plans on a programme. This is how you clean that up.\n\nTWO CALLS. First WITHOUT `token`: writes nothing and returns what is attached now, what would be attached, and the exact attach/detach list. Show it to the operator. Then call again with `token` + `confirmed: true` to apply.\n\n`template_ids` is the COMPLETE list the programme should end up with — anything attached but missing from it is detached. Pass an empty array to detach everything. Detaching removes the plan from the programme and from its classes; bookings already ON that plan keep their existing payment schedule, so no client is re-billed, but new bookings can no longer pick it.\n\nUse setup_add_payment_template to CREATE a template. Use classes_find_courses to resolve the programme.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "course_id": { "description": "Required on the FIRST call. The programme, from classes_find_courses.", "exclusiveMinimum": 0, "type": "integer" }, "template_ids": { "description": "Required on the FIRST call. The COMPLETE set of payment template ids the programme should offer. Anything currently attached and not listed here gets detached. Empty array detaches everything.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "type": "array" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "setup_update_course_templates", "outputSchema": null }, { "description": "Submit user feedback about the Zooza MCP integration to the engineering team. Two paths:\n\n- 'path: \"github\"' — returns a prefilled issue-creation URL on the **public** `zooza-dev/zooza-mcp-server` repo. The user opens it in their browser and files the issue themselves (no MCP-side auth). The body MUST be fully anonymized (no user_id, company_id, company name, user email/name, course/class/event names, customer/client identifiers). The server runs a safety-net regex and will reject the call if obvious identifiers (long numbers, emails) remain.\n- 'path: \"internal\"' — files an issue on the user's behalf in the **private** `zooza-dev/zooza-mcp` repo, recording their authenticated user_id and company_id so engineering can follow up. Use this for users who don't have GitHub or prefer the private channel.\n\nALWAYS show the user the exact 'title' and 'body' and get explicit affirmative confirmation before calling — once invoked with 'path: \"internal\"', the issue is filed and cannot be undone from this tool. The 'feedback-nudge' skill (load via `get_skill name=feedback-nudge`) describes when to proactively offer this tool and how to anonymize properly.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "body": { "description": "Full feedback in markdown. For path='github' MUST be pre-anonymized — strip user_id, company_id, company name, user email/name, course/class/event names, customer/client info. For path='internal' the user/company context is added automatically by the server.", "minLength": 1, "type": "string" }, "category": { "description": "Optional. Drives GitHub labels. Pick the closest match — when in doubt use 'other'.", "enum": [ "bug", "feature_request", "praise", "other" ], "type": "string" }, "path": { "description": "Which feedback channel to use. Decided by asking the user 'do you have a GitHub account?' — yes → 'github' (returns a URL they open themselves), no → 'internal' (server files the issue on their behalf). NEVER surface the 'github'/'internal' labels or the words 'public'/'private' to the user — those are implementation detail. Use 'I have GitHub' / 'I don't have GitHub' as the user-facing option labels.", "enum": [ "github", "internal" ], "type": "string" }, "related_tool": { "description": "Optional. Snake-case name of the MCP tool the feedback is about (e.g. 'create_class', 'classes_find_courses'). For path='internal' it's embedded in the issue header; for path='github' it's omitted from the URL (mildly fingerprinting).", "type": "string" }, "title": { "description": "Short, search-friendly issue title (one line). Becomes the GitHub issue title verbatim.", "minLength": 1, "type": "string" } }, "required": [ "path", "title", "body" ], "type": "object" }, "name": "submit_feedback", "outputSchema": null }, { "description": "Create a to-do item for a Zooza operator — a task a human needs to action. Give it a `message` and the `to_user_id` of the person it's assigned to. Optionally link it to a record (`entity_type` + `entity_id`, e.g. a registration) so the operator can open the thing it's about, and set a `due_date`. Use this to escalate — e.g. a lead asked a question that needs a human reply. It creates an OPEN todo in Zooza's normal to-do list; it does not email anyone. There is no `inbound_reply` entity type — link a reply escalation to its registration instead.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "due_date": { "description": "Optional due date, YYYY-MM-DD.", "type": "string" }, "entity_id": { "description": "Id of the linked record. Required when entity_type is set.", "exclusiveMinimum": 0, "type": "integer" }, "entity_type": { "description": "Optionally link the todo to a record kind (e.g. 'registration'). Requires entity_id.", "enum": [ "registration", "event", "course", "schedule", "payment", "scheduled_payment", "person", "user", "system_message", "slack_message" ], "type": "string" }, "message": { "description": "The task text (≤500 chars). Required.", "maxLength": 500, "minLength": 1, "type": "string" }, "to_user_id": { "description": "The Zooza user id of the operator this todo is assigned to. Required. Resolve a person's id with classes_find_resource (kind:'trainer') — operators/instructors share that id space; never guess it. A wrong id silently creates a to-do nobody sees.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "message", "to_user_id" ], "type": "object" }, "name": "todos_add", "outputSchema": null }, { "description": "Change the status of a to-do item: `done` (completed), `cancelled` (won't do), or `open` (reopen). Only OPEN todos can be marked `done` or `cancelled`; a `done` or `cancelled` todo can only be reopened to `open`. Marking `done` stamps completion time automatically.", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "status": { "description": "Target status: 'open', 'done', or 'cancelled'.", "enum": [ "open", "done", "cancelled" ], "type": "string" }, "todo_id": { "description": "Id of the todo to update.", "exclusiveMinimum": 0, "type": "integer" } }, "required": [ "todo_id", "status" ], "type": "object" }, "name": "todos_mark", "outputSchema": null }, { "description": "Register **additional lecturers** — a second instructor, assistant, or helper — on one or more classes, and control which of their sessions each one actually works. This is NOT how you set or change a class's main instructor (that is `classes_update`, or `sessions_update` for one-off substitutions); additional lecturers are extra people who work *alongside* the main instructor. By default a lecturer you add here works **every** session of the class — that is the normal arrangement. Restrict one to certain days by giving that assignment `weekdays` (1=Monday … 7=Sunday), or to hand-picked sessions with `event_ids`. This handles the whole \"Martin works Mondays, Peter works Tuesdays, both on Wednesdays\" pattern across a programme's classes in one action. Select classes with `schedule_ids`, or the way operators say it — `course_id` plus `billing_period_id` (\"the Junior classes in Winter 2026\"). Resolve `trainer_id` first with `classes_find_resource kind:\"trainer\"`, and the programme and billing period with `classes_find_courses` and `classes_find_resource kind:\"billing_period\"`. `role` is one of `secondary` (\"Secondary instructor\", the default), `assistant` (\"Assistant\"), `helper` (\"Assistant instructor\") or `trainer` (\"Instructor\"), and is per CLASS — Zooza cannot give someone one role on Mondays and another on Wednesdays. This writes across every class and session you select, so it is a two-step tool: call it once with no token to get a plan naming every class and session count, show that to the operator, then call it again with the returned token and `confirmed: true`. To SEE who is currently assigned, use `sessions_find_events` (per session) or `classes_find_classes` (the class roster).", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "assignments": { "description": "Who to put on these classes. Entries for one trainer merge, so \"Martin Mondays, Peter Tuesdays, both Wednesdays\" is two entries: Martin [1,3], Peter [2,3].", "items": { "additionalProperties": false, "properties": { "event_ids": { "description": "Restrict to these exact sessions instead of weekdays; must be in the selected classes.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "from": { "description": "YYYY-MM-DD. Only sessions on or after this date.", "type": "string" }, "role": { "description": "Role on the class, default `secondary`. One role per person per class; cannot vary by session.", "enum": [ "secondary", "assistant", "helper", "trainer" ], "type": "string" }, "to": { "description": "YYYY-MM-DD. Only sessions on or before this date.", "type": "string" }, "trainer_id": { "description": "The lecturer to add. Resolve with classes_find_resource kind:\"trainer\".", "exclusiveMinimum": 0, "type": "integer" }, "weekdays": { "description": "Restrict to these weekdays, 1=Mon … 7=Sun. OMIT for the normal case — no restriction means EVERY session of the class, and then `session_scope` is required.", "items": { "maximum": 7, "minimum": 1, "type": "integer" }, "minItems": 1, "type": "array" } }, "required": [ "trainer_id" ], "type": "object" }, "type": "array" }, "billing_period_id": { "description": "Term block narrowing the programme. classes_find_resource kind:\"billing_period\".", "exclusiveMinimum": 0, "type": "integer" }, "clear_unlisted": { "description": "Default false: sessions in `existing_outside_rules` are left alone. True only once the operator confirms they meant \"these days and nothing else\".", "type": "boolean" }, "company_id": { "description": "Zooza company id to operate against. Optional: if the user has exactly one company, the server defaults to it — you can omit this field. With multiple companies, you MUST specify which; get the id list from `whoami.available_companies[].id`. If the user hasn't indicated which company they mean, ask them before guessing.", "exclusiveMinimum": 0, "type": "integer" }, "confirmed": { "description": "Required (true) on the apply call, alongside `token`. Asserts that you have SHOWN the user the preview from the first call and they approved it — not that you believe the change is correct. If the user has not seen the preview, show it and ask before setting this. Must be omitted on the preview call.", "type": "boolean" }, "course_id": { "description": "Programme whose classes to act on. Requires billing_period_id. Resolve with classes_find_courses.", "exclusiveMinimum": 0, "type": "integer" }, "deactivate": { "description": "Turn someone OFF on specific sessions, leaving them on the class roster.", "items": { "additionalProperties": false, "properties": { "event_ids": { "description": "Switch them off on these exact sessions.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "from": { "description": "YYYY-MM-DD. Only sessions on or after this date.", "type": "string" }, "to": { "description": "YYYY-MM-DD. Only sessions on or before this date.", "type": "string" }, "trainer_id": { "description": "The lecturer to switch off.", "exclusiveMinimum": 0, "type": "integer" }, "weekdays": { "description": "Switch them off on these weekdays, 1=Mon … 7=Sun.", "items": { "maximum": 7, "minimum": 1, "type": "integer" }, "minItems": 1, "type": "array" } }, "required": [ "trainer_id" ], "type": "object" }, "type": "array" }, "remove_from_class": { "description": "Remove these lecturers from the selected classes ENTIRELY — roster row AND every session assignment. Not reversible in one step; the preview states the session count.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "schedule_ids": { "description": "Classes to act on, by id (resolve with classes_find_classes). Use this OR course_id.", "items": { "exclusiveMinimum": 0, "type": "integer" }, "minItems": 1, "type": "array" }, "session_scope": { "description": "REQUIRED when an assignment has no weekdays/event_ids — that person lands on every session otherwise. `upcoming` (usual) / `all` (incl. past) / `class_only` (roster only). Ignored when restricted.", "enum": [ "all", "upcoming", "class_only" ], "type": "string" }, "token": { "description": "Omit on the FIRST call — that call previews the change and returns a token. Pass the token back on the SECOND call to apply the previewed change. Single-use, expires in 15 minutes; if it is expired or already used, run the preview again.", "type": "string" } }, "type": "object" }, "name": "trainers_add_helpers", "outputSchema": null }, { "description": "Returns the connected user's identity, the companies they can operate on, regional context, and the session's token state. Call ONCE at the start of every conversation.\n\nHow to interpret the response:\n\n- 'status: \"ok\"' — authenticated, at least one company available. Pick a 'company_id' from 'companies' for follow-up calls (ask the user if more than one exists).\n- 'status: \"no_companies\"' — authenticated but no companies linked. Surface 'status_message' verbatim. No other tool will work.\n- 'status: \"invalid_user\"' — account rejected by api-v1. Surface 'status_message' verbatim.\n- 'status: \"api_error\"' — api-v1 unreachable. Surface 'status_message' and suggest retry.\n\nIdentity fields (use these to scope follow-up calls to the calling user):\n- 'identity.user_id' — the caller's Zooza user id. Pass this as 'trainer_id' to filter sessions_find_events / sessions_get_attendance / etc. to the user's OWN data whenever the user says \"my sessions,\" \"my classes today,\" \"what am I teaching tomorrow,\" etc. Without it, sessions_find_events returns ALL company events, not just the caller's.\n- 'identity.email', 'identity.name' — for display only.\n\nRegional context fields (use these to adapt behaviour):\n- 'server_region' — which Zooza installation this token routes to, taken from the JWT 'region' claim: \"eu\" (SK/CZ/DE/RO/HU/IT/PL), \"uk\", \"us\", \"asia\". This is the API-instance region, NOT the company's market region. Null in dev-fallback (no JWT).\n- 'company.region' — the company's MARKET region code (e.g. \"sk\", \"cz\", \"de\", \"en\"). Different axis from 'server_region': one EU instance ('server_region: \"eu\"') serves Slovak, Czech, German, … companies.\n- 'company.locale' — BCP-47 locale for date/number/currency formatting (e.g. \"sk-SK\", \"cs-CZ\", \"en-GB\").\n- 'company.language' — the company's primary language code.\n- 'company.currency' — the company's currency (e.g. \"EUR\", \"CZK\", \"GBP\").\n\nUse 'company.region' and 'company.language' to resolve terminology: a Slovak company saying \"kurz\" means Programme; a Czech company saying \"lekce\" means Session. When region context is available, skip asking the user to clarify language.\n\nBranding (per company in 'companies[].branding'):\n- 'branding.logo' (boolean) and 'branding.primary_color' (#hex or null) report what brand assets were cached for this company. Use 'primary_color' to brand a report artifact you compose for the client; the actual logo image is held server-side and never passes through you. Null branding = fetch failed or nothing configured; default to Zooza styling.\n\nFeedback context (used by the 'feedback-nudge' skill):\n- 'last_feedback_at' — ISO timestamp of the user's last MCP feedback submission, or null if never. Drives the skill's 7-day cool-off on proactive feedback nudges.\n- 'feedback_count' — total submissions to date (0 if never).\n\nTool-list freshness (Zooza has no marketplace auto-update, so proactively spot an out-of-date connector). There are TWO independent staleness signals — check BOTH:\n1. NEW or REMOVED tools — compare 'tool_surface.names' (every tool the live server exposes) against the tools you actually have available in THIS session. Any name in that list you do NOT have = the client is missing newly-added tools.\n2. UPDATED tools (same names, changed behaviour) — compare 'server_version' in this response against the [SURFACE_BUILD: …] marker at the very end of THIS tool description. If they differ, the tool DEFINITIONS you currently hold are from an OLDER build even when every tool name still matches — an existing tool's description, inputs, or behaviour changed after the client last connected. (This is why a deploy that only tweaks existing tools is still detectable.)\nIf EITHER signal fires, tell the user once — briefly, not naggingly — that their Zooza tools are out of date, and how to refresh: in Claude Desktop or Claude Code toggle the Zooza connector off then on (or reconnect / reinstall it); a browser client may just need a reload. Do NOT prompt when neither signal fires.\n\nNever surface 'sub' or 'scopes' to the user — diagnostic only.\n\n[SURFACE_BUILD: 0.15.0]", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": {}, "type": "object" }, "name": "whoami", "outputSchema": null } ] }
Verify it yourselfcurl -s https://api.teppi.xyz/v1/evidence/sha256:510a1f1cc54055e31e42a6570b90fdc81ef945f9d8573776b9b4bb885d5b9e31 | sha256sum