> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fynn.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Connect AI assistants like Claude directly to Fynn: search customers, retrieve invoices, create offers and start DATEV exports, all via natural language.

The Fynn MCP server implements the [Model Context Protocol](https://modelcontextprotocol.io) and makes the most important Fynn features directly accessible to AI assistants like Claude. Instead of opening a user interface, you describe what you need, and the assistant calls the appropriate tools.

Typical use cases:

* Query customer and invoice data in natural language
* Automatically populate offers, including price lookups and images
* Start DATEV cloud exports with a confirmation dialog

## Setting up the connection

### Endpoint

```
POST https://coreapi.io/portal/mcp
```

For the sandbox environment, replace the host with `preview.coreapi.io`.

### Transport

The server uses **Streamable HTTP** (MCP specification 2025-03-26). MCP clients that support this transport protocol connect directly to the endpoint above.

### Authentication

Every request requires two headers:

| Header             | Value                    |
| ------------------ | ------------------------ |
| `Authorization`    | `Bearer <API key>`       |
| `X-Fynn-Tenant-Id` | UUID of the organization |

Create an API key under [Settings > API Keys](https://app.fynn.eu/settings/api-tokens). You find the organization ID in your organization settings.

### Configuration example

Most MCP clients (e.g. Claude Desktop) accept a JSON configuration. Here is an example:

```json theme={null}
{
  "mcpServers": {
    "fynn": {
      "type": "http",
      "url": "https://coreapi.io/portal/mcp",
      "headers": {
        "Authorization": "Bearer <API-KEY>",
        "X-Fynn-Tenant-Id": "<ORGANIZATION-ID>"
      }
    }
  }
}
```

## Permissions

All tools respect the role permissions of the connected user. Anyone who is not allowed to read invoices in Fynn will not receive them through the MCP server either. In that case, the request fails with a clear error message rather than returning empty data.

***

## Tool reference

The following 31 tools are available, grouped by functional area.

### General

#### `fynn_ping`

Checks the connection, authentication, and organization context. Returns `ok`, the organization ID, and a timestamp. Takes no parameters.

***

### Customers

#### `customer_search`

Searches for customers by a search term or subscription number.

| Parameter            | Type    | Required | Description                                                              |
| -------------------- | ------- | -------- | ------------------------------------------------------------------------ |
| `search`             | string  |          | Partial match on name, company, customer number, or DATEV debtor account |
| `subscriptionNumber` | string  |          | Subscription number to resolve the associated customer directly          |
| `page`               | integer |          | Page number (1-based, default 1)                                         |
| `limit`              | integer |          | Results per page (max 50, default 20)                                    |

Returns identity fields only (ID, customer number, display name, type, status). No addresses or contact data.

***

#### `customer_balance`

Returns the debtor balance of a customer. Without a time range, returns the current total balance; with a time range, additionally returns debit/credit sums. Amounts are in cents with currency.

| Parameter    | Type   | Required | Description                                                   |
| ------------ | ------ | -------- | ------------------------------------------------------------- |
| `customerId` | string | yes      | UUID of the customer                                          |
| `yearMonth`  | string |          | Month in `YYYY-MM` format, e.g. `2026-01`                     |
| `from`       | string |          | Lower date boundary `YYYY-MM-DD` (alternative to `yearMonth`) |
| `to`         | string |          | Upper date boundary `YYYY-MM-DD` (alternative to `yearMonth`) |

***

#### `customer_payments`

Lists payment transactions for a customer, newest first. Includes date, amount (cents), status, payment method, and document number, but no bank details or IBAN.

| Parameter    | Type    | Required | Description                            |
| ------------ | ------- | -------- | -------------------------------------- |
| `customerId` | string  | yes      | UUID of the customer                   |
| `dateFrom`   | string  |          | Lower date boundary `YYYY-MM-DD`       |
| `dateTo`     | string  |          | Upper date boundary `YYYY-MM-DD`       |
| `page`       | integer |          | Page number (default 1)                |
| `limit`      | integer |          | Results per page (max 100, default 25) |

***

### Invoices

#### `invoice_search`

Searches invoices, credit notes, and cancellations. Filterable by status, document type, customer, and finalization date range. Amounts in cents, dates as ISO-8601.

| Parameter         | Type    | Required | Description                                                                   |
| ----------------- | ------- | -------- | ----------------------------------------------------------------------------- |
| `search`          | string  |          | Partial match on document number or customer number                           |
| `statuses`        | array   |          | Document statuses, e.g. `["STATUS_PAID", "STATUS_UNPAID"]`                    |
| `type`            | string  |          | Document type: `TYPE_INVOICE`, `TYPE_CREDIT`, `TYPE_CANCEL`, `TYPE_REFUND`    |
| `customerId`      | string  |          | UUID of the customer                                                          |
| `finalizedAfter`  | string  |          | Lower date boundary `YYYY-MM-DD`                                              |
| `finalizedBefore` | string  |          | Upper date boundary `YYYY-MM-DD`                                              |
| `sort`            | string  |          | Sort field: `finalizationDate`, `number`, `dueDate`, `creationDate`, `status` |
| `sortDirection`   | string  |          | `asc` or `desc` (default `desc`)                                              |
| `page`            | integer |          | Page number (default 1)                                                       |
| `limit`           | integer |          | Results per page (max 100, default 20)                                        |

***

#### `invoice_reference_chain`

Resolves the cancellation/reference chain of a document. Returns the document itself, the referenced original document, and all cancellations/credit notes that reference this document.

| Parameter       | Type   | Required | Description                                  |
| --------------- | ------ | -------- | -------------------------------------------- |
| `invoiceId`     | string |          | UUID of the document                         |
| `invoiceNumber` | string |          | Document number (alternative to `invoiceId`) |

***

#### `invoice_pdf_download`

Creates a time-limited download link for the PDF of a document. Returns `url`, `filename`, and `expiresAt`. No binary data is returned.

| Parameter      | Type    | Required | Description                                          |
| -------------- | ------- | -------- | ---------------------------------------------------- |
| `invoiceId`    | string  | yes      | UUID of the document                                 |
| `secondsValid` | integer |          | Validity period in seconds (60 to 3600, default 300) |

***

#### `invoices_zip_download`

Bundles the PDFs of multiple documents into a ZIP archive and returns a signed download link. Documents without a PDF are skipped.

| Parameter         | Type   | Required | Description                                              |
| ----------------- | ------ | -------- | -------------------------------------------------------- |
| `finalizedAfter`  | string |          | Lower date boundary `YYYY-MM-DD`                         |
| `finalizedBefore` | string |          | Upper date boundary `YYYY-MM-DD`                         |
| `statuses`        | array  |          | Status filter                                            |
| `type`            | string |          | Document type filter                                     |
| `customerId`      | string |          | UUID of the customer                                     |
| `zipName`         | string |          | Optional name for the ZIP archive without file extension |

### Creating and editing invoices

The write invoice tools require the `invoice:write` permission. An invoice follows a clear lifecycle: created as a draft, positions added and edited, then finalized or closed. A finalized invoice is immutable and legally binding; finalization therefore requires a manual approval by a human.

#### `invoice_create`

Creates a new invoice in draft status. Positions are added separately using `invoice_add_position` or `invoice_add_product_position`.

| Parameter      | Type   | Required | Description                                                                            |
| -------------- | ------ | -------- | -------------------------------------------------------------------------------------- |
| `customerId`   | string | yes      | UUID of the customer. The customer must be active.                                     |
| `currencyCode` | string |          | Currency code, e.g. `"EUR"`. Defaults to the customer's default currency.              |
| `dueDate`      | string |          | Due date in `YYYY-MM-DD` format. Defaults to the organization's standard payment term. |
| `title`        | string |          | Invoice title. Defaults to the title from the standard invoice template.               |
| `introduction` | string |          | Opening text.                                                                          |
| `closing`      | string |          | Closing text.                                                                          |
| `internalNote` | string |          | Internal note (not visible on the document).                                           |

Returns the new draft invoice with header data, positions, and totals (in cents).

***

#### `invoice_get`

Reads an invoice with header data, positions, and totals. The invoice ID can be a UUID or an invoice number.

| Parameter   | Type   | Required | Description                                |
| ----------- | ------ | -------- | ------------------------------------------ |
| `invoiceId` | string | yes      | UUID of the invoice or the invoice number. |

Returns: header data (id, number, status, type, customer, currencyCode, dueDate, finalizedAt, title, introduction, closing, internalNote), totals (`totals.net`/`tax`/`gross` in cents), positions, and an `editable` flag.

***

#### `invoice_update`

Updates the header fields of an invoice that has not yet been finalized. Only fields that are sent are changed; missing fields remain unchanged.

| Parameter           | Type   | Required | Description                                                         |
| ------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `invoiceId`         | string | yes      | UUID of the invoice.                                                |
| `title`             | string |          | New title.                                                          |
| `introduction`      | string |          | New opening text.                                                   |
| `closing`           | string |          | New closing text.                                                   |
| `internalNote`      | string |          | Internal note.                                                      |
| `dueDate`           | string |          | Due date in `YYYY-MM-DD` format.                                    |
| `customerAddressId` | string |          | UUID of an existing customer address to use as the billing address. |
| `paymentMethodId`   | string |          | UUID of an existing payment method for the customer.                |

Returns the updated invoice (same structure as `invoice_get`).

***

#### `invoice_add_position`

Adds a freely defined position to a draft invoice. Price and tax group must always be specified explicitly.

| Parameter            | Type   | Required | Description                                                                             |
| -------------------- | ------ | -------- | --------------------------------------------------------------------------------------- |
| `invoiceId`          | string | yes      | UUID of the draft invoice.                                                              |
| `name`               | string | yes      | Position name.                                                                          |
| `unitId`             | string | yes      | UUID of the unit.                                                                       |
| `unitPrice`          | string | yes      | Net unit price as a string in `"12.34"` format (decimal point).                         |
| `taxGroupId`         | string | yes      | UUID of the tax group.                                                                  |
| `quantity`           | number |          | Quantity (default 1, can be negative).                                                  |
| `description`        | string |          | Optional position description.                                                          |
| `discountPercentage` | number |          | Discount in percent (0-100). Mutually exclusive with `discountFixed`.                   |
| `discountFixed`      | string |          | Fixed net discount as a string `"12.34"`. Mutually exclusive with `discountPercentage`. |
| `serviceDateFrom`    | string |          | Service period start `YYYY-MM-DD`. Only together with `serviceDateTo`.                  |
| `serviceDateTo`      | string |          | Service period end `YYYY-MM-DD`. Only together with `serviceDateFrom`.                  |
| `productId`          | string |          | UUID of a product for statistics purposes (no price derivation).                        |

Returns `{ positionId, invoice }` where `invoice` contains the updated invoice with totals in cents.

***

#### `invoice_add_product_position`

Derives an invoice position from a product in the catalog. Name, unit, and tax group come from the product; the price comes from a flat-fee price plan or an explicit price override.

<Info>
  A price is never guessed. If neither a matching price plan nor a `unitPrice` override is provided, the tool rejects the request with a clear error message.
</Info>

| Parameter     | Type   | Required | Description                                                                 |
| ------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `invoiceId`   | string | yes      | UUID of the draft invoice.                                                  |
| `productId`   | string | yes      | UUID of the product to derive the position from.                            |
| `pricePlanId` | string |          | UUID of a flat-fee price plan for the product to derive the price from.     |
| `unitPrice`   | string |          | Price override as a string `"12.34"`. Takes precedence over the price plan. |
| `quantity`    | number |          | Quantity (default 1).                                                       |
| `taxGroupId`  | string |          | Tax group override (UUID). Defaults to the product's tax group.             |
| `name`        | string |          | Position name override. Defaults to the product name.                       |

Returns `{ positionId, invoice }` (totals in cents).

***

#### `invoice_update_position`

Updates a position on an editable invoice. Only fields that are sent are changed.

| Parameter            | Type   | Required | Description                                                                            |
| -------------------- | ------ | -------- | -------------------------------------------------------------------------------------- |
| `invoiceId`          | string | yes      | UUID of the invoice.                                                                   |
| `positionId`         | string | yes      | UUID of the position to update.                                                        |
| `name`               | string |          | New position name.                                                                     |
| `quantity`           | number |          | New quantity.                                                                          |
| `unitPrice`          | string |          | New net unit price as a string `"12.34"`.                                              |
| `taxGroupId`         | string |          | UUID of the tax group.                                                                 |
| `description`        | string |          | New position description.                                                              |
| `discountPercentage` | number |          | Discount in percent (0-100). Mutually exclusive with `discountFixed`.                  |
| `discountFixed`      | string |          | Fixed net discount as a string `"5.00"`. Mutually exclusive with `discountPercentage`. |
| `serviceDateFrom`    | string |          | Service date from (`YYYY-MM-DD`).                                                      |
| `serviceDateTo`      | string |          | Service date to (`YYYY-MM-DD`).                                                        |

Returns `{ positions[], totals }` (amounts in cents).

***

#### `invoice_remove_position`

Removes a position from an editable invoice. Remaining positions are renumbered and totals recalculated.

| Parameter    | Type   | Required | Description                     |
| ------------ | ------ | -------- | ------------------------------- |
| `invoiceId`  | string | yes      | UUID of the invoice.            |
| `positionId` | string | yes      | UUID of the position to remove. |

Returns `{ positions[], totals }` (amounts in cents).

***

#### `invoice_close`

Permanently closes an invoice without finalizing it. The document receives `STATUS_CLOSED` and is discarded: no document number, no PDF, no payment. Only possible from draft (`STATUS_DRAFT`) or `STATUS_NEW` status.

| Parameter   | Type   | Required | Description                            |
| ----------- | ------ | -------- | -------------------------------------- |
| `invoiceId` | string | yes      | UUID of the invoice.                   |
| `reason`    | string |          | Optional comment (max 255 characters). |

Returns `{ id, status }`.

***

#### `invoice_finalize`

Prepares the finalization of an invoice and creates an approval task in the app inbox. The invoice is **not** finalized directly.

Because a finalized invoice is legally binding and immutable, finalization follows a two-step process: the AI assistant validates that the invoice is ready to finalize (valid payment method, active customer) and creates an approval task. A human then reviews and approves or rejects it in the app inbox. Only after manual approval is the invoice actually finalized.

A second call for the same invoice returns the existing open task, as long as the invoice has not changed since the first request; the parameters of the original request are kept in that case. If the invoice was edited in the meantime, the old task is dismissed and a new one with a new `actionItemId` is created. Approval requests expire 72 hours after creation and must then be re-requested.

| Parameter   | Type    | Required | Description                                                                                            |
| ----------- | ------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `invoiceId` | string  | yes      | UUID of the invoice.                                                                                   |
| `dueDate`   | string  |          | Due date in `YYYY-MM-DD` format. Defaults to the organization's standard payment term on finalization. |
| `sendEmail` | boolean |          | Whether to send an email to the customer on approval (default: `true`).                                |

Returns `{ actionItemId, status: "pending_approval", invoiceId, summary, hint }`, where `summary` contains invoice totals (net, tax, gross in cents), position count and customer data, and `hint` is a reminder that human approval in the inbox is required. Use `pending_action_status` with the returned `actionItemId` to poll for the approval status and the expiry date. Permission: `invoice:finalize`.

***

#### `pending_action_status`

Queries the status of an approval task, for example one created by `invoice_finalize`.

| Parameter      | Type   | Required | Description                                        |
| -------------- | ------ | -------- | -------------------------------------------------- |
| `actionItemId` | string | yes      | UUID of the approval task from `invoice_finalize`. |

Returns `{ id, type, status, createdAt, resolvedAt, expiresAt, invoiceId, executions }`. `expiresAt` is the point in time (UTC) at which the approval request expires; after that, it must be re-requested via `invoice_finalize`.

Possible values for `status`: `open`, `snoozed`, `in_progress`, `resolved`, `dismissed`.

Each entry in `executions` contains `actionKey` (e.g. `approve` or `reject`), `resultStatus` (`success` or `failure`), `resultMessage`, and `executedAt`. On successful approval, `resultMessage` includes the assigned invoice number. If approval fails because the invoice was edited after the request or the request has expired, `resultMessage` explains the reason; the invoice remains unfinalized. Permission: `action-item:read`.

***

## Creating and finalizing an invoice with AI: a typical workflow

Here is how to create a complete invoice with an AI assistant and send it for approval:

<Steps>
  <Step title="Create the invoice">
    Create a new invoice for the desired customer:

    ```json theme={null}
    {
      "tool": "invoice_create",
      "arguments": {
        "customerId": "<CUSTOMER-ID>",
        "title": "Services July 2026",
        "dueDate": "2026-08-15"
      }
    }
    ```

    The response contains the `invoiceId` you need for all subsequent calls.
  </Step>

  <Step title="Add positions">
    Add positions from the catalog or freely defined positions:

    ```json theme={null}
    {
      "tool": "invoice_add_product_position",
      "arguments": {
        "invoiceId": "<INVOICE-ID>",
        "productId": "<PRODUCT-ID>",
        "pricePlanId": "<PRICE-PLAN-ID>",
        "quantity": 5
      }
    }
    ```

    Note: `unitPrice` always comes from the price plan or an explicit override, never from an estimate.
  </Step>

  <Step title="Review the invoice">
    Read the current invoice with all positions and totals before requesting finalization:

    ```json theme={null}
    {
      "tool": "invoice_get",
      "arguments": { "invoiceId": "<INVOICE-ID>" }
    }
    ```
  </Step>

  <Step title="Request approval">
    Create the approval task. The invoice is **not yet** finalized:

    ```json theme={null}
    {
      "tool": "invoice_finalize",
      "arguments": {
        "invoiceId": "<INVOICE-ID>",
        "sendEmail": true
      }
    }
    ```

    The response contains `actionItemId`, `status: "pending_approval"`, a `summary` of the invoice totals, and a `hint` text. Let the user know that the approval is waiting in the app inbox.
  </Step>

  <Step title="Human reviews and approves">
    A team member opens the inbox, reviews the invoice, and approves or rejects it. This step takes place exclusively in the app and cannot be bypassed by the AI assistant.
  </Step>

  <Step title="Poll for status">
    Poll for the approval status until the task has been processed:

    ```json theme={null}
    {
      "tool": "pending_action_status",
      "arguments": { "actionItemId": "<TASK-ID>" }
    }
    ```

    When `status: "resolved"` and `executions[0].resultStatus: "success"`, the invoice is finalized; `resultMessage` contains the assigned document number. When `resultStatus: "failure"`, the invoice was not finalized (for example, because it was edited after the request was created); create a new approval request in that case.
  </Step>
</Steps>

<Warning>
  An approved invoice is legally binding and immutable. The manual approval cannot be bypassed from the MCP server.
</Warning>

***

## Audit

Every tool call through the MCP server is automatically logged: user account, organization, tool name, parameters (without sensitive content), outcome, and execution duration. The audit log is immutable and provides traceability for compliance requirements. Failed and rejected calls are also recorded.

***

### DATEV

#### `datev_export_status`

Shows whether a DATEV cloud account is connected, and provides a monthly summary of exported and pending postings as well as the last export status.

| Parameter    | Type    | Required | Description                                                  |
| ------------ | ------- | -------- | ------------------------------------------------------------ |
| `monthsBack` | integer |          | Number of months back for the overview (1 to 36, default 12) |
| `yearMonth`  | string  |          | Month `YYYY-MM` to list the individual export runs for       |

***

#### `datev_cloud_export_start`

Starts a DATEV cloud export for one or more months. This tool writes data. Without `confirm: true`, a confirmation request is returned and nothing is executed.

| Parameter    | Type    | Required | Description                                           |
| ------------ | ------- | -------- | ----------------------------------------------------- |
| `yearMonth`  | string  |          | Single month `YYYY-MM`                                |
| `yearMonths` | array   |          | List of months `YYYY-MM` (alternative to `yearMonth`) |
| `confirm`    | boolean |          | Must be `true` to actually start the export           |
| `sendAgain`  | boolean |          | Re-export even if already exported (default `false`)  |

***

### Product catalog

#### `product_tree`

Returns the product catalog as a category tree. Each node contains `id`, `name`, `path`, `count` (number of products), and `children`. With `includeProducts: true`, additionally returns a flat product list.

| Parameter         | Type    | Required | Description                                                     |
| ----------------- | ------- | -------- | --------------------------------------------------------------- |
| `includeProducts` | boolean |          | When `true`, also returns a flat product list (default `false`) |
| `locale`          | string  |          | Language for product and unit names, e.g. `"de"` or `"en"`      |

***

#### `product_search`

Searches products in the catalog. Partial match on product name, internal name, and product number.

| Parameter         | Type    | Required | Description                                    |
| ----------------- | ------- | -------- | ---------------------------------------------- |
| `query`           | string  |          | Search term                                    |
| `familyId`        | string  |          | UUID of a product category                     |
| `includeArchived` | boolean |          | Include archived products (default `false`)    |
| `limit`           | integer |          | Maximum number of results (max 50, default 20) |
| `offset`          | integer |          | Offset for pagination (default 0)              |
| `locale`          | string  |          | Language for names, e.g. `"de"` or `"en"`      |

***

#### `product_prices`

Returns all price plans for a product in complete form. The returned price plan ID is exactly the `pricePlan` reference that a subscription proposal product entry needs. All amounts are integer cents.

| Parameter   | Type   | Required | Description         |
| ----------- | ------ | -------- | ------------------- |
| `productId` | string | yes      | UUID of the product |

Each price plan includes: `id`, `name`, `internalName`, `status`, `currency`, `chargeType`, `billingInterval`, `billingFrequency`, `payInAdvance`, `proRata`, `freeUnits`, `minimumCommitment`, and a `price` block with amounts in cents.

***

### Offers

The offer tools distinguish between read and write operations. Read tools (`offer_get`, `offer_block_types`) require `offer:read`. Write tools (`offer_create`, `offer_update`, `offer_add_block`, `offer_update_block`, `offer_remove_block`, `offer_upload_image`) require `offer:write`. Accepted or archived offers cannot be modified.

#### `offer_block_types`

Lists all supported block types with metadata (whether the block is atomic, whether it is allowed in PDF and document, and which attributes it accepts). With an optional `type`, also returns a concrete example node that can be passed directly to `offer_add_block`.

| Parameter | Type   | Required | Description                                            |
| --------- | ------ | -------- | ------------------------------------------------------ |
| `type`    | string |          | Block type, e.g. `"heading"`, `"subscriptionProposal"` |

***

#### `offer_get`

Reads the state of an offer efficiently: settings (ID, number, name, status, display language, customer, validity date, acceptance mode, custom variables) and a compact section and block overview. With `sectionIndex` (and optionally `blockIndex`), additionally returns the raw JSON body of exactly that section or block.

| Parameter      | Type    | Required | Description                            |
| -------------- | ------- | -------- | -------------------------------------- |
| `offerId`      | string  | yes      | ULID of the offer                      |
| `sectionIndex` | integer |          | 0-based section index for the raw body |
| `blockIndex`   | integer |          | 0-based block index within the section |

***

#### `offer_create`

Creates a new offer. All parameters are optional: an offer without a customer is explicitly allowed.

| Parameter        | Type   | Required | Description                                      |
| ---------------- | ------ | -------- | ------------------------------------------------ |
| `customerId`     | string |          | UUID of the customer                             |
| `name`           | string |          | Offer title                                      |
| `locale`         | string |          | Display language, e.g. `"de"`, `"en"`, `"de-DE"` |
| `validUntil`     | string |          | Validity date ISO-8601, e.g. `2026-12-31`        |
| `salesChannelId` | string |          | UUID or technical name of the sales channel      |
| `templateId`     | string |          | UUID of an offer template                        |

***

#### `offer_update`

Updates the settings of an offer. Only fields that are sent are changed; missing fields remain unchanged.

| Parameter         | Type   | Required | Description                                      |
| ----------------- | ------ | -------- | ------------------------------------------------ |
| `offerId`         | string | yes      | ULID of the offer                                |
| `name`            | string |          | New offer title                                  |
| `locale`          | string |          | New display language                             |
| `validUntil`      | string |          | New validity date (empty string clears the date) |
| `acceptanceMode`  | string |          | `click`, `esignature`, or `print`                |
| `customVariables` | object |          | Custom variables as a flat object                |

***

#### `offer_add_block`

Inserts a block into an offer section. Use `offer_block_types` to get the valid block node format. If the offer has no sections yet, one is created automatically.

| Parameter      | Type    | Required | Description                                                |
| -------------- | ------- | -------- | ---------------------------------------------------------- |
| `offerId`      | string  | yes      | ULID of the offer                                          |
| `block`        | object  | yes      | Tiptap node with `type` and optionally `attrs`/`content`   |
| `sectionIndex` | integer |          | 0-based section index (default 0)                          |
| `position`     | integer |          | Insertion position in the section (default: append at end) |

***

#### `offer_update_block`

Replaces an existing block completely with the provided Tiptap node.

| Parameter      | Type    | Required | Description           |
| -------------- | ------- | -------- | --------------------- |
| `offerId`      | string  | yes      | ULID of the offer     |
| `sectionIndex` | integer | yes      | 0-based section index |
| `blockIndex`   | integer | yes      | 0-based block index   |
| `block`        | object  | yes      | New Tiptap node       |

***

#### `offer_remove_block`

Removes a block from a section; subsequent blocks shift up.

| Parameter      | Type    | Required | Description           |
| -------------- | ------- | -------- | --------------------- |
| `offerId`      | string  | yes      | ULID of the offer     |
| `sectionIndex` | integer | yes      | 0-based section index |
| `blockIndex`   | integer | yes      | 0-based block index   |

***

#### `offer_upload_image`

Uploads an image for use in offers. Allowed formats: PNG, JPEG, WebP, and GIF (max 5 MB decoded). The MIME type is detected from the image data; the file extension is enforced accordingly. SVG is rejected. The response includes `mediaId`, `url`, `filename`, and a ready-made `imageBlock` that can be passed directly to `offer_add_block`.

| Parameter  | Type   | Required | Description                                  |
| ---------- | ------ | -------- | -------------------------------------------- |
| `filename` | string | yes      | File name of the image                       |
| `data`     | string | yes      | Base64-encoded image data (max 5 MB decoded) |
| `altText`  | string |          | Alternative text for the image               |
| `title`    | string |          | Title of the image                           |

***

## Creating offers with AI: a typical workflow

Here is how to create a complete offer with an AI assistant:

<Steps>
  <Step title="Create the offer">
    Create an empty offer for the desired customer:

    ```json theme={null}
    {
      "tool": "offer_create",
      "arguments": {
        "customerId": "<CUSTOMER-ID>",
        "name": "Software Package 2026",
        "locale": "en",
        "validUntil": "2026-12-31"
      }
    }
    ```

    The response contains the `offerId` you need for all subsequent calls.
  </Step>

  <Step title="Find matching products">
    Search the product catalog for the desired product:

    ```json theme={null}
    {
      "tool": "product_search",
      "arguments": {
        "query": "Pro Plan",
        "locale": "en"
      }
    }
    ```
  </Step>

  <Step title="Get the price plan">
    Load the price plans for the found product:

    ```json theme={null}
    {
      "tool": "product_prices",
      "arguments": {
        "productId": "<PRODUCT-ID>"
      }
    }
    ```

    Use the `id` of the desired plan as `pricePlan` in the next step.
  </Step>

  <Step title="Add a product block">
    Insert a `subscriptionProposal` block with the product and price plan:

    ```json theme={null}
    {
      "tool": "offer_add_block",
      "arguments": {
        "offerId": "<OFFER-ID>",
        "block": {
          "type": "subscriptionProposal",
          "attrs": {
            "currency": "EUR",
            "products": [
              {
                "product": "<PRODUCT-ID>",
                "pricePlan": "<PRICE-PLAN-ID>",
                "quantity": 1
              }
            ],
            "terms": [
              { "contractPeriod": "12M", "cancellationPeriod": "3M" }
            ]
          }
        }
      }
    }
    ```
  </Step>

  <Step title="Add text content">
    Add headings, paragraphs, or tables. Use `offer_block_types` to see which block types are available and what a valid node looks like.
  </Step>

  <Step title="Upload and insert an image">
    Upload an image and insert it directly. The tool returns a ready-made `imageBlock`:

    ```json theme={null}
    {
      "tool": "offer_upload_image",
      "arguments": {
        "filename": "logo.png",
        "data": "<BASE64-DATA>",
        "altText": "Company logo"
      }
    }
    ```

    Pass the returned `imageBlock` as `block` to `offer_add_block`.
  </Step>
</Steps>

<Info>
  All offer tools respect the lifecycle: an accepted or archived offer can no longer be modified. Check the status in advance with `offer_get`.
</Info>
