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

# Setting up a spending limit

> Cap what an account, a portfolio, or a segment can spend in a period with Tracer, activate the cap, and read how much of it is left from the validation decision.

export const GAuditTrail = ({children}) => <Tooltip headline="Audit trail" tip="A chronological, immutable record of every action and transaction in the system — essential for regulatory compliance and dispute resolution." cta="See glossary" href="/en/glossary">
    {children}
  </Tooltip>;

A product or risk decision has landed as a sentence: "no more than R\$ 20,000 per account per month." This guide turns that sentence into a live cap, and shows where to read how much of it a customer has left.

**What changes in your operation:** the ceiling stops being a constant compiled into a service. It becomes a stored definition you create, activate, raise, and stop — and every decision that touches it reports what it consumed.

<Tip>
  **Who is this guide for?** Product and risk teams setting the ceiling, and developers wiring `POST /v1/validations` into the payment path. Steps 1 and 2 are decisions to make before any call; steps 3 to 6 are the calls.
</Tip>

## Before you start

***

* [ ] Tracer running and reachable, with an API key — see [Getting started](./getting-started.mdx)
* [ ] The id of the account, portfolio, or segment the cap applies to
* [ ] The currency of the transactions you want capped
* [ ] Familiarity with limit types, time windows, and custom periods — see [Spending limits](./spending-limits.mdx)

All calls below send the API key as `X-API-Key` and run against `http://localhost:4020`.

***

## Step 1: Decide what the cap is addressed to

***

A limit carries a list of scope objects, and each object names what the limit applies to. These fields are available inside one object:

| Field             | Applies the limit to                                                         |
| ----------------- | ---------------------------------------------------------------------------- |
| `accountId`       | Transactions on one account                                                  |
| `portfolioId`     | Transactions on one portfolio                                                |
| `segmentId`       | Transactions on one segment                                                  |
| `merchantId`      | Transactions to one merchant                                                 |
| `transactionType` | One of `CARD`, `WIRE`, `PIX`, `CRYPTO`                                       |
| `subType`         | One transaction subtype, such as `purchase` — matched without regard to case |

Fields inside one object combine with AND. A field you leave out is a wildcard. Across objects the list combines with OR, so a limit with two objects applies when either one matches.

The choice that decides the answer to "R\$ 20,000 per account" is **which id you name**:

<Tabs>
  <Tab title="One account, its own budget">
    ```json theme={null}
    "scopes": [
      { "accountId": "550e8400-e29b-41d4-a716-446655440100" }
    ]
    ```

    The cap is addressed to that account. It counts that account's spending and no one else's. For a per-account ceiling across a book of customers, each account gets its own limit.
  </Tab>

  <Tab title="One segment, one shared budget">
    ```json theme={null}
    "scopes": [
      { "segmentId": "2f1c9b7e-40a5-4d18-9c6b-3e7f5a0d1b24" }
    ]
    ```

    The cap is addressed to the segment as a whole. Every account in that segment draws down the same R\$ 20,000 — the first customers to spend consume it for the rest.
  </Tab>

  <Tab title="Narrowed to one rail">
    ```json theme={null}
    "scopes": [
      {
        "accountId": "550e8400-e29b-41d4-a716-446655440100",
        "transactionType": "PIX"
      }
    ]
    ```

    The cap counts only that account's Pix transactions. Its card and wire traffic passes without touching this limit.
  </Tab>
</Tabs>

<Note>
  A limit also carries a `currency`, and Tracer checks a limit against a transaction only when the two match. A limit created in `BRL` is not checked against a `USD` transaction, so a book that settles in two currencies needs a limit for each.
</Note>

