# Wantly proposals guide for AI agents

How to prepare and edit sales proposals (offers) with the Wantly tools (MCP, API v2 `POST /api/v2/<action>`, in-app AI). MCP clients also get this guide as the resource `wantly://guides/proposals`; plain HTTP: `GET /api/v2/guide?topic=proposals`. Products come from the catalog; see the catalog guide (`guide_read`, topic `catalog`) for how products and variations work.

## Data model

- **Proposal**: name, number (`nr`, given automatically), client, status, dates (`created`, `deadline` = valid until), currency, language, whole-proposal discount (%), author and owner (users), a public link the client opens (`public_url`) and an editor link (`edit_url`).
- **Template**: a proposal with type `template`. It holds a design, text blocks, settings and usually typical lines. New proposals are copies of a template.
- **Lines** (in order):
  - `group` — a section header; the lines after it belong to that group until the next header.
  - `item` — a product or service: name, SKU, quantity, unit, unit price without VAT, cost, discount %, VAT %. Catalog lines keep `product_id` (and `variation_id`).
  - `text` — a text block (may contain HTML from the template).
  - `optional` items can be chosen by the client in the offer.
  - **Sub-lines** (`children`): lines under a top-level item, one level deep — parts, included services or a package's contents. `parent_line_id` in proposals_get shows the parent. Removing a line removes its sub-lines.
  - **Attribute values** (`attributes`) on an item: size, colour, engraving text, amounts. See *Attributes on lines*.
