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

# Tools

> Reference for all tools available in the Subtotal Connector.

The Subtotal Connector's tools are grouped into toolsets that match the [API reference](/docs/api-reference/introduction.md): most tools have an API endpoint with the same data, linked from each tool.

| Toolset | Tools | Permissions | API |
| :- | :- | :- | :- |
| [Ask and metrics](#ask-and-metrics) | [`ask`](#ask), [`get_metric`](#get_metric), [`get_data_model`](#get_data_model) | `mcp:read` | — |
| [Connections](#connections) | [`list_connections`](#list_connections), [`get_connection`](#get_connection), [`get_connection_activity`](#get_connection_activity) | `connections:read` (`list_connections` and `get_connection` need `simulator:read` for simulated connections) | [Connections](/docs/api-reference/connections/get-connections.md) |
| [Purchases](#purchases) | [`get_purchase`](#get_purchase) | `mcp:read` | [Purchases](/docs/api-reference/purchases/get-purchase.md) |
| [Products](#products) | [`list_products`](#list_products), [`get_product`](#get_product) | `mcp:read` | [Products](/docs/api-reference/products/get-products.md) |
| [Product groups](#product-groups) | [`list_product_groups`](#list_product_groups), [`create_product_group`](#create_product_group), [`rename_product_group`](#rename_product_group), [`delete_product_group`](#delete_product_group), [`update_product_group_products`](#update_product_group_products) | `product_groups:read`, `product_groups:write` | [Product groups](/docs/api-reference/product-groups/list-product-groups.md) |
| [Retailers](#retailers) | [`get_retailers`](#get_retailers), [`update_retailer`](#update_retailer) | `retailers:read`, `retailers:write` | [Retailers](/docs/api-reference/retailers/get-retailers.md) |
| [Event simulator](#event-simulator) | [`simulate_connection`](#simulate_connection), [`simulate_purchase`](#simulate_purchase), [`set_simulated_status`](#set_simulated_status), [`delete_simulated_connection`](#delete_simulated_connection) | `simulator:write` (`simulator:read` to read results back) | [Event simulator](/docs/api-reference/simulated-connections/simulate-connection.md) |
| [Webhooks](#webhooks) | [`list_webhooks`](#list_webhooks), [`create_webhook`](#create_webhook), [`update_webhook`](#update_webhook), [`delete_webhook`](#delete_webhook) | `webhooks:read`, `webhooks:write` | [Webhooks](/docs/api-reference/webhooks/get-webhooks.md) |

## Ask and metrics

Query your purchase data in plain language and get dashboard metrics.

### ask

Ask a natural language question about your purchase data. The server translates it into SQL and returns the results — no SQL knowledge required.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `question` | string | Yes | A natural language question about your purchase data |

**Example questions:**

* "Show me the top 5 retailers by purchase count this year"
* "How many active connections do I have?"
* "What are my highest-value purchases from the last 30 days?"
* "Which brands appear most frequently in my items?"

**Response format:**

```json theme={null}
{
  "fields": ["name", "purchase_count"],
  "results": [
    ["Amazon", 1523],
    ["Target", 892],
    ["Walmart", 641]
  ],
  "result_count": 3
}
```

**Constraints:**

* Questions must be plain English — embedded SQL will be rejected
* Only `SELECT` queries are generated — your data is never modified
* Results are limited to 100 rows and 5 MB
* A 5-second execution timeout is enforced

> [!WARNING]
>
> If the question cannot be answered with the available data, or the generated query fails, the response will contain an `error` field:
>
> ```json theme={null}
> {
>   "error": "This question cannot be answered with the available data."
> }
> ```
>
> Other possible errors include request timeouts and response size limits.

> [!NOTE]
>
> `ask` uses an AI model to translate your question into SQL. For best results, be specific about the data you want, time ranges, and how results should be grouped or sorted. To inspect the data model itself (entities, fields, relationships), use `get_data_model` instead.

### get\_metric

Get a [Subtotal Dashboard](https://dashboard.subtotal.com) metric for a date range, computed the way the dashboard computes it, with a link to that page. Prefer it over `ask` for these metrics; use `ask` for everything else.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `metric` | string | Yes | One of the metrics below. |
| `start_date` | string | Yes | First day, `YYYY-MM-DD` (UTC). |
| `end_date` | string | Yes | Last day, inclusive. At most 366 days after `start_date`. |
| `retailer_ids` | string\[] | No | Only these retailers, from `get_retailers`. Defaults to all. |
| `limit` | integer | No | `top_*` only: rows to return, 1–50. Defaults to 10. |

**Metrics:**

| Group | Metrics | Needs analytics |
| :- | :- | :- |
| Program | `purchases`, `shared_purchases` (by date shared with you), `connections_activated`, `active_connections` (on the window's last day) | No |
| Your brands' orders | `brand_total_order_value`, `brand_average_order_value`, `brand_units_per_order`, `brand_products_per_order` | No |
| Order value | `total_order_value`, `average_order_value` | Yes |
| Your brands' reach | `brand_purchase_rate`, `brand_share_of_basket`, `brand_share_of_basket_spend` | Yes |
| Rankings | `top_brands_by_basket_presence`, `top_brands_by_units_sold`, `top_brands_by_gmv`, `top_products_by_units_sold`, `top_products_by_gmv`, `top_products_by_basket_presence` | Yes |

**Example:**

```json theme={null}
{ "metric": "average_order_value", "start_date": "2026-09-01", "end_date": "2026-09-03", "retailer_ids": ["walmart"] }
```

```json theme={null}
{
  "value": 18.38,
  "previous_value": 21.04,
  "series": [
    { "day": "2026-09-01", "value": 16.20 },
    { "day": "2026-09-02", "value": 0.0 },
    { "day": "2026-09-03", "value": 22.75 }
  ],
  "rows": null,
  "dashboard_url": "https://dashboard.subtotal.com/analytics/metrics/average-order-value-over-time?preset=custom&start_date=2026-09-01&end_date=2026-09-03&retailer_ids=walmart"
}
```

**Fields:**

| Field | Description |
| :- | :- |
| `value` | The window's figure: a count, USD, or 0–1 for rates and shares. 0 when there are no orders to divide by. |
| `previous_value` | The same metric for the equal-length window just before. |
| `series` | One `{day, value}` per UTC day, the line the dashboard charts. Rates and shares are `null` on days with nothing to divide by. |
| `rows` | `top_*` only, best first: `name`, `brand`, `units`, `gmv` (USD), `basket_presence` (0–1). `value`, `previous_value` and `series` are `null`. |
| `dashboard_url` | The dashboard page for this metric, window and retailers. |

> [!NOTE]
>
> Metrics that need analytics return an error on plans without it. Windows must start in 2000 or later. Your team's access scope applies, as it does for `ask`.

### get\_data\_model

Returns the data model for your purchase data, including entities, fields, types, and relationships.

**Parameters:** None

**Returns:** A plain-text data model describing six entities: `connection`, `purchase`, `item`, `retailer`, `product`, and `brand`.

> [!NOTE]
>
> If you're using `ask`, you typically don't need to call `get_data_model` — the server already has the data model when answering questions.

## Connections

Find connections and read their history.

### list\_connections

[API: GET /connections](/docs/api-reference/connections/get-connections.md)

List your connections, newest first. Every filter is optional. For counts or breakdowns across many connections, use `ask` instead. Requires the `connections:read` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `customer_id` | string | No | Only connections with this customer ID. |
| `retailer_id` | string | No | Only connections to this retailer, a `retailer_id` from `get_retailers`. |
| `status` | string | No | Only connections in this status: `initialized`, `active`, `unauthenticated`, or `disconnected`. |
| `created_after` | string | No | Only connections created at or after this time (ISO 8601). |
| `created_before` | string | No | Only connections created at or before this time (ISO 8601). |
| `include_simulated` | boolean | No | Also list your simulated test connections from the [Event Simulator](/docs/event-simulator.md), marked `is_simulated`. Needs the `simulator:read` scope. Defaults to `false`. |
| `limit` | integer | No | How many connections to return, 1 to 100. Defaults to 25. |
| `starting_after` | string | No | For the next page, the last `connection_id` of the previous page. |

**Response format:**

```json theme={null}
{
  "has_more": true,
  "connections": [
    {
      "connection_id": "01J51S0JYV6N7K1030CV1ZKSCA",
      "customer_id": "8a46e581-48ba-498a-9ad0-2ee72582e1af",
      "retailer_id": "walmart",
      "status": "active",
      "email": null,
      "mobile": null,
      "creation_date": "2026-10-14T17:02:11Z",
      "tags": { "campaign": "summer_sweeps" },
      "is_simulated": false
    }
  ]
}
```

When `has_more` is `true`, call again with `starting_after` set to the last `connection_id` you received.

> [!NOTE]
>
> These are the same connections as the public `GET /connections` API, except `email` and `mobile` are always `null` (use the API for contact details) and simulated test connections are listed only with `include_simulated`, which needs the `simulator:read` scope (the API always includes them). Without that scope, `include_simulated` returns `error_category: "insufficient_scope"`. Deleted connections aren't listed.

### get\_connection

[API: GET /connections/\{connection\_id}](/docs/api-reference/connections/get-connection.md)

Get one connection: its customer ID, retailer, status, creation date and tags. Requires the `connections:read` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `connection_id` | string | Yes | The connection to read, from `list_connections` or an `ask` result. |

**Response format:** a single connection, in the same shape as one entry of `list_connections`.

> [!NOTE]
>
> This is the same connection as the public `GET /connections/{connection_id}` API, except `email` and `mobile` are always `null`. A connection that doesn't exist, belongs to another account, or has been deleted returns a "not found" error, and so does a simulated test connection unless the connector has the `simulator:read` scope.

### get\_connection\_activity

[API: GET /connections/\{connection\_id}/activity](/docs/api-reference/connections/get-connection-activity.md)

Get a connection's whole activity log, newest first: when it was created and activated, when it became unauthenticated or was disconnected, and when its customer ID, email or mobile changed. Each entry says who made the change. Pass a `connection_id` from `list_connections` or an `ask` result, for example after asking "Which of my connections became unauthenticated this week?". Requires the `connections:read` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `connection_id` | string | Yes | The connection to read. |

**Response format:**

```json theme={null}
{
  "connection_id": "01J51S0JYV6N7K1030CV1ZKSCA",
  "activity": [
    {
      "timestamp": "2026-10-14T17:02:11Z",
      "event_source": "Retailer",
      "event": "connection_unauthenticated",
      "details": "Connection unauthenticated.",
      "source_id_type": null,
      "source_id_name": null,
      "connection_before": {
        "connection_id": "01J51S0JYV6N7K1030CV1ZKSCA",
        "customer_id": "8a46e581-48ba-498a-9ad0-2ee72582e1af",
        "retailer_id": "walmart",
        "status": "active",
        "email": null,
        "mobile": null
      },
      "connection_after": {
        "connection_id": "01J51S0JYV6N7K1030CV1ZKSCA",
        "customer_id": "8a46e581-48ba-498a-9ad0-2ee72582e1af",
        "retailer_id": "walmart",
        "status": "unauthenticated",
        "email": null,
        "mobile": null
      }
    }
  ]
}
```

**Fields:**

| Field | Description |
| :- | :- |
| `event` | What happened: `connection_created`, `connection_activated`, `connection_unauthenticated`, `connection_disconnected`, `customer_id_updated`, `email_updated`, or `mobile_updated`. |
| `event_source` | Who made the change: `API`, `Dashboard`, `Retailer`, `System`, or `Consumer`. |
| `source_id_name` | For `API` changes, the name of the API key that made them. For `System` backfills, where the data came from. For a consumer disconnecting their own connection, `Subtotal`. Otherwise `null`. |
| `connection_before` / `connection_after` | The connection's state before and after the change. `email` and `mobile` are always `null` here; use the API for contact details. |

> [!NOTE]
>
> These are the same entries as the Activity tab in the dashboard and the public `GET /connections/{connection_id}/activity` API. A connection that doesn't exist, belongs to another account, or has been deleted returns a "not found" error.

## Purchases

Read one purchase and its items.

### get\_purchase

[API: GET /purchases/\{purchase\_id}](/docs/api-reference/purchases/get-purchase.md)

Get one purchase with its items: totals, purchase type, status, and each item's price, quantity and product. Pass a `purchase_id` from an `ask` result, for example after asking "What were my 10 largest orders last month?".

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `purchase_id` | string | Yes | The purchase to read. |

**Response format:**

```json theme={null}
{
  "purchase_id": "01J51S0JYV6N7K1030CV1ZKSCF",
  "date": "2024-01-01T00:00:00Z",
  "total": "9.34",
  "tax": "0.85",
  "subtotal": "8.49",
  "purchase_type": "online",
  "status": "completed",
  "item_count": 1,
  "items": [
    {
      "item_id": "01J51S0JYV6N7K1030CV1ZKSCA",
      "price": "8.49",
      "quantity": 1,
      "product": {
        "product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
        "name": "Diet Coke 12pk",
        "description": "Diet Coke - 12pk/12oz cans",
        "brand": "diet-coke",
        "upc": "049000028911"
      }
    }
  ]
}
```

> [!NOTE]
>
> This is the same purchase as the public `GET /purchases/{purchase_id}` API, filtered by your team's access scope: items outside it, and sensitive items, are left out. A purchase that doesn't exist or isn't visible to your account returns a "not found" error.

> **Purchase status values**
>
> `ask` can also query by `status`, for example: "How many purchases are shipped?"
>
> <Snippet file="snippets/purchase-status.mdx" />

## Products

Browse and search your brand catalog.

### list\_products

[API: GET /products](/docs/api-reference/products/get-products.md)

List or search your brand catalog: every active product of the brands you subscribe to, whether or not consumers have bought it yet, ordered by `product_id`. These are the same products as the **Products** page in the dashboard. For sales figures across products, use `ask` instead.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `search` | string | No | Only products whose description, short description or UPC contains this, ignoring case. Up to 256 characters. |
| `limit` | integer | No | The maximum number of products to return: 1 to 100, default 25. |
| `starting_after` | string | No | For the next page: the last `product_id` of the previous page, when `has_more` was `true`. Keep the same `search` and `group_id`. |
| `group_id` | string | No | Only products in this product group, a `product_group_id` from `list_product_groups`. A group that isn't yours matches no products. |

**Response format:**

```json theme={null}
{
  "has_more": true,
  "products": [
    {
      "product_id": "01J51S0JYV6N7K1030CV1ZKPR1",
      "description": "Sparkling Water, Lime, 12 fl oz",
      "short_description": "Lime Sparkling Water",
      "upc": "012345678905",
      "brand_id": "01J51S0JYV6N7K1030CV1ZKBR1",
      "brand_name": "Acme",
      "brand_logo_url": "https://cdn.example.com/acme.png",
      "top_level_category": "BEVERAGE",
      "groups": [{ "product_group_id": "01J51S0JYV6N7K1030CV1ZKGR1", "name": "Summer line" }]
    }
  ]
}
```

### get\_product

[API: GET /products/\{product\_id}](/docs/api-reference/products/get-product.md)

Get one product from your brand catalog, in the same shape as a `list_products` row. Pass a `product_id` from `list_products` or an `ask` result.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `product_id` | string | Yes | The product to read. |

> [!NOTE]
>
> A product that doesn't exist, is inactive, or isn't one of your brands' products returns a "not found" error.

## Product groups

Organise your catalog into groups and change their products.

### list\_product\_groups

[API: GET /product-groups](/docs/api-reference/product-groups/list-product-groups.md)

List the groups you organise your brand catalog into, ordered by name, each with how many products it holds. These are the same groups as the **Products** page in the dashboard; each `list_products` row names its groups in `groups`. Takes no parameters. To see a group's products, pass its `product_group_id` to `list_products` as `group_id`. Requires the `product_groups:read` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Response format:**

```json theme={null}
{
  "groups": [
    {
      "product_group_id": "01J51S0JYV6N7K1030CV1ZKGR1",
      "name": "Summer line",
      "product_count": 12,
      "creation_date": "2026-06-01T15:04:05"
    }
  ]
}
```

### create\_product\_group

[API: POST /product-groups](/docs/api-reference/product-groups/create-product-group.md)

Create an empty product group. Returns the group in the same shape as a `list_product_groups` row. Requires the `product_groups:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `name` | string | Yes | The group's name, 1 to 60 characters. Unique on your account, ignoring case. |

### rename\_product\_group

[API: PATCH /product-groups/\{product\_group\_id}](/docs/api-reference/product-groups/rename-product-group.md)

Rename a product group. Its products stay in it. Requires the `product_groups:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `product_group_id` | string | Yes | The group to rename, from `list_product_groups`. |
| `name` | string | Yes | The new name, 1 to 60 characters. Unique on your account, ignoring case. |

### delete\_product\_group

[API: DELETE /product-groups/\{product\_group\_id}](/docs/api-reference/product-groups/delete-product-group.md)

Delete a product group. Its products are not deleted. The tool is marked destructive, so assistants usually ask you to confirm first. Requires the `product_groups:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `product_group_id` | string | Yes | The group to delete, from `list_product_groups`. |

### update\_product\_group\_products

[API: PUT /product-groups/\{product\_group\_id}/products](/docs/api-reference/product-groups/update-product-group-products.md)

Add and remove products in a group in one call. Returns the group with its new `product_count`. Requires the `product_groups:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `product_group_id` | string | Yes | The group to change, from `list_product_groups`. |
| `add` | string\[] | No | Up to 500 `product_id`s from `list_products` to add. |
| `remove` | string\[] | No | Up to 500 `product_id`s to take out of the group. |

> [!NOTE]
>
> Only products from your brand catalog can be added. If any product to add isn't in it, nothing changes and the tool returns a "not in your catalogue" error. A group that isn't yours returns a "not found" error.

## Retailers

See and configure the retailers available for linking.

### get\_retailers

[API: GET /retailers](/docs/api-reference/retailers/get-retailers.md)

List the retailers available to your account as structured rows. By default returns only the retailers enabled for your team (each with a Subtotal Link URL); pass `include="all"` to list every active retailer in the catalog, annotated with whether it's enabled for your team.

Requires the `retailers:read` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `include` | string | No | `enabled` (default) — only the retailers enabled for your team, each with a `link_url`. `all` — every active retailer in the catalog, each annotated with an `enabled` flag and a `link_url` (present only when enabled). These are the only accepted values. |

**Response format:**

```json theme={null}
{
  "retailers": [
    {
      "retailer_id": "walmart",
      "name": "Walmart",
      "images": {
        "logo": { "light": ".../light/logo.svg", "dark": ".../dark/logo.svg" },
        "icon": { "light": ".../light/icon.svg", "dark": ".../dark/icon.svg" }
      },
      "status": "active",
      "enabled": true,
      "link_url": "https://link.subtotal.com/a1b2c3d4",
      "link_visibility": "restricted",
      "link_audience": { "allowed_emails": [], "allowed_domains": ["example.com"] }
    }
  ]
}
```

**Fields:**

| Field | Description |
| :- | :- |
| `retailer_id` | Retailer slug — the exact value used to filter purchase data by retailer (e.g. in `ask`). |
| `name` | Retailer display name. |
| `images` | Logo and icon URLs, each with a `light` and `dark` theme. |
| `status` | Platform status: `active`, `limited`, or `inactive`. |
| `enabled` | Whether this retailer is enabled for your team. |
| `link_url` | URL to launch Subtotal Link for the retailer. Present only when the retailer is enabled; `null` otherwise. |
| `link_visibility` | Who sees the retailer on the Subtotal Link retailer-selection page: `everyone`, `restricted` (only consumers matching `link_audience`), or `hidden`. `link_url` works regardless. `null` when not enabled. |
| `link_audience` | `{allowed_emails, allowed_domains}` applied when `link_visibility` is `restricted`. `null` when not enabled. |

> [!NOTE]
>
> This is the same retailer contract returned by the public `GET /retailers` API. With `include="all"`, `link_url` is `null` for retailers not enabled for your team.

### update\_retailer

[API: PATCH /retailers/\{retailer\_id}](/docs/api-reference/retailers/update-retailer.md)

Update a retailer for your account: enable or disable it, and set who sees it on the Subtotal Link retailer-selection page. Every argument except `retailer_id` is optional, and omitted ones are left unchanged, but a call must set at least one of `enabled`, `link_visibility` or `link_audience`. Requires the `retailers:write` OAuth scope; a connector without it gets `error_category: "insufficient_scope"`.

* **Visibility needs the retailer enabled:** either already, or with `enabled: true` in the same call.
* **`link_audience` replaces the stored lists.** Send both lists to keep entries you want.
* **`restricted` needs an audience:** at least one email or domain, counting lists already stored.
* **Disabling takes no visibility settings:** `enabled: false` can't be combined with `link_visibility` or `link_audience`.

> [!WARNING]
>
> Disabling is destructive: it removes your configuration for the retailer. Links you've already shared stop working, and re-enabling issues a new `link_url` with `link_visibility` reset to `everyone`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `retailer_id` | string | Yes | Retailer slug to update — the `retailer_id` from `get_retailers` (e.g. `walmart`). |
| `enabled` | boolean | No | `true` to enable the retailer for your account, `false` to disable it. |
| `link_visibility` | string | No | `everyone`, `restricted` (only consumers matching `link_audience`), or `hidden`. `link_url` works regardless. |
| `link_audience` | object | No | `{allowed_emails, allowed_domains}` for `restricted`. Domains match exactly; subdomains are separate. |

**Example — enable and restrict to one domain in a single call:**

```json theme={null}
{
  "retailer_id": "walmart",
  "enabled": true,
  "link_visibility": "restricted",
  "link_audience": { "allowed_emails": [], "allowed_domains": ["example.com"] }
}
```

**Response format:**

```json theme={null}
{
  "retailer_id": "walmart",
  "name": "Walmart",
  "images": { "logo": { "light": "…", "dark": "…" }, "icon": { "light": "…", "dark": "…" } },
  "status": "active",
  "enabled": true,
  "link_url": "https://link.subtotal.com/a1b2c3d4",
  "link_visibility": "restricted",
  "link_audience": { "allowed_emails": [], "allowed_domains": ["example.com"] }
}
```

With `enabled: false`, `link_url`, `link_visibility` and `link_audience` come back `null`.

> [!NOTE]
>
> Returns the same row shape as `get_retailers` and the public `PATCH /retailers/{retailer_id}` API. Repeating a call returns the current row. These return an `error` field with `error_category: "invalid_input"`: an unknown `retailer_id`, no fields to update, visibility settings for a retailer that isn't enabled, visibility settings with `enabled: false`, or `restricted` with no audience. Invalid emails or domains, more than 100 entries in a list, or a `link_visibility` other than the three values are also rejected.

## Event simulator

Create test connections and events to try your webhooks or integration without a real shopper: the [Event Simulator](/docs/event-simulator.md) dashboard actions. Events go to webhook destinations that [accept simulated events](/docs/event-simulator.md#configure-simulated-events-for-your-webhook-destination), marked `is_simulated`. Each tool works only on your simulated connections, so real connections never change.

### simulate\_connection

[API: POST /simulated-connections](/docs/api-reference/simulated-connections/simulate-connection.md)

Simulate a shopper linking a retailer account: creates an active simulated connection and sends `connection.activated`. Every call creates a new connection. Returns the connection in the same shape as `get_connection`, with `is_simulated: true` and `email` and `mobile` always `null`. Read it back with `get_connection`, or `list_connections` with `include_simulated`; both need the `simulator:read` scope. Requires the `simulator:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `retailer_id` | string | Yes | The retailer to simulate a link to, a `retailer_id` from `get_retailers`. |
| `customer_id` | string | No | Your identifier for the test customer, up to 64 characters. Generated when omitted. |
| `email` | string | No | The test customer's email. |
| `mobile` | string | No | The test customer's mobile number, up to 16 characters. |
| `include_purchases` | boolean | No | Also create 1 to 4 simulated purchases, dated in the last 60 days as history. They don't send `purchase.created`. Defaults to `true`. |

> [!NOTE]
>
> An unknown `retailer_id` returns an error and creates nothing. To send a `purchase.created` event, use `simulate_purchase`.

### simulate\_purchase

[API: POST /simulated-connections/\{connection\_id}/purchases](/docs/api-reference/simulated-connections/simulate-purchase.md)

Simulate a purchase on one of your simulated connections, with random products from the connection's retailer, dated now. Sends `purchase.created`. Every call creates a new purchase. Returns its `purchase_id`; read it with `get_purchase`. Requires the `simulator:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `connection_id` | string | Yes | A simulated connection's `connection_id`, from `simulate_connection` or `list_connections` with `include_simulated`. |

**Response format:**

```json theme={null}
{ "purchase_id": "01J51S0JYV6N7K1030CV1ZKSCA" }
```

> [!NOTE]
>
> A real, deleted or unknown connection returns a "not found" error. The purchase includes at least one of your brands' products when the retailer sells one. If it sells none and your account sees only your brands' products, `get_purchase` returns "not found" for it: the purchase was still created and its event sent, so don't retry.

### set\_simulated\_status

[API: PATCH /simulated-connections/\{connection\_id}](/docs/api-reference/simulated-connections/set-simulated-status.md)

Set one of your simulated connections to `active` or `unauthenticated`, sending `connection.activated` or `connection.unauthenticated`, to test how your integration handles a shopper losing and regaining their retailer login. Setting the status it already has sends the event again. Returns the connection, as `simulate_connection` does. Requires the `simulator:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `connection_id` | string | Yes | A simulated connection's `connection_id`. |
| `status` | string | Yes | `active` or `unauthenticated`. |

> [!NOTE]
>
> A real, deleted or unknown connection returns a "not found" error and nothing changes.

### delete\_simulated\_connection

[API: DELETE /simulated-connections/\{connection\_id}](/docs/api-reference/simulated-connections/delete-simulated-connection.md)

Delete one of your simulated connections with its simulated purchases, to clean up test data. It sends no event and can't be undone. The tool is marked destructive, so assistants usually ask you to confirm first. Returns the deleted `connection_id`. Requires the `simulator:write` OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns `error_category: "insufficient_scope"`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `connection_id` | string | Yes | The simulated connection to delete. |

> [!NOTE]
>
> Real connections can't be deleted: a real, deleted or unknown connection returns a "not found" error.

## Webhooks

Create and manage the webhooks that receive your events.

These tools manage the same webhooks as the Webhooks page of the [Subtotal Dashboard](https://dashboard.subtotal.com) and the public [webhooks API](/docs/api-reference/webhooks/get-webhooks.md). Listing needs the `webhooks:read` OAuth scope; creating, updating and deleting need `webhooks:write`. Connectors set up before these scopes existed need to disconnect and reconnect to grant them; until then the tools return `error_category: "insufficient_scope"`.

> [!WARNING]
>
> A webhook sends your purchase and connection events to its URL. `create_webhook` creates it **paused** (`is_active: false`) unless `is_active` is `true`, and `update_webhook` can pause it or turn it on. Check that the URL is yours before you turn a webhook on.

Invalid input returns an `error` field with `error_category: "invalid_input"`: an unknown `webhook_id` or event type, a name already used by another of your webhooks, a start date after the end date, or a URL that isn't `https` on port 443 or doesn't resolve only to public IP addresses.

### list\_webhooks

[API: GET /webhooks](/docs/api-reference/webhooks/get-webhooks.md)

List your webhooks with their subscribed events. The signing secret is never returned.

**Parameters:** None

**Response format:**

```json theme={null}
{
  "webhooks": [
    {
      "webhook_id": "01JA7Q2W3E4R5T6Y7U8I9O0P1A",
      "name": "Order sync",
      "url": "https://hooks.example.com/subtotal",
      "is_active": true,
      "event_types": ["connection.activated", "purchase.created"],
      "receive_simulated_events": false,
      "purchase_date_start": null,
      "purchase_date_end": null,
      "created_at": "2026-10-14T17:02:11Z"
    }
  ]
}
```

### create\_webhook

[API: POST /webhooks](/docs/api-reference/webhooks/create-webhook.md)

Create a webhook subscribed to the given events. It starts paused unless `is_active` is `true`.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `name` | string | Yes | Unique per account, 1–64 characters. |
| `url` | string | Yes | `https` URL on port 443 that receives the events. |
| `event_types` | string\[] | Yes | Event names from [Event types](/docs/webhooks/events.md), such as `purchase.created`. At least one. An unknown name returns an error that lists the valid names. |
| `is_active` | boolean | No | Start delivering immediately. Defaults to `false` (paused). |
| `receive_simulated_events` | boolean | No | Also deliver events from the [Event Simulator](/docs/event-simulator.md). Defaults to `false`. |
| `purchase_date_start` | date | No | Only purchases on or after this date. |
| `purchase_date_end` | date | No | Only purchases on or before this date. |

**Response format:** the webhook, in the `list_webhooks` shape, plus `signing_secret`.

```json theme={null}
{
  "webhook_id": "01JA7Q2W3E4R5T6Y7U8I9O0P1A",
  "name": "Order sync",
  "url": "https://hooks.example.com/subtotal",
  "is_active": false,
  "event_types": ["purchase.created"],
  "receive_simulated_events": false,
  "purchase_date_start": null,
  "purchase_date_end": null,
  "created_at": "2026-10-14T17:02:11Z",
  "signing_secret": "Vq3n…"
}
```

> [!NOTE]
>
> `signing_secret` verifies the `X-Subtotal-Signature` header on every delivery (see [Verifying signatures](/docs/webhooks/verifying-signatures.md)). It's shown only in this response and can't be read back. If it's lost, delete the webhook and create a new one.

### update\_webhook

[API: PATCH /webhooks/\{webhook\_id}](/docs/api-reference/webhooks/update-webhook.md)

Update a webhook. Every argument except `webhook_id` is optional and omitted ones are left unchanged, but a call must change at least one field. `event_types` replaces the subscribed events.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `webhook_id` | string | Yes | The `webhook_id` from `list_webhooks`. |
| `name` | string | No | New name. |
| `url` | string | No | New `https` URL on port 443. |
| `event_types` | string\[] | No | Replaces the subscribed events. |
| `is_active` | boolean | No | `false` pauses the webhook; `true` turns it on. |
| `receive_simulated_events` | boolean | No | Also deliver events from the Event Simulator. |
| `purchase_date_start` | date | No | New start of the purchase window. |
| `purchase_date_end` | date | No | New end of the purchase window. |

**Response format:** the updated webhook, in the `list_webhooks` shape.

> [!NOTE]
>
> This tool can set purchase date bounds but not clear them. Clear a bound with the [Update webhook](/docs/api-reference/webhooks/update-webhook.md) API. The signing secret doesn't change.

### delete\_webhook

[API: DELETE /webhooks/\{webhook\_id}](/docs/api-reference/webhooks/delete-webhook.md)

Delete a webhook. It stops receiving events immediately, and this can't be undone. To stop deliveries temporarily, pause it with `update_webhook` and `is_active: false` instead.

**Parameters:**

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `webhook_id` | string | Yes | The `webhook_id` from `list_webhooks`. |

**Response format:**

```json theme={null}
{
  "webhook_id": "01JA7Q2W3E4R5T6Y7U8I9O0P1A",
  "deleted": true
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.