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

# Rules

> Create and attach ordered API v2 rules that skip, recategorise, rename, or annotate transactions before delivery.

A **ruleset** is a named, reusable list of up to 50 **rules**; a sync has at most one ruleset attached. Each rule has `conditions` (groups of conditions: OR across groups, AND within a group) and one `action`. The [Rules guide](/docs/rules) covers the behaviour; this page covers the API surface.

```json theme={null}
{
  "id": "rule_2Ab4Cd6eFg8hJk0lMn2oPq",
  "object": "rule",
  "ruleset": "rset_6Kp8Lm1nQr3sTu5vWx7yZa",
  "name": "Skip internal transfers",
  "position": 0,
  "enabled": true,
  "conditions": { "groups": [ { "conditions": [ { "field": "description", "operator": "contains", "value": "transfer to savings" } ] } ] },
  "action": { "type": "skip" },
  "metadata": null,
  "livemode": true,
  "created": "2026-08-21T02:00:00.000Z",
  "updated": "2026-08-21T02:00:00.000Z"
}
```

**Fields:**

* Strings: `merchantName`, `description`, `reference`, `accountName`, `merchantCategoryCode`
* Enums: `direction`, `category`, `subcategory`, `customCategoryGroup`, `class`, `accountId`, `currency`, `dayOfWeek` (`monday`…`sunday`)
* Numbers: `amount` (absolute), `signedAmount` (keeps the sign), `dayOfMonth` (1–31)

`category` reads the resolved custom category when the user has one, else the raw bank category. `dayOfMonth`/`dayOfWeek` derive from the posting date in the account's timezone.

**Operators:**

* Strings: `contains`, `does_not_contain`, `equals`, `starts_with`, `ends_with`, `matches_glob` / `does_not_match_glob` (`*` and `?` wildcards, not regex), `is_empty` / `is_not_empty` (omit `value`), `is_in`, `is_not_in`
* Enums: `is`, `is_not`, `is_in`, `is_not_in`
* Numbers: `gt`, `lt`, `eq`, `between`

**Actions:** `{ "type": "skip" }`, `{ "type": "email" }` (notify by email, still written), `{ "type": "set_category", "value": "…" }`, `{ "type": "set_category_group", "value": "…" }`, `{ "type": "clear_category" }`, `{ "type": "set_merchant", "value": "…" }`, `{ "type": "set_description", "value": "…" }`, `{ "type": "set_note", "value": "…" }`, `{ "type": "add_tag", "value": "…" }`.

Merchant, description, note, and tag values support `{merchant}`, `{description}`, `{category}`, `{subcategory}`, `{amount}`, `{account}`, `{reference}`, and `{date}` placeholders. Rules evaluate in `position` order; the first matching rule wins each field, and the first `skip` stops evaluation.

Notes and tags land in dedicated Note/Tags fields on Sheets, Notion, Airtable, and Webhook destinations (`note` / `tags` payload keys); the sync must have custom categories enabled, and YNAB is excluded. Category actions write the enriched `custom_category` / `custom_category_group` fields under the same gating.

`value` is always a string, or an array of strings for `between`, `is_in` and `is_not_in`. A number is rejected with `400 parameter_invalid`.

<Warning>
  Rule amounts are the **only** place in v2 where money is not minor units. An `amount` value is a decimal string in major units — `"100"` means one hundred dollars, not one hundred cents — and it is compared against the **absolute** value of the transaction, so `gt "100"` matches a −$104.96 debit and a +$104.96 credit alike. Use a `direction` condition in the same group to restrict to one side. `between` takes two values and orders them for you.
</Warning>

Dry-run a ruleset before attaching it: `POST /v2/rulesets/{id}/preview` returns the actions each recent transaction would receive, and writes nothing.

| Method and path                         | Scope              | Notes                                                                                                                                   |
| --------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v2/rulesets`, `POST /v2/rulesets` | rules:read / write | `{ name, enabled?, metadata? }`                                                                                                         |
| `GET`/`POST`/`DELETE /v2/rulesets/{id}` | rules:read / write |                                                                                                                                         |
| `POST /v2/rulesets/{id}/duplicate`      | rules:write        | Copies the ruleset and its rules                                                                                                        |
| `GET`/`POST /v2/rulesets/{id}/rules`    | rules:read / write | Create appends; 409 `rule_limit_reached` at 50                                                                                          |
| `POST /v2/rulesets/{id}/rules/reorder`  | rules:write        | `{ rules: [every rule id, in order] }`                                                                                                  |
| `GET`/`POST`/`DELETE /v2/rules/{id}`    | rules:read / write |                                                                                                                                         |
| `POST /v2/rulesets/{id}/preview`        | rules:read (heavy) | `{ account, limit?, from?, to? }` dry run over recent transactions; returns `{ transaction, actions, skipped }` per row, writes nothing |
| `POST /v2/syncs/{id}/rulesets`          | syncs:write        | Attach (`["rset_…"]`) or detach (`[]`)                                                                                                  |
