# Update customiser font tier (`update_customiser_font_tier`)

Changes one font tier of a simple_letter customiser with a JSON merge patch: fields you omit stay as they are, and sending rows replaces every row of the tier. Amounts are integer minor units of the Store currency. Fonts priced by the tier use the new prices on the storefront. Read the tier with get_customiser_font_tier first. It requires the pricing:write scope, takes an optional idempotency_key for replay, and supports dry_run.

- Source URL: https://www.signcustomiser.com/help/mcp/tools/update_customiser_font_tier/
- Markdown URL: https://www.signcustomiser.com/help/mcp/tools/update_customiser_font_tier.md
- MCP endpoint: https://mcp.signcustomiser.com
- Required scope: `pricing:write`
- Behaviour: destructive write

## Purpose

Change the name, letter pricing mode or prices of one font tier.

## Annotations

| Hint | Value |
| --- | --- |
| readOnlyHint | false |
| destructiveHint | true |
| idempotentHint | false |
| openWorldHint | false |

## Prerequisites

- A customiser id from list_customisers and a font tier id from list_customiser_font_tiers.
- The current tier from get_customiser_font_tier, because rows is replaced whole.
- A merchant has connected this store and approved the pricing:write scope.

## Side effects

- Changes what every font priced by this tier costs on the storefront.
- Clears the customiser cached storefront configuration.
- Spends one unit of the store option-write budget.

## Arguments

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `customiser_id` | integer | yes | The customiser id, from list_customisers. |
| `font_tier_id` | integer | yes | The font tier id, from list_customiser_font_tiers. |
| `name` | string | no | The tier display name. |
| `letter_pricing_mode` | string | no | How letters are charged (closed set): every_letter_cost charges each letter the same, price_by_position charges by letter position from the per-position schedule. One of: `every_letter_cost`, `price_by_position`. |
| `rows` | array of object | no | Every priced row of the tier. Sending rows replaces the whole set, and an empty list clears it. A row sent without letter_price_by_position keeps the schedule its size already had; send null to clear one. |
| `rows[].size_id` | integer | yes | One of the customiser own size ids, from list_customiser_options with option_type size. |
| `rows[].base_price` | object | yes | The flat amount a sign costs at this size, keyed line_1 to line_10 by line count, in integer minor units of the Store currency. line_1 to line_3 are required. |
| `rows[].base_price.line_1` | integer | yes |  |
| `rows[].base_price.line_2` | integer | yes |  |
| `rows[].base_price.line_3` | integer | yes |  |
| `rows[].base_price.line_4` | integer or null | no |  |
| `rows[].base_price.line_5` | integer or null | no |  |
| `rows[].base_price.line_6` | integer or null | no |  |
| `rows[].base_price.line_7` | integer or null | no |  |
| `rows[].base_price.line_8` | integer or null | no |  |
| `rows[].base_price.line_9` | integer or null | no |  |
| `rows[].base_price.line_10` | integer or null | no |  |
| `rows[].letter_price` | object | yes | The amount each letter adds at this size, keyed line_1 to line_10 by line count, in integer minor units of the Store currency. line_1 to line_3 are required. |
| `rows[].letter_price.line_1` | integer | yes |  |
| `rows[].letter_price.line_2` | integer | yes |  |
| `rows[].letter_price.line_3` | integer | yes |  |
| `rows[].letter_price.line_4` | integer or null | no |  |
| `rows[].letter_price.line_5` | integer or null | no |  |
| `rows[].letter_price.line_6` | integer or null | no |  |
| `rows[].letter_price.line_7` | integer or null | no |  |
| `rows[].letter_price.line_8` | integer or null | no |  |
| `rows[].letter_price.line_9` | integer or null | no |  |
| `rows[].letter_price.line_10` | integer or null | no |  |
| `rows[].letter_price_by_position` | object or null | no | Only on a price_by_position tier: per line count, a list of 1 to 100 per-letter prices in integer minor units, the first entry being the first letter. |
| `rows[].letter_price_by_position.line_1` | array of integer | no |  |
| `rows[].letter_price_by_position.line_2` | array of integer | no |  |
| `rows[].letter_price_by_position.line_3` | array of integer | no |  |
| `rows[].letter_price_by_position.line_4` | array of integer | no |  |
| `rows[].letter_price_by_position.line_5` | array of integer | no |  |
| `rows[].letter_price_by_position.line_6` | array of integer | no |  |
| `rows[].letter_price_by_position.line_7` | array of integer | no |  |
| `rows[].letter_price_by_position.line_8` | array of integer | no |  |
| `rows[].letter_price_by_position.line_9` | array of integer | no |  |
| `rows[].letter_price_by_position.line_10` | array of integer | no |  |
| `idempotency_key` | string | no | An optional client-generated key unique to this logical write, such as a UUID. Repeating the call with the same key and the same arguments replays the original result; the route does not require one. |
| `dry_run` | boolean | no | When true, the patch is checked against the customiser pricing model and its sizes is checked and nothing is written: the result is the same verdict the apply would have reached, and a problem the apply would have raised comes back as the same tool error. Defaults to false. A dry run claims no key. |

