> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.thepennyinc.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.thepennyinc.com/_mcp/server.

# Spend Controls

A Spend Control is a named bundle of rules you can assign to a card, cardholder, account, or your whole business. One spend control can be shared across many objects: assign the same `spend_control_id` to ten cards, and updating the control updates the limit for all ten at once.

Spend controls are created and managed on Penny Banking (`https://sandbox.api.thepennyinc.com`). Card and cardholder assignment endpoints are on Penny Issuing (`https://issuing.sandbox.api.thepennyinc.com`).

## Rule types

Every spend control is made of one or more rules. In a request, each rule is a `rule_type` plus a typed `configuration` object whose shape depends on that type:

| Rule type            | Controls                                                                                                             |
| -------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `transaction_amount` | Minimum and/or maximum amount per transaction                                                                        |
| `velocity`           | Count or amount limits per calendar hour, day, week, month, quarter, or year (UTC), or over the lifetime of the rule |
| `merchant`           | Allowed or blocked merchant IDs, merchant category codes, and merchant names                                         |
| `geographic`         | Allowed or blocked merchant countries, subdivisions (states or provinces), and postal codes                          |
| `currency`           | Allowed or blocked transaction currencies                                                                            |
| `processing`         | Allowed or blocked PAN entry modes                                                                                   |
| `live_decisioning`   | A real-time approve or decline decision from your own endpoint                                                       |

Use the rule guides for configuration shapes and evaluation behavior: [Transaction Amount](/spend-controls/transaction-amount-rules), [Velocity](/spend-controls/velocity-rules), [Merchant](/spend-controls/merchant-rules), [Geographic](/spend-controls/geographic-rules), [Currency](/spend-controls/currency-rules), [Processing](/spend-controls/processing-rules), and [Live Decisioning](/spend-controls/live-decisioning-rules). See [Missing Transaction Details](/spend-controls/missing-transaction-details) for how checks behave when a required value is unavailable. Fetch the current schema and an example for every rule type before building a request:

```http
GET /spend_controls/rule_types
```

## Allowlists and denylists

The `merchant`, `geographic`, `currency`, and `processing` rules use `allowlist` and `denylist` arrays. Configure one list per block:

* **Allowlist only:** only the listed values pass.
* **Denylist only:** every value passes except the listed ones.

A value can't appear in both lists; the request is rejected. If you set both lists on the same block, the denylist governs: a value passes when it is on the allowlist or absent from the denylist, so the allowlist doesn't narrow anything further. Merchant category codes (`merchant_category_codes_rules`) are the exception: when that block has an allowlist, only the allowlist is checked and the denylist is ignored.

## Creating a spend control

Each rule must explicitly list its `applicable_transaction_authorizations` and `applicable_card_issuance_modes`. See [Rule Applicability](/spend-controls/rule-applicability) for the supported values. Each `rule_type` can appear at most once in a spend control.

**`POST /spend_controls/`**

```json title="POST /spend_controls/"
{
  "rule_configs": [
    {
      "rule_type": "transaction_amount",
      "applicable_transaction_authorizations": ["debit"],
      "applicable_card_issuance_modes": ["managed"],
      "configuration": {
        "USD": { "minimum": "10.00", "maximum": "500.00" }
      }
    },
    {
      "rule_type": "velocity",
      "applicable_transaction_authorizations": ["debit"],
      "applicable_card_issuance_modes": ["managed"],
      "configuration": {
        "velocity_controls": {
          "USD": { "week": { "usage_limit": 20, "amount_limit": "2000.00" } }
        }
      }
    }
  ]
}
```

### The spend control response

Responses return the control's rules in a `rules` array. Each rule has its own `rule_id`, `version`, and `version_time`, and its configuration fields appear directly on the rule object rather than inside a `configuration` wrapper. Field names differ by rule type. A `transaction_amount` rule returns its limits as `currency_amount_limits`, and a `velocity` rule returns each window's current `usage` and `balance`:

**`201 Created`**

```json title="201 Created"
{
  "spend_control_id": "spend_control_019379d9-a170-796b-867a-a17096b67a21",
  "version": 1,
  "version_time": "2026-10-01T14:05:12Z",
  "active": true,
  "status": "active",
  "rules": [
    {
      "rule_id": "spend_control_rule_019370e9-c1a6-78ad-8fee-c1a68adfee20",
      "version": 1,
      "version_time": "2026-10-01T14:05:12Z",
      "applicable_card_issuance_modes": ["managed"],
      "applicable_transaction_authorizations": ["debit"],
      "rule_type": "transaction_amount",
      "currency_amount_limits": {
        "USD": { "minimum": "10.00", "maximum": "500.00" }
      }
    },
    {
      "rule_id": "spend_control_rule_019370ea-2b41-7c1e-9d3a-2b417c1e9d3a",
      "version": 1,
      "version_time": "2026-10-01T14:05:12Z",
      "applicable_card_issuance_modes": ["managed"],
      "applicable_transaction_authorizations": ["debit"],
      "rule_type": "velocity",
      "velocity_controls": {
        "USD": {
          "week": {
            "usage": {
              "count": 0,
              "count_limit": 20,
              "limit_window": "week",
              "last_reset_time": "2026-09-28T00:00:00Z"
            },
            "balance": {
              "currency": "USD",
              "available_balance": "2000.00",
              "amount_limit": "2000.00",
              "limit_window": "week",
              "last_reset_time": "2026-09-28T00:00:00Z"
            }
          }
        }
      }
    }
  ],
  "business_id": "business_01937b95-484e-75af-8c68-484e5afc68d3"
}
```

`status` is one of `active`, `deactivated`, `deleting`, or `deleted`. Read-back field names for the other rule types:

