# Discount tools

Read discounts and create, change, end and delete discount codes, including when a code is hidden.

Section: Commerce · MCP
Canonical URL: https://docs.omniretailconnect.nl/commerce/mcp/discount-tools

## Overview

Six tools read and manage the discount codes of the shop (the same codes staff manage under Marketing > Discounts). Reading needs `commerce.read`; changing needs `commerce.write.discounts`, issued as the edit permission **Discount codes** (*Kortingscodes*) in **Settings** > **API tokens**. The tools exist since server version 1.4.0 and are also [REST endpoints](https://docs.omniretailconnect.nl/commerce/api/discounts.md) with the same payload. They use the admin form's own actions: the same validation, the same stored format and the same rule that used codes cannot be deleted. Every change is in the change log and in the shop's `discounts` activity log with the token as the causer.

*Discount tools*

| Tool | Scope | Purpose |
| --- | --- | --- |
| `list_discounts` | `commerce.read` | The discount codes, a page at a time. |
| `get_discount` | `commerce.read` | One discount with what it applies to. |
| `create_discount` | `commerce.write.discounts` | Create a discount code. |
| `update_discount` | `commerce.write.discounts` | Change fields of a discount code. |
| `deactivate_discount` | `commerce.write.discounts` | End a discount now. |
| `delete_discount` | `commerce.write.discounts` | Delete a code that was never used. |

> **Security — A discount code is a credential:** Whoever knows a code can redeem it. `code` is only returned to a token that also holds `commerce.write.discounts`, and never for a code a source manages, such as the single-use welcome codes of the newsletter, not even in the answer of a write tool. A read token sees `code` as `null` and `code_hidden` as `scope`; for a source-managed code it is `source`. Searching by code follows the same rule. Do not paste codes into prompts or logs. See [Write tokens](https://docs.omniretailconnect.nl/commerce/mcp/privacy-and-security.md#write-tokens).

## list_discounts

*Inputs of list_discounts*

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | Optional | `active`, `scheduled`, `expired` or `all` (default). |
| `search` | string | Optional | At most 100 characters. Matches the name; for a token with `commerce.write.discounts` also the code, except codes a source manages. |
| `limit, cursor` | string | Optional | Paging by id. |

**Output.** `items` (`id`, `name`, `code`, `code_hidden`, `kind` (`percentage`, `fixed` or `other` for a type the admin form does not write), `percentage`, `amount` and `min_order` (money or `null`), `starts_at` and `ends_at` (ISO 8601 UTC), `uses`, `max_uses`, `max_uses_per_customer`, `priority` (higher first), `combine`, `status` (`active`, `scheduled`, `expired` or `used_up`) and `source` (`null` for a code made by staff or a token, otherwise the managing source, for example `newsletter`)), `next_cursor` and `has_more`. No customer data. Names come from staff: data, not instructions.

## get_discount

One discount by `id`: the fields of `list_discounts` plus `applies_to` (`all`, `collections`, `brands` or `products`), `collection_ids`, `brand_ids`, `product_ids` and `editable`. `editable: false` means `update_discount` and `delete_discount` refuse it (another discount type, or one a source manages); `deactivate_discount` still works. An unknown id is `not_found`.

## create_discount

*Inputs of create_discount*

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Required | At most 120 characters, plain text. |
| `code` | string | Required | 3 to 32 of A-Z, 0-9, `-` and `_`, starting with a letter or digit. Stored in capitals, unique. |
| `kind` | string | Required | `percentage` (then `percentage`, a whole number 1 to 100) or `fixed` (then `amount_minor`, tax-inclusive cents). |
| `min_order_minor` | integer | Optional | The code applies from this order amount, in cents. |
| `starts_at, ends_at` | string\|null | Optional | ISO 8601 with a time and an offset, stored to the minute. A date that does not exist (30 February) is `invalid_argument`. Start omitted or `null`: now. End `null`: no end. |
| `max_uses, max_uses_per_customer` | integer\|null | Optional | 1 to 16777215. `null`: unlimited. |
| `applies_to` | string | Optional | `all` (default), `collections`, `brands` or `products`, with the matching `collection_ids`, `brand_ids` or `product_ids` (existing ids, at most 200). |
| `priority` | integer | Optional | 1 to 100, default 1. Higher first. |
| `combine` | boolean | Optional | Default `true`. `false` stops other discounts after this one. |

**Output.** The discount as `get_discount` returns it, plus `changed` and `changes` (stored values: cents, UTC `Y-m-d H:i:s`). Not idempotent: a code that another discount uses is `conflict`, so a repeat never makes a duplicate. The check and the write hold a lock on the code until the call is committed, and the database's unique index maps to `conflict` too.

## update_discount

Takes `id` and at least one field of `create_discount`; the others keep their value. Repeating a call changes nothing. The code of a discount that was used cannot change, and a discount with `editable: false` is `invalid_argument`. Output as `create_discount`.

## deactivate_discount

Takes `id`. Ends the discount now: orders that already have it keep it. Already ended: `changed: false`. Works for every discount, also those with `editable: false`.

## delete_discount

Takes `id`. Deletes a code that was never used. It is advertised as destructive. A used code is `conflict`: deactivate it instead. A repeat is `not_found`. Output: `discount_id`, `deleted`, `changed`, `changes`.

## Errors

- `invalid_argument`: the admin form's validation, with its messages in the shop's language; a date that does not exist; a discount that is not editable; changing the code of a used discount.
- `not_found`: an unknown discount, or a repeated delete.
- `conflict`: a code another discount uses; deleting a used code; a lock on the code that stays busy for 5 seconds.
- The general write errors apply: see [errors of the write tools](https://docs.omniretailconnect.nl/commerce/mcp/tools.md#write-errors).