## Result

The font tier as it now stands, with every row. A dry run returns the update verdict instead and writes nothing.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `request_id` | string | yes |  |
| `store` | object | yes |  |
| `store.id` | integer | yes |  |
| `store.name` | string | yes |  |
| `customiser_id` | integer | yes |  |
| `font_tier` | object | no | The font tier as it now stands, with every row. Absent on a dry run. |
| `font_tier.id` | integer | yes |  |
| `font_tier.customiser_id` | integer | yes |  |
| `font_tier.name` | string or null | no |  |
| `font_tier.letter_pricing_mode` | string or null | no | One of: `every_letter_cost`, `price_by_position`. |
| `font_tier.currency` | string or null | no | The ISO 4217 currency of every integer minor-unit amount on the record. |
| `font_tier.rows` | array of object | no |  |
| `font_tier.rows[].size_id` | integer or null | no |  |
| `font_tier.rows[].base_price` | object | no |  |
| `font_tier.rows[].letter_price` | object | no |  |
| `font_tier.rows[].letter_price_by_position` | object or null | no |  |
| `font_tier.created_at` | string or null | no |  |
| `font_tier.updated_at` | string or null | no |  |
| `idempotent_replay` | boolean | no | True when this call replayed a previously recorded response for the same idempotency_key instead of applying the write again. |
| `dry_run` | boolean | no | Present and true only when dry_run was requested. |
| `valid` | boolean | no | Present only on a dry run, and always true: a failing dry run returns a tool error instead. |
| `verdict` | object | no | Present only on a dry run: the update verdict. |

## Error cases

| Code | Recovery |
| --- | --- |
| `idempotency_subject_changed` | The tier was deleted after the recorded update. Issue a new key and re-check the resource before writing again. |
| `insufficient_scope` | The connection was approved without pricing:write. |
| `pricing_model_mismatch` | The customiser no longer prices by simple_letter, so its font tiers cannot be changed. |
| `rate_limited` | The store option-write budget is spent. Wait retry_after seconds. |
| `resource_not_found` | No font tier with that id belongs to that customiser in this store. |
| `unknown_field` | Remove the field named by pointer; it is not part of a font tier. |
| `validation_failed` | A field is out of range, a required line price is missing, or a row names a size that is not this customiser. The pointer names it. |

## Example: Rename a tier

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "update_customiser_font_tier",
    "arguments": {
      "customiser_id": 42,
      "font_tier_id": 7,
      "name": "Premium"
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "structuredContent": {
      "request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t",
      "store": {
        "id": 12,
        "name": "Demo Signs"
      },
      "customiser_id": 42,
      "font_tier": {
        "id": 7,
        "customiser_id": 42,
        "name": "Premium",
        "letter_pricing_mode": "every_letter_cost",
        "currency": "USD",
        "rows": [],
        "created_at": "2026-08-13T00:00:00Z",
        "updated_at": "2026-08-13T00:05:00Z"
      },
      "idempotent_replay": false
    }
  }
}
```

## Example: A row for a size of another customiser (error)

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "update_customiser_font_tier",
    "arguments": {
      "customiser_id": 42,
      "font_tier_id": 7,
      "rows": [
        {
          "size_id": 999,
          "base_price": {
            "line_1": 2500,
            "line_2": 3000,
            "line_3": 3500
          },
          "letter_price": {
            "line_1": 400,
            "line_2": 450,
            "line_3": 500
          }
        }
      ],
      "dry_run": true
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "isError": true,
    "structuredContent": {
      "error": {
        "code": "validation_failed",
        "title": "Validation failed",
        "detail": "The request payload failed validation.",
        "recovery": "Correct every field listed in errors, using its pointer to locate the value and allowed_values where one is given, then call the tool again.",
        "pointer": "/rows/0/size_id"
      }
    }
  }
}
```