| Rule type          | Response fields                                                             |
| ------------------ | --------------------------------------------------------------------------- |
| `merchant`         | `merchant_id_rules`, `merchant_category_codes_rules`, `merchant_name_rules` |
| `geographic`       | `country_rules`, `subdivision_rules`, `zip_code_rules`                      |
| `currency`         | `currency_rules`                                                            |
| `processing`       | `pan_entry_mode_rules`                                                      |
| `live_decisioning` | `notification_endpoint_id`, `fallback_result`                               |

## Managing a spend control

All of these endpoints are on Penny Banking:

| Action               | Endpoint                                                      |
| -------------------- | ------------------------------------------------------------- |
| Create               | `POST /spend_controls/`                                       |
| List                 | `GET /spend_controls/`                                        |
| Retrieve             | `GET /spend_controls/{spend_control_id}`                      |
| Add or replace rules | `PATCH /spend_controls/{spend_control_id}`                    |
| Remove one rule      | `DELETE /spend_controls/{spend_control_id}/rules/{rule_type}` |
| Pause enforcement    | `PATCH /spend_controls/{spend_control_id}/deactivate`         |
| Resume enforcement   | `PATCH /spend_controls/{spend_control_id}/activate`           |
| Delete               | `DELETE /spend_controls/{spend_control_id}`                   |

`PATCH /spend_controls/{spend_control_id}` replaces each rule you send with the same `rule_type` and leaves the other rules unchanged.

Deactivating a control pauses enforcement of its rules; resume enforcement with `activate`. Deleting a control removes it from every object it is assigned to. The delete response returns the control with `status: "deleting"` while removal is in progress; it then moves to `deleted` and is kept for historical reference.

## Assigning it

A spend control applies once it is assigned to the object you want it to govern. Each object has at most one spend control; assigning another replaces it.

| Attach to       | Endpoint                                           | API           |
| --------------- | -------------------------------------------------- | ------------- |
| Card            | `PUT /cards/{card_id}/spend_control`               | Penny Issuing |
| Cardholder      | `PUT /cardholders/{cardholder_id}/spend_control`   | Penny Issuing |
| Capital Account | `PUT /accounts/capital/{account_id}/spend_control` | Penny Banking |
| Control Account | `PUT /accounts/control/{account_id}/spend_control` | Penny Banking |
| Business        | `PUT /businesses/spend_control`                    | Penny Banking |

Each of these accepts a body of `{ "spend_control_id": "spend_control_019379d9-a170-796b-867a-a17096b67a21" }`. Remove an assignment with the matching `DELETE` on the same path.

### Associations

The associations endpoints on Penny Banking let you look up assignments from either side:

| Action                                    | Endpoint                                                     |
| ----------------------------------------- | ------------------------------------------------------------ |
| List the objects a control is assigned to | `GET /spend_controls/{spend_control_id}/associations`        |
| Get the control assigned to an object     | `GET /spend_controls/associations/{associated_object_id}`    |
| Remove the control assigned to an object  | `DELETE /spend_controls/associations/{associated_object_id}` |

The list endpoint is paginated with `page_size` and `next_page_token`. Each item has `spend_control_id`, `associated_object_id`, and `associated_object_type` (`business`, `business_entity`, `program`, `account`, `cardholder`, or `card`). `GET /spend_controls/associations/{associated_object_id}` returns the full spend control. The `DELETE` returns `204 No Content`.

## Business-wide rules

To apply a rule to every card, cardholder, and account in your business, such as blocking a merchant category everywhere or restricting all spend to a set of countries, attach a spend control to the business itself:

**`cURL`**

```bash title="cURL"
curl -X PUT https://sandbox.api.thepennyinc.com/businesses/spend_control \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spend_control_id": "spend_control_019379d9-a170-796b-867a-a17096b67a21"}'
```

**`Python`**

```python title="Python"
import requests

requests.put(
    "https://sandbox.api.thepennyinc.com/businesses/spend_control",
    headers={"Authorization": f"Bearer {access_token}"},
    json={"spend_control_id": "spend_control_019379d9-a170-796b-867a-a17096b67a21"},
)
```

**`JavaScript`**

```javascript title="JavaScript"
await fetch("https://sandbox.api.thepennyinc.com/businesses/spend_control", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ spend_control_id: "spend_control_019379d9-a170-796b-867a-a17096b67a21" }),
});
```

A business-wide control is checked on every authorization, alongside any control attached to the card, cardholder, or account involved. Remove it with `DELETE /businesses/spend_control`.

## Where a limit applies

Spend controls can attach at several levels, and more than one can apply to a single transaction, for example a card's own limit, its cardholder's limit, and a business-wide rule. Penny checks every attached control, from the most specific level to the broadest:

```
Card → Cardholder → Control Account (if present) → Capital Account → Program → Business Entity → Business
```

When a card uses a Control Account, its attached control is checked at the narrower Control Account level, and the funding Capital Account's control is checked at the broader level. Both apply when both are attached. Without a Control Account, the Capital Account level still applies.

The transaction is authorized only if it passes every rule in every applicable control. No level overrides another, so when two attached controls apply the same `rule_type` to the same transaction, such as a `transaction_amount` limit on both the card and its cardholder, each is enforced and the most restrictive one determines the outcome. A \$500 per-transaction limit on a card and a \$200 limit on its cardholder together decline anything over \$200.

`live_decisioning` is the exception: it runs once per authorization, at the most specific level where a `live_decisioning` rule is attached. A `live_decisioning` rule at a broader level is skipped when a more specific level already has one. See [Live Decisioning](/platform/webhooks-live-decisioning).

See [Setting Spend Controls](/spend-controls/setting-spend-controls) for a worked example of building and assigning one.