# Create a bank rule

Operation ID: `createBankRule`

Group: Banking › Bank Rules

[Open in the interactive reference](https://docs.api.lensing.ai/lensing/v1/endpoints/createBankRule)

`POST /banking/bank-rules`

Retry-safe: A write that accepts Idempotency-Key: repeating a request with the same key replays the stored answer instead of executing twice.

Creates a bank rule, with its criteria, accounts, and action object. The response contains only its `id`. Read the record back with `GET banking/bank-rules/{id}` if you need its stored state; `bankRuleNumber` comes from a gapless number range and is visible only on that follow-up GET.

A rule is created whole: the header, at least one criterion, the watched bank accounts, and the action object describing the entry to create. `createBankEntry` and `createCreditCardEntry` need at least one watched account; `createFundTransfer` derives its two sides from the action object and takes none.

**The watched accounts constrain the action.** For `createBankEntry` and `createCreditCardEntry` every account must belong to one legal entity (`ACCOUNTS_SPAN_ENTITIES`), and they must be either all credit card accounts or all not (`MIXED_ACCOUNT_TYPES`); `createCreditCardEntry` then works only with credit card accounts and `createBankEntry` only with the rest, a mismatch being `ACTION_ACCOUNT_TYPE_MISMATCH`. `createFundTransfer` is exempt from all three: a transfer spans two accounts, so it may mix entities (intercompany) and account types.

**Only three of the four rule actions are implemented.** `autoMatch` is rejected with `RULE_ACTION_NOT_SUPPORTED`, which is why the input is a three-member union rather than the whole enum.

**Operators are constrained by the criterion.** Numeric operators apply only to `amount`, text operators only to `description` and `payeePayor`, and an `amount` value must parse as a number. Anything else is `INVALID_CRITERIA` with the offending criterion named.

All guards run before the first write, so a rejected request creates nothing.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Api-Version` | header | string · v1 | yes | The API version this request targets, named by its public identifier. Required on every request; omitting it, or naming a version this company does not have, is answered with 400. This document describes version `v1`. |
| `Idempotency-Key` | header | string | no | An opaque caller-chosen string (1-255 characters) that makes this request safe to retry. Repeating the same key with an identical request body replays the original response instead of re-executing - but only once that original request succeeded; a failed attempt re-executes on retry. Repeating the key with a different body is answered with 422. A key still in flight is answered with 409 - as is a key whose original request was interrupted before it finished, for a few minutes afterwards, until that attempt is presumed dead. A completed response stays replayable for 24 hours. The key is scoped to the caller, not to the action: reusing it for a different action is answered with 409 or 422 rather than treated independently. On read actions this header is ignored. On write actions that cannot offer the guarantee it is answered with `400 IDEMPOTENCY_NOT_SUPPORTED` rather than silently ignored. |

## Request body

Required, `application/json`: createBankRuleInput.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `rule` | string | yes | The name of the bank rule. Must be unique within the tenant. Example: `"Stripe payouts"`. |
| `action` | any | yes | What the rule does when a transaction matches. Only these three are implemented; autoMatch is rejected with RULE_ACTION_NOT_SUPPORTED. One of: `"createBankEntry"`, `"createCreditCardEntry"`, `"createFundTransfer"`. Example: `"createBankEntry"`. |
| `status` | any | yes | The status of the rule. Accepted values: 'active' (the rule runs whenever bank rules run), 'inactive' (the rule is kept but does not run). One of: `"active"`, `"inactive"`. Example: `"active"`. |
| `priority` | number | yes | The order in which this rule runs relative to other rules. Lower numbers run first. A bank transaction matched by one rule is no longer available to the rules after it. Example: `10`. |
| `description` | string | no | A description of the rule. Example: `"Book Stripe payouts to the clearing account"`. |
| `bankAccountIds` | array<string> | yes | The bank accounts or credit card accounts this rule applies to. At least one is required. They must all belong to the same legal entity, and must be all bank accounts or all credit card accounts, not a mix. See id on GET banking/bank-accounts/:id. Example: `["8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e"]`. |
| `criteria` | array<object> | yes | The conditions a bank transaction must meet for the rule to act on it. At least one is required. They are evaluated in the order given. Example: `[{"criteria":"description","operator":"containsCaseInsensitive","logicalOperator":"and","value":"STRIPE"}]`. |
| `criteria[].criteria` | any | yes | The bank transaction field this criterion tests. Accepted values: 'amount' (the signed transaction amount, negative for money out), 'description' (the description as the bank supplied it), 'payeePayor' (the name of the counterparty). One of: `"amount"`, `"description"`, `"payeePayor"`. |
| `criteria[].operator` | string | yes | How the field is compared with value. When criteria is 'amount', use 'equal', 'notEqual', 'greaterThan', 'greaterThanOrEqual', 'lessThan' or 'lessThanOrEqual'. When criteria is 'description' or 'payeePayor', use 'is', 'isNot', 'contains' or 'doesNotContain', which are case-sensitive, or 'isCaseInsensitive', 'isNotCaseInsensitive', 'containsCaseInsensitive' or 'doesNotContainCaseInsensitive'. |
| `criteria[].logicalOperator` | any | no | How this criterion combines with the next one. Accepted values: 'and', 'or'. Defaults to 'and'. Ignored on the last criterion. Consecutive criteria joined by the same operator form a group, and groups are always combined with 'and'. For example, A or B, and C matches (A or B) and C, while A and B, or C matches A and B and C. Ignored when criteriaGroups is sent. One of: `"and"`, `"or"`. |
| `criteria[].value` | string | yes | The value the field is compared with. Cannot be empty. When criteria is 'amount', send a number, for example '-250.00'. Otherwise, send text. |
| `criteria[].groupKey` | string | no | The key of the criteria group this criterion belongs to. See key on criteriaGroups. Required when criteriaGroups is sent, and left out otherwise. |
| `criteriaGroups` | array<object> | no | Optional nested AND/OR groups for the criteria. When sent, exactly one group has no parentKey and is the root, every criterion names its group in groupKey, and each group combines its criteria and child groups with its own logicalOperator. For example, (A and B) or C is a root 'or' group holding C and a child 'and' group holding A and B. When left out, criteria are combined using logicalOperator on each criterion. |
| `criteriaGroups[].key` | string | yes | A key you choose for the group, unique within this request. Criteria refer to it in groupKey and child groups in parentKey. |
| `criteriaGroups[].parentKey` | string | no | The key of the parent group. Left out for the root group. |
| `criteriaGroups[].logicalOperator` | any | yes | How the group combines its criteria and child groups. Accepted values: 'and', 'or'. One of: `"and"`, `"or"`. |
| `actionObject` | object | yes | The values for the bank entry or credit card entry the rule creates for each bank transaction that meets its criteria. All of its fields are optional. Example: `{"accountId":"6d1e4b27-8a90-4c31-b5f2-7e8a9c0d1b23","name":"Stripe payout","description":"Auto-created by rule"}`. |
| `actionObject.accountId` | string | no | The account each bank entry or credit card entry posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. |
| `actionObject.name` | string | no | The name given to each bank entry or credit card entry. |
| `actionObject.description` | string | no | The description set on each bank entry or credit card entry and on its line. |
| `actionObject.billableExpense` | any | no | Marks the line of each credit card entry as a billable expense when 'yes'. Accepted values: 'yes', 'no'. Applies only to rules with the action 'createCreditCardEntry'. One of: `"yes"`, `"no"`. |
| `actionObject.businessUnitId` | string | no | The business unit set on each bank entry or credit card entry. |
| `actionObject.departmentId` | string | no | The department set on each bank entry or credit card entry. |
| `actionObject.customerId` | string | no | The customer set on each credit card entry. Use only when billableExpense is 'yes'. Not applied to bank entries. See id on GET accounting/customers/:id. |
| `actionObject.vendorId` | string | no | A vendor to save on the rule. It is not applied to bank entries or credit card entries. See id on GET accounting/vendors/:id. |
| `actionObject.intercompanyPartnerId` | string | no | The intercompany partner set on each bank entry or credit card entry. Applied only when accountId is an elimination account, and ignored otherwise. No endpoint returns intercompany partners. |
| `actionObject.valueAllocationTags` | array<string> | no | The value allocation tag codes set on each bank entry or credit card entry. A tag is applied only while value allocation tags are turned on and its dimension is enabled for that type of entry. |
| `actionObject.fromAccountId` | string | no | [fk: AccountApi.accounts.id] createFundTransfer only. Source cash account of the transfer, given as its id (a UUID). Required for createFundTransfer. |
| `actionObject.toAccountId` | string | no | [fk: AccountApi.accounts.id] createFundTransfer only. Destination cash account of the transfer, given as its id (a UUID). Required for createFundTransfer, and must differ from fromAccountId. |
| `actionObject.fromLineDescription` | string | no | createFundTransfer only. Description stamped on the transfer's From line. |
| `actionObject.toLineDescription` | string | no | createFundTransfer only. Description stamped on the transfer's To line. |
| `actionObject.intercompanyProcessId` | string | no | [fk: IntercompanyProcessApi.intercompanyProcesses.id] createFundTransfer only, given as its id (a UUID). Required when the two accounts belong to different legal entities. |
| `actionObject.transferReason` | any | no | createFundTransfer only. Required when the two accounts belong to different legal entities. One of: `"funding"`, `"repayment"`. |
| `actionObject.intercompanyTransactionalCurrency` | string | no | createFundTransfer only. Currency the intercompany balance is carried in. Required when the two accounts belong to different legal entities. |
| `actionObject.fromFeeToleranceType` | any | no | createFundTransfer only. Whether the sending-side bank fee tolerance is read as a percentage or a fixed amount. One of: `"percentage"`, `"fixedAmount"`. |
| `actionObject.fromFeeToleranceValue` | decimal | no | createFundTransfer only. Sending-side tolerance; a shortfall up to this much is booked as a bank fee, beyond it the two transactions are not paired. Left unset, the amounts must match exactly. |
| `actionObject.toFeeToleranceType` | any | no | createFundTransfer only. Whether the receiving-side bank fee tolerance is read as a percentage or a fixed amount. One of: `"percentage"`, `"fixedAmount"`. |
| `actionObject.toFeeToleranceValue` | decimal | no | createFundTransfer only. Receiving-side tolerance; a shortfall up to this much is booked as a bank fee, beyond it the two transactions are not paired. Left unset, the amounts must match exactly. |

```json
{
  "rule": "Stripe payouts",
  "action": "createBankEntry",
  "status": "active",
  "priority": 10,
  "description": "Book Stripe payouts to the clearing account",
  "bankAccountIds": [
    "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e"
  ],
  "criteria": [
    {
      "criteria": "description",
      "operator": "containsCaseInsensitive",
      "logicalOperator": "and",
      "value": "STRIPE"
    }
  ],
  "criteriaGroups": [
    {
      "key": "<key>",
      "parentKey": "<parentKey>",
      "logicalOperator": "and"
    }
  ],
  "actionObject": {
    "accountId": "6d1e4b27-8a90-4c31-b5f2-7e8a9c0d1b23",
    "name": "Stripe payout",
    "description": "Auto-created by rule"
  }
}
```

## Response

`200` Success

Under `data` (createBankRuleOutput):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | The unique identifier for the created bank rule. Example: `"9f2c6a10-7d34-4c8e-9b21-5a0f3e6d81b7"`. |

```json
{
  "data": {
    "id": "9f2c6a10-7d34-4c8e-9b21-5a0f3e6d81b7"
  },
  "diagnostics": [
    {
      "code": "<code>",
      "message": "<message>",
      "severity": "error",
      "path": [
        "<path>"
      ],
      "details": "<details>"
    }
  ]
}
```

## Response headers

- `Idempotency-Replayed` — Present and `true` only when this response replays a previously stored one for the same Idempotency-Key, rather than reflecting a live execution. Absent on a live response - never sent as `false`.

## Errors

| Code | Status |
| --- | --- |
| `REFERENCE_NOT_FOUND` | 400 |
| `ACCOUNTS_REQUIRED` | 400 |
| `CRITERIA_REQUIRED` | 400 |
| `ACTION_OBJECT_REQUIRED` | 400 |
| `ACCOUNTS_SPAN_ENTITIES` | 400 |
| `MIXED_ACCOUNT_TYPES` | 400 |
| `ACTION_ACCOUNT_TYPE_MISMATCH` | 400 |
| `RULE_ACTION_NOT_SUPPORTED` | 400 |
| `DUPLICATE_RULE_NAME` | 400 |
| `INVALID_FUND_TRANSFER` | 400 |
| `INVALID_CRITERIA` | 400 |
| `ACCOUNT_NOT_POSTABLE` | 400 |
| `IDEMPOTENCY_KEY_CONFLICT` | 409 |
| `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 |

## Statuses

- `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, ACCOUNTS_REQUIRED, CRITERIA_REQUIRED, ACTION_OBJECT_REQUIRED, ACCOUNTS_SPAN_ENTITIES, MIXED_ACCOUNT_TYPES, ACTION_ACCOUNT_TYPE_MISMATCH, RULE_ACTION_NOT_SUPPORTED, DUPLICATE_RULE_NAME, INVALID_FUND_TRANSFER, INVALID_CRITERIA, ACCOUNT_NOT_POSTABLE
- `401` — The request carries no valid access token.
- `403` — Not authorized
- `404` — Not found
- `409` — The Idempotency-Key is still being processed by another request.
- `422` — The Idempotency-Key was already used with a different request payload.
- `429` — Too many requests from this client address. The response has no body and no `Retry-After` header.
- `500` — Internal server error