A limit must carry at least one scope object, and each object must set at least one field. An empty list, or an empty object, is rejected — see [What goes wrong](#what-goes-wrong).

***

## Step 2: Choose the period

***

`limitType` decides both the size of the ceiling's window and when a new count starts. Tracer offers five:

| `limitType`       | The count covers        | A new count starts                                        |
| ----------------- | ----------------------- | --------------------------------------------------------- |
| `DAILY`           | One calendar day, UTC   | Each day at 00:00 UTC                                     |
| `WEEKLY`          | One ISO week, UTC       | Each Monday at 00:00 UTC                                  |
| `MONTHLY`         | One calendar month, UTC | On the 1st at 00:00 UTC                                   |
| `CUSTOM`          | A date range you set    | Not at all — one count spans the whole range              |
| `PER_TRANSACTION` | A single transaction    | No count is kept; each transaction is measured on its own |

"per month" is `MONTHLY`.

<Warning>
  **The boundary is UTC, not local time.** A São Paulo customer spending at 21:30 on 31 July is at 00:30 UTC on 1 August, so that amount lands in the August count, not July's. When a cap is agreed in local terms, expect the last hours of the local month to belong to the next one.
</Warning>

`CUSTOM` takes `customStartDate` and `customEndDate` and is the shape for a campaign or a promotion. Those two fields belong to `CUSTOM` and are rejected on the other four types. To restrict a cap to certain hours of the day instead, see [time windows](./spending-limits.mdx#time-windows).

***

## Step 3: Create the limit

***

`POST /v1/limits` stores the definition. It is created in `DRAFT`, which means it is stored but not yet checked.

```bash theme={null}
curl -X POST http://localhost:4020/v1/limits \
  -H "X-API-Key: your-secure-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Monthly Account Spending Cap",
    "description": "Caps total monthly outflow for one account",
    "limitType": "MONTHLY",
    "maxAmount": "20000.00",
    "currency": "BRL",
    "scopes": [
      { "accountId": "550e8400-e29b-41d4-a716-446655440100" }
    ]
  }'
```

A successful call answers `201`. The body is the stored limit; two fields matter now:

| Field     | What to do with it                                                                       |
| --------- | ---------------------------------------------------------------------------------------- |
| `limitId` | The id every later call in this guide takes. Keep it                                     |
| `status`  | `DRAFT` — the limit is not being checked yet. [Step 4](#step-4-activate-it) changes that |

<Note>
  **Names are unique across limits, and the character set is narrow.** `name` accepts ASCII letters and digits, spaces, and `-`, `_`, `.`, `(`, `)`. An accent or a dash typed as an em dash is rejected with `0371`. A name another live limit already holds is rejected with `0442`; deleting a limit frees its name again.
</Note>

For the full payload and every optional field, see [Create a limit](/en/reference/tracer/create-limit).

***

## Step 4: Activate it

***

A `DRAFT` limit is not checked. Activation is what puts it in the path:

```bash theme={null}
curl -X POST http://localhost:4020/v1/limits/{limitId}/activate \
  -H "X-API-Key: your-secure-api-key"
```

The response carries the limit with `status` set to `ACTIVE`. From here, every `POST /v1/validations` whose transaction matches the scope and the currency is measured against it.

<Note>
  **Activating mid-month does not import the month's history.** Tracer counts a transaction against a limit at the moment it decides on it, so spending that happened while the limit was `DRAFT` is not in the count. A cap activated on the 20th governs what happens from the 20th onward.
</Note>

See [Activate a limit](/en/reference/tracer/activate-limit).

***

## Step 5: Read how much is left

***

Every `POST /v1/validations` response carries `limitUsageDetails`, with one entry per limit Tracer checked:

```json theme={null}
{
  "limitId": "b2d4f6a8-1c3e-4507-9b8d-6f0a2c4e6810",
  "limitAmount": "20000",
  "scope": "(account:550e8400-e29b-41d4-a716-446655440100)",
  "period": "MONTHLY",
  "currentUsage": "20000",
  "attemptedAmount": "1500",
  "exceeded": false
}
```

| Field             | What it reports                                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------------- |
| `limitAmount`     | The ceiling that was checked                                                                          |
| `currentUsage`    | Projected consumption of that limit's current period and matched scope if this transaction is allowed |
| `attemptedAmount` | The amount measured against the ceiling                                                               |
| `period`          | The limit's type — `MONTHLY` here                                                                     |
| `scope`           | The limit's scope as text, each object in parentheses, several objects joined by `OR`                 |
| `exceeded`        | Whether the amount would push this cap past its ceiling                                               |

In the entry above, a R\$ 1,500 purchase took an 18,500 month to exactly 20,000 — the cap is now spent, and the next transaction on that account is denied until August.

When a limit is exceeded, the decision is `DENY` and `reason` is `limit_exceeded`; the amount that would have crossed the ceiling is not added to the count. A limit produces a denial, not a flag for review. To go from such a denial back to the limit that caused it, see [Reviewing a denied transaction](./reviewing-a-denied-transaction.mdx).

### The other reading

`GET /v1/limits/{limitId}/usage` answers with a total for the limit rather than for one period: it adds up the usage counters recorded against it, across the periods and scopes it has accumulated. Use it to review how much a limit has absorbed overall — not to answer how much a customer has left this month.

A counter is deleted 90 days after its period ends, so on a long-running limit this total covers only the periods still retained, not the limit's full lifetime. See [Usage tracking](./spending-limits.mdx#usage-tracking).

See [Retrieve a limit usage snapshot](/en/reference/tracer/retrieve-limit-usage).

***

## Step 6: Change it, pause it, remove it

***

<Steps>
  <Step title="Raise or lower the ceiling">
    ```bash theme={null}
    curl -X PATCH http://localhost:4020/v1/limits/{limitId} \
      -H "X-API-Key: your-secure-api-key" \
      -H "Content-Type: application/json" \
      -d '{ "maxAmount": "30000.00" }'
    ```

    `name`, `description`, `maxAmount`, and `scopes` are editable, and so are the time-window and custom-period fields. `limitType` and `currency` are not — a request that sends either is rejected with `0380`, and a different period or currency means a new limit. A body with nothing in it is rejected with `0183`. See [Update a limit](/en/reference/tracer/update-limit).

    Changing the ceiling does not clear the running count. Lower it below what the current period already consumed and the account is denied until the next period starts.
  </Step>

  <Step title="Stop enforcing it">
    ```bash theme={null}
    curl -X POST http://localhost:4020/v1/limits/{limitId}/deactivate \
      -H "X-API-Key: your-secure-api-key"
    ```

    The limit moves to `INACTIVE` and stops being checked. It keeps its definition and its place in the <GAuditTrail>audit trail</GAuditTrail>, and `POST /v1/limits/{limitId}/activate` puts it back in the path. See [Deactivate a limit](/en/reference/tracer/deactivate-limit).
  </Step>

  <Step title="Take it back to draft">
    ```bash theme={null}
    curl -X POST http://localhost:4020/v1/limits/{limitId}/draft \
      -H "X-API-Key: your-secure-api-key"
    ```

    Moves an `INACTIVE` limit back to `DRAFT`. See [Return a limit to draft](/en/reference/tracer/draft-limit).
  </Step>

  <Step title="Remove it">
    ```bash theme={null}
    curl -X DELETE http://localhost:4020/v1/limits/{limitId} \
      -H "X-API-Key: your-secure-api-key"
    ```

    Answers `204`. A limit that is currently `ACTIVE` has to be deactivated first — Tracer rejects the deletion with `0363`, which is what keeps a live ceiling from disappearing by accident. See [Delete a limit](/en/reference/tracer/delete-limit).
  </Step>
</Steps>

### Find the limits you already have

`GET /v1/limits` lists them, and filters by the same scope fields you set in Step 1:

```http theme={null}
GET /v1/limits?status=ACTIVE&account_id=550e8400-e29b-41d4-a716-446655440100&limit_type=MONTHLY
X-API-Key: {api_key}
```

`name`, `status`, `limit_type`, `account_id`, `segment_id`, `portfolio_id`, `merchant_id`, `transaction_type`, and `sub_type` all filter; results are cursor-paginated. See [List limits](/en/reference/tracer/list-limits), and [Retrieve a limit](/en/reference/tracer/retrieve-limit) for one by id.

***

## What goes wrong

***

<Warning>
  **What usually goes wrong when a cap is set up:**

  * **"The customer overspent and nothing stopped them."** Check `status` first — a limit in `DRAFT` or `INACTIVE` is not checked. Then check that the limit's `currency` matches the transaction's, and that the scope names the id the transaction actually carried.
  * **"The segment cap ran out on day two."** A segment-scoped limit is one budget for the whole segment, not one per account in it. A per-account ceiling means a limit addressed to each account.
  * **"The month rolled over a few hours early."** Period boundaries are UTC. Spending after 21:00 in UTC-3 belongs to the next UTC day, and on the last day of the month, to the next month.
  * **"I raised the limit and the account is still denied."** Raising the ceiling does not clear the count. Confirm the new `maxAmount` is above what the period already consumed.
  * **"`GET /v1/limits/{id}/usage` reports more than the customer spent this month."** That endpoint totals the counters recorded for the limit, across periods and scopes. For the current period, read `limitUsageDetails` on the validation response.
</Warning>

### Error codes

| Code   | Status | What to change                                                                                                                                                                               |
| ------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0009` | 400    | A field failed validation — `detail` names it, for example `scopes must have at least 1 item(s)`, `scope at index 0 must have at least one field set`, or `maxAmount must be greater than 0` |
| `0065` | 400    | The id in the path is not a UUID                                                                                                                                                             |
| `0183` | 400    | The `PATCH` body carried no editable field                                                                                                                                                   |
| `0362` | 404    | No limit has that id                                                                                                                                                                         |
| `0363` | 422    | The transition is not allowed — deleting an `ACTIVE` limit, for instance                                                                                                                     |
| `0366` | 400    | `currency` is three uppercase letters but not an ISO 4217 code                                                                                                                               |
| `0371` | 400    | `name` carries a character the field does not accept                                                                                                                                         |
| `0380` | 422    | The request tried to change `limitType` or `currency`                                                                                                                                        |
| `0442` | 409    | Another live limit already holds that name                                                                                                                                                   |

The complete list is in the [Tracer error list](/en/reference/tracer/tracer-error-list).

***

## Quick reference

***

| Step                               | Method | Endpoint                         |
| ---------------------------------- | ------ | -------------------------------- |
| Create a limit                     | POST   | `/v1/limits`                     |
| Activate it                        | POST   | `/v1/limits/{id}/activate`       |
| Read consumption with the decision | POST   | `/v1/validations`                |
| Read the limit's cumulative total  | GET    | `/v1/limits/{id}/usage`          |
| Change the ceiling                 | PATCH  | `/v1/limits/{id}`                |
| Stop enforcing it                  | POST   | `/v1/limits/{id}/deactivate`     |
| Take it back to draft              | POST   | `/v1/limits/{id}/draft`          |
| Remove it                          | DELETE | `/v1/limits/{id}`                |
| Find limits                        | GET    | `/v1/limits` · `/v1/limits/{id}` |