- **Totals** are calculated when you read the proposal, the same way the offer page does:
  - `total_without_vat`, `vat`, `total_with_vat` = **one-off** lines (after line and proposal discounts).
  - **Recurring lines** (subscriptions, `recurring: true`, e.g. 59 €/mėn) are not in that total: they are in `recurring_totals`, one entry per period (`mėn`, `12 mėn`…), as the page shows them.
  - When some lines are `accepted` (the client's choice, sometimes already set in a template), **only accepted lines count**.
  - `group_totals` has the one-off and recurring totals of each group. `totals_basis` explains which rules applied.
- **Groups can be tabs (alternatives).** When a proposal shows its groups as tabs (e.g. "Mėnesinė prenumerata" / "Metinė prenumerata"), the client picks **one** of them. Compare the options with `group_totals`; do not add the tabs together. Unless the proposal setting `groupinstotal` is on, the **offer page's total box adds up all tabs** (and so do the headline totals here). For alternatives that is misleading: with the user's consent set it with `proposals_update` `{"settings": {"groupinstotal": 1}}` (on the template too, so new proposals inherit it). Then the page and `total_*` show only the selected tab.
- **Layout markers.** Rows whose text is only numbers such as `1`, `1,1,1`, `2,1` (and column containers) set the column layout of the text blocks under them. `proposals_get` marks them `layout: true`. **Never edit, move or remove them**; the tools refuse unless `allow_layout` is set on the user's explicit request.
- **Statuses** are company specific: get them from `proposals_options`. Status `-1` (Draft) is an unsaved editor draft; it is listed there but cannot be set.
- **Recurring lines and periods.** A recurring line's period is its unit (`mėn`) or, with two quantities, `quantity2` + `unit2` (`1 × 12 mėn` → period "12 mėn"). A **yearly** price must be modelled as `quantity: 1, quantity2: 12, period: "mėn"` (or `period: "12 mėn"`), not as `quantity: 12` with period "mėn" — otherwise the page shows a year's sum as "/ mėn". Mark catalog products that are subscriptions (modules, plans) with `products_update` `recurring: true`; new lines then take it over.

## Preparing a proposal (workflow)

1. `proposals_templates` — show the templates to the user and **ask which one to use** (or none). Do not pick one silently.
2. Client: `contacts_search` by name, company code, e-mail or phone → `contact_id`. If the client is not found, confirm the details with the user and create it with `contacts_create` (name, code, vat, person, email, phone, address, city, website). It refuses a duplicate (same company code, or same name without a code) and returns the existing contact's id instead; `update_existing: true` updates that contact. To correct a known contact use `contacts_update` (only the given fields change); `contacts_get` shows all its fields and latest proposals. Passing `contact` {…} directly to `proposals_create` also creates or updates a contact.
3. Products: `products_search` (catalog tools) to find SKUs. A variable product must be added through one variation (`variation_sku`); `products_get` lists variations.
4. `proposals_create` with `name`, `template_id`, client and `items`. Use `dry_run: true` first when unsure; show the user the lines and totals.
   - `template_lines: false` keeps the template's design, text blocks, headers, footers, groups and layout, without its priced lines.
   - The new proposal's `owner` is the author (you / the API user) unless you give `owner`; it is not taken from the template.
   - Items without `price` take the catalog price for that quantity (quantity tiers). Give `price` only when the user names one.
   - Below the product's smallest tier (e.g. 2 pcs of a product sold from 10 pcs) the smallest tier's price is used and a `warnings` entry says so; ask the user.
   - VAT comes from the product, like in the editor: a product with VAT 0% gives a 0% line even when the company default is 21% (a `warnings` entry says so). Give `vat` when the user wants another rate.
   - Read `warnings` in the result of `proposals_create` / `proposals_add_items` and tell the user about them.
   - `group` on an item puts it into that group; an existing template group gets the item at its end.
5. Give the user the `edit_url` (editor), `public_url` (client link) and, if useful, `downloads` (PDF / Excel / Word) from `proposals_get`.
6. Send it only when the user asks: see *Sending to the client*. The user can also send it themselves in Wantly.
7. Later: `proposals_analytics` shows whether and how the client read it (views, reading time, scroll depth, which lines interested them).

## Sending to the client

`proposals_send` e-mails the proposal link to the client, like the editor's **Send → Send email** button. The e-mail goes out from the company's Wantly mail account with the proposal author as the sender name and reply-to, so the client's answer goes to the author.

1. **Only when the user asks you to send.** Never send on your own initiative, and never as a side effect of preparing or editing a proposal.
2. **Preview first:** call it with `dry_run: true`. Nothing is sent and nothing changes; the result shows `email.to`, `cc`, `from_name`, `reply_to`, `subject`, the full `text` and the `link`, and the status change.
3. **Confirm with the user:** show the recipient(s), subject and text, and send only after the user confirms them. If the user changes anything, preview again.
4. Send with the same input and without `dry_run`. `sent: true` means the mail server accepted it.

Input: `id` (or `url`); `to` (default = the client contact's e-mail; refused when there is none); `cc` (list); `subject` (default = the proposal name); `message` (plain text; default = a short greeting in the proposal's language; the proposal link is added under it unless the text already contains it; the author's name is added as a signature); `set_status` (default true: a *Pending* or *Ready* proposal becomes *Sent*; later statuses are never changed). Write `message` in the proposal's language. Templates and unsaved drafts cannot be sent. Needs the `proposals.send` scope.

Errors: `invalid_input` (no recipient, bad e-mail, template, draft), `send_failed` (mail server error; nothing was changed — tell the user, do not retry in a loop), `mail_not_configured` (the account has no mail sender; the user sends it from Wantly).

## Attributes on lines

A catalog line automatically gets the product's own attribute values, exactly like the editor (for a variation also its variation values, e.g. Color = C001). `attributes` adds to or replaces them per attribute:

```json
"attributes": [
  {"attribute": "Dydis", "values": ["M", "L"]},
  {"attribute": "Graviravimas", "text": "Jonas"},
  {"attribute": 63, "number": 42}
]
```

- `attribute`: id or exact name (`attributes_list`). If several attributes share the name, use the id.
- Select / colour attributes (types 0, 1): `values` are names or ids of the attribute's existing values. An unknown value is an error listing the allowed ones; add new values only with the user's consent (`attributes_add_values`).
- Text attributes (type 2): `text`. Amount attributes (type 3): `number`.
- Removing: `values: []`, `text: ""` or `number: null` (in `proposals_update_items`).
- Included / related products and print settings (types 4–7) are not line attributes: add sub-lines with `children` instead.

## Sub-lines

```json
{"sku": "SOFA-1", "children": [
  {"name": "Surinkimas", "price": 40},
  {"sku": "PAG-2", "quantity": 2}
]}
```

`proposals_add_items` with `parent_line_id` adds a sub-line under an existing top-level line (after its other sub-lines). Sub-lines cannot have sub-lines; groups and text blocks cannot be parents.

## Hanna CRM

When the company uses Hanna CRM (`proposals_options` → `hanna_crm_enabled`), a proposal can be linked to a Hanna **opportunity** (sales deal):

1. `hanna_clients_search` → client uuid; `hanna_opportunities_search` (client and/or text) → opportunity uuid.
2. `proposals_search` with `hanna_opportunity` shows proposals already linked to it (for a new version, edit or copy one of those).
3. `proposals_create` with `hanna_opportunity` (and optionally `hanna_client`): the name, client contact and author are taken from the opportunity unless you give them.
4. `proposals_update` with `hanna_opportunity` links or moves a proposal; `""` removes the link.

Every proposal summary has `hanna` (linked opportunity, client, when it was sent to Hanna) or null. Changing the status of a linked proposal moves the opportunity stage in Hanna, and the approved status sends the final lines to Hanna, so change statuses only when the user asks. Without the integration these tools return `hanna_disabled`.

## Combining templates

`proposals_add_template` adds another template to an existing proposal (e.g. a hotel offer + the Wantly services template): its lines, groups / tabs, text blocks, layout markers, images, sub-lines and recurring settings are copied. Its intro (`include_header`) and closing blocks (`include_footer`: reviews, FAQ, contacts) are left out by default so they are not duplicated. Place it with `after_line_id` / `before_line_id`, or `as_new_tab: true` (+ `tab_name`) for its own tab. Group names that already exist get " (2)". Use `dry_run` first; `added_line_ids` lists the new lines.

## Blocks, images, accepted lines

- `kind: "header"` / `"footer"` / `"text"` with `html` adds an intro, closing or text block. A text block can be a column of a layout marker: `parent_line_id` = the marker's line id.
- `image_ids` (existing images, e.g. from `products_get`) or `image_urls` (downloaded on a real run) put images on a line or block; `proposals_update_items` replaces them (`[]` removes).
- `accepted: true` marks a line as chosen by the client; when some lines are accepted, only those count in the totals.
- Removing a group header removes its lines but **keeps footer blocks** that sit in that group (they move to the previous group).

## Where lines go, moving lines

- `proposals_add_items` without a position puts a line at the end of its group / tab, **above footer blocks** (reviews, FAQ, contacts) that templates often keep in the last tab. A new group is added above the footers too.
- Exact place: `after_line_id` (after that line and its sub-lines; after a group header = first in the group) or `before_line_id`. With `parent_line_id` too, `after_line_id` may be a sibling sub-line.
- `proposals_move_items` moves a line with its sub-lines: `after_line_id` / `before_line_id`, `to_group` (another tab), `parent_line_id` (make it a sub-line; `0` = back to a top-level line). Group headers and layout markers are not moved.

## Editing text blocks (keep the formatting)

Text blocks, headers and footers are HTML (bold, line breaks, columns). `proposals_get` shows them as a short plain-text preview.

- **Change words** (a name, a date, a price in the text): `proposals_replace_text` with `find` / `replace`, e.g. `{"find": "Andriau", "replace": "Skaiste"}`. Only visible text changes; tags, links and layout stay. Use `dry_run: true` to see how many places change; `line_ids` limits it; `include_name` also changes the proposal title.
- **Rewrite a block**: read it with `proposals_get` `full_text: true` or `html: true` (gives `html`), edit the HTML, and send it back as `name` in `proposals_update_items`. Sending plain text over a formatted block is refused (it would remove the formatting) unless `replace_formatting: true`.

## Editing

- `proposals_search` — find proposals by text, client, status, author, dates; `templates: true` / `deleted: true` for templates or deleted ones.
- `proposals_get` — lines with their `id` (= `line_id`); `full_text: true` returns the HTML of text blocks and descriptions.
- `proposals_add_items`, `proposals_update_items` (by `line_id`), `proposals_remove_items` (removing a group header removes its lines).
- `proposals_update` — name, status, client, dates, currency, language, discount, author / owner, display settings; `as_template: true` saves it as a template.
- `proposals_delete` / `proposals_restore` — soft delete; only when the user asks.
- `proposals_send` — e-mail the proposal to the client; only when the user asks, after a `dry_run` preview the user confirmed.

## Rules

- Ask which template to use; ask before changing a proposal the user did not mention.
- Never invent prices or discounts; use catalog prices unless the user gives others.
- Do not change a proposal that has status *approved* or later unless the user asks explicitly.
- Every write tool accepts `dry_run: true` (everything runs, then rolls back).
- Sending e-mail cannot be undone: always confirm the recipient and the text with the user before `proposals_send` without `dry_run`.

## Error codes

Over MCP an error is a tool result with `isError: true` and the same body as API v2: `{"ok": false, "error": {"code", "message", "details"}}`.

`internal_error` (unexpected; the message has a reference to report — never details), `not_found` (proposal, template, line, product, variation, contact, user, attribute or attribute value, Hanna opportunity), `hanna_disabled` (Hanna CRM not set up), `hanna_error` (Hanna CRM did not answer), `invalid_input` (missing name, variable product without variation, unknown status, bad field, no e-mail recipient), `send_failed` (`proposals_send`: the mail server refused; nothing was changed), `conflict` (`contacts_create`: the contact already exists — details hold it), `forbidden` (the API key lacks the scope).

## API key scopes

| Scope | Tools |
|---|---|
| `proposals.read` | proposals_templates, proposals_options, proposals_search, proposals_get, proposals_analytics, hanna_clients_search, hanna_opportunities_search |
| `proposals.write` | proposals_create, proposals_update, proposals_add_items, proposals_add_template, proposals_update_items, proposals_move_items, proposals_remove_items, proposals_replace_text, proposals_restore |
| `proposals.delete` | proposals_delete |
| `proposals.send` | proposals_send (e-mails the client; a separate permission, not included in `proposals.write`) |
| `contacts.read` | contacts_search, contacts_get |
| `contacts.write` | contacts_create, contacts_update |

A key for an agent that prepares proposals typically needs `catalog.read`, `proposals.write` and `contacts.write` (write includes read). The main key from Settings → API has every scope.
