VoltsBook

Developers · Agents · Flows

Build the front door with an agent.

A guided-booking flow is the "What are you here for?" wizard that greets a visitor who does not know what to book, walks them through two or three questions, and lands them on the booking page with the right service already chosen. An AI agent holding the right key can build one, check it, and publish it — this page is the complete lesson: the grammar, the rules, and a worked example as the exact tool calls an agent makes.

Not connected yet? Connect an agent over MCP first — one config line, and every operation on this page arrives as a tool.

What a flow is — and what it can never touch

A flow is a small decision tree of cards. Each card asks one question and offers options; each option either leads to another card or ends the flow by handing the visitor to the booking page with a service — and optionally a staff member and a location — preselected. The published flow lives at its own public address, /flow/{business}/{slug}, wearing the business's own colours, with a plain link to skip the questions and book directly.

The part that makes flows safe to hand to an agent: a flow is a marketing layer only. It holds no price, no duration, no availability, no deposit and no rule of any kind. The only thing an end state produces is a link into the booking page, and the booking page re-derives every fact from the catalog. There is nothing an agent can write into a flow that sells anything at the wrong price, because nothing in the grammar can express a price.

Flows are managed through seven operations on the same /v1 API everything else uses, so they arrive automatically as MCP tools:

  • listFlows / getFlow— read flows, each with its live validation findings
  • createFlow— mint an unpublished draft with an address and a name
  • updateFlow— save the whole card tree in one document
  • publishFlow / unpublishFlow— the deliberate doors on and off the public address
  • deleteFlow— remove it, cards and all

The key needs the "Change settings" scope, ticked when the key is minted (Settings → API keys — or start from the "Full admin" preset). The business's plan must include the flow builder. A key without the scope gets a structured insufficient_scope refusal that names the scope to ask for.

The grammar

Three shapes, and everything on this page is made of them. The same descriptions travel inside the OpenAPI contract and every derived MCP tool schema, so an agent gets these rules even if nobody shows it this page.

The flow

slug (the public address — lower-case letters, digits and hyphens, unique per business), name, description, published, startCardId (the card visitors see first) and cards. Saving cards replaces the stored tree: a stored card whose id is not in the array is deleted. Read first, modify, send everything back.

A card

title is the question ("What are you here for?"), plus an optional description and imageUrl, and up to forty options in display order. Ids are remapped on save: reuse a stored id to keep a card across saves, or invent any string ("start", "size") for a new one — other cards' targetCardId and the flow's startCardId may reference either kind in the same request, and the server rewrites them all consistently. An id that is not one of this flow's own cards is treated as new, never as a reference — so a foreign or mistyped id cannot smuggle anything in.

An option

A label and exactly one of two actions. action: "go_to_card" navigates — set targetCardId. action: "book" ends the flow — set serviceId (one of the business's own, from listServices) and optionally staffId and locationId; the visitor lands on the booking page with those preselected. A "book" option may also carry an upsell: upsellServiceId offers a different service instead — accepting swaps the service in the handoff, and nothing is ever added on top.

The rules the validator enforces

Publishing runs a validator, and it is the same one the human editor runs — one implementation, shared — so an agent can never publish a flow the editor would refuse, and never gets refused for a rule the editor does not show. Every read of a flow returns the current findings in issues; each finding carries a machine code, a human sentence, and the cardId or optionId it is about.

Errors — these block publishing

  • no_cards The flow has no cards at all — there is nothing for a visitor to answer.
  • no_start_card / start_card_missing No card is chosen as the first one, or the chosen one was deleted. Set startCardId, or send cards and let the server pick the first.
  • empty_card A card has no options. A visitor who reaches it can neither move on nor book — a dead end on a public page.
  • option_missing_target A "go_to_card" option has no targetCardId — it navigates nowhere.
  • dangling_target A "go_to_card" option points at a card that no longer exists, usually after a save that dropped the card from the tree.
  • option_missing_service A "book" option has no serviceId — it ends the flow at nothing.
  • no_end_state No option anywhere books anything. The whole purpose of a flow is to end at a booking, so a flow that cannot reach one is refused.

Warnings — these never block

  • unreachable_card Nothing links to a card, so no visitor will ever see it. Usually work in progress; never blocks publishing.
  • cycle A card can be reached again from itself. Legal and often deliberate — a "start over" option is a cycle — so the validator notes it and moves on.

Drafts may be broken; published flows may not. The gate also re-runs when a published flow is edited, so an agent cannot walk a live flow into a broken state while leaving it public. Unpublishing never validates — taking a broken thing down is always allowed.

The loop an agent should follow

  1. 1

    Read the catalog first

    Every "book" option must name one of the business’s own service ids, and the server checks against the live catalog — so the agent starts with listServices and works from the ids that come back, never from memory.

  2. 2

    Create the flow — it is always born unpublished

    createFlow takes only the address and the name. Nothing is public yet: a draft may be saved broken as many times as it takes, which is what makes it safe for an agent to work in passes.

  3. 3

    Save the whole card tree

    updateFlow replaces the stored tree with what is sent. Invent readable client ids ("start", "size"), point options at them, and name the start card; the server remaps every id on save and keeps the references consistent.

  4. 4

    Validate by reading it back

    getFlow returns the saved document with a live issues list — the same findings the human editor sees. An empty list, or warnings only, means the flow will publish.

  5. 5

    Publish

    publishFlow runs the same validator the panel’s publish button runs. On success the flow serves at its public path immediately; on refusal every finding comes back structured, and the agent fixes the cards and tries again.

Worked example: a detailing shop, three cards

The goal: greet a visitor with "What are you here for?", route a full detail through a car-size question, and route ceramic coating straight to booking. These are the actual tool calls, in order, with the service names from our auto-detailing starter kit.

1 · Find the service ids

listServices {}
→ [
  { "id": "a1e6…", "name": "Full detail — small car",  … },
  { "id": "b2f7…", "name": "Full detail — medium car", … },
  { "id": "c3a8…", "name": "Full detail — large car",  … },
  { "id": "d4b9…", "name": "Ceramic coating — two layers", … },
  …
]

Ids below are shortened for reading; an agent uses the full uuids exactly as returned.

2 · Create the draft

createFlow {
  "slug": "whats-my-car-need",
  "name": "What does my car need?",
  "description": "Answer two questions and we'll point you at the right service."
}
→ { "id": "9f30…", "published": false, "path": "/flow/shine-co/whats-my-car-need",
    "issues": [ { "severity": "error", "code": "no_cards", … } ], … }

Born unpublished, and already reporting no_cards — which is fine: nothing is public yet.

3 · Save the whole tree in one document

updateFlow {
  "id": "9f30…",
  "startCardId": "start",
  "cards": [
    {
      "id": "start",
      "title": "What are you here for?",
      "options": [
        { "label": "A full detail", "action": "go_to_card", "targetCardId": "size" },
        { "label": "Ceramic coating", "action": "book", "serviceId": "d4b9…" }
      ]
    },
    {
      "id": "size",
      "title": "What size is the car?",
      "options": [
        { "label": "Small — hatchback or coupe", "action": "book", "serviceId": "a1e6…" },
        { "label": "Medium — saloon or estate", "action": "book", "serviceId": "b2f7…" },
        { "label": "Large — SUV, van or truck", "action": "book", "serviceId": "c3a8…",
          "upsellServiceId": "d4b9…",
          "upsellHeadline": "Protect it while it's spotless?" }
      ]
    }
  ]
}

"start" and "size" are client ids the agent invented; the server mints real ids and rewrites startCardId and the targetCardId through the same map. The large-car option carries an upsell: after choosing it, the visitor is offered the coating instead — a swap, never a silent addition.

4 · Validate — read it back

getFlow { "id": "9f30…" }
→ { "published": false, "issues": [], "cardCount": 2, … }

An empty issues array means the publish gate will pass. Warnings would be listed here too — they never block, but a careful agent mentions them to its human.

5 · Publish

publishFlow { "id": "9f30…" }
→ { "published": true, "path": "/flow/shine-co/whats-my-car-need", … }

The wizard is live at that path immediately, wearing the business's booking-page theme. Hand the path to the human as the link to put in an Instagram bio, a Google Business profile, or a "not sure what you need?" button.

When publish refuses — and how to repair

Suppose the agent had sent the "size" card without the option pointing at it ever being fixed — say the start card's first option targeted a card id that is not in the tree. The refusal is not prose to parse; it is the validator's findings, structured:

publishFlow { "id": "9f30…" }
→ 400 {
  "error": {
    "code": "validation_failed",
    "message": "“A full detail” on “What are you here for?” points at a card that no longer exists.",
    "details": {
      "issues": [
        { "severity": "error", "code": "dangling_target",
          "cardId": "e5c1…", "optionId": "f6d2…",
          "message": "“A full detail” on “What are you here for?” points at a card that no longer exists." },
        { "severity": "error", "code": "no_end_state",
          "message": "No option books anything, so this flow can never hand over to the booking page." }
      ]
    }
  }
}

The repair loop is mechanical, which is what makes it a good agent task: for each entry in details.issues, the code says what kind of problem it is and the cardId/optionId say where. The agent reads the flow with getFlow, fixes exactly those cards, saves the whole tree with updateFlow, and publishes again. Nothing was made public in between — a refused publish leaves the flow unpublished and untouched.

Two refusals that are not validation: insufficient_scope means the key was minted without "Change settings" — mint one with it, or start from the "Full admin" preset. feature_disabled means the business's plan does not include the flow builder; that is a plan conversation, not something an agent can code around.

Things worth telling your agent

Always read before writing

Saving replaces the whole card tree. An agent that edits from a stale copy deletes cards it never saw. The rule: getFlow, modify, updateFlow — in one sitting.

Use real service ids, freshly read

The server drops a "book" option from the public page if its service is later deleted or deactivated — the flow degrades gracefully — but authoring against ids from an old conversation starts broken. listServices first, every session.

Keep flows short

Two or three questions is the sweet spot: enough to route, not enough to lose the visitor. Every extra card is another place to drop off before the booking page.

Cycles are a tool, not a bug

A "start over" option that points back at the first card is a legal, sensible design. The validator flags it as a warning so a typo is noticed — an agent that meant it can say so and publish anyway.

Preview like a visitor

The flow's path serves the published document; while drafting, the issues list on every read is the agent's preview. If a human wants to see the draft rendered, the panel's flow editor shows exactly the same document with the same findings.

One flow per campaign is fine

Flows are cheap and each has its own address. A seasonal offer can be its own flow, published for the season and unpublished after — unpublishing keeps the document for next year.

Connect an agent over MCP · The full API reference · All developer doors

Take your first booking today.

Free plan, no card, and your trade already set up with real services and prices.

Start free