> ## 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/start-here/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 is no longer 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 asset code for 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 applies 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 applies 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 an `asset`, and Tracer checks a limit against a transaction only when the two match. Tracer does not check a limit created in `BRL` against a `USD` transaction. 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. Tracer rejects an empty list or an empty object. 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. That amount lands in the August count, not July's. When a cap uses 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`. Tracer rejects them 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. Tracer creates it in `DRAFT` and does not check it against transactions yet.

```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",
    "asset": "BRL",
    "scopes": [
      { "accountId": "550e8400-e29b-41d4-a716-446655440100" }
    ]
  }'
```

A successful call answers `201`. The body carries 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 `-`, `_`, `.`, `(`, `)`. Tracer rejects an accent, or a dash typed as an em dash, with `0371`. Tracer rejects a name another live limit already holds, with `0442`. Deleting a limit frees its name again.
</Note>

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

***

## Step 4: Activate it

***

Tracer does not check a `DRAFT` limit. 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, Tracer checks every `POST /v1/validations` whose transaction matches the scope and asset against this limit.

<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. 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/products/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 and spent the cap. Tracer denies the next transaction on that account until August.

When a transaction exceeds a limit, the decision is `DENY` and `reason` is `limit_exceeded`. Tracer does not add to the count the amount that would have crossed the ceiling. 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.

Tracer deletes a counter 90 days after its period ends. 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/products/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 `asset` are not. Tracer rejects a request that sends either with `0380`, and a different period or asset means a new limit. Tracer rejects an empty body with `0183`. See [Update a limit](/en/reference/products/tracer/update-limit).

    Changing the ceiling does not clear the running count. Lower it below what the current period already consumed, and Tracer denies the account 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 Tracer stops checking it. 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/products/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/products/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`. You have to deactivate a limit that is currently `ACTIVE` first. Tracer rejects the deletion with `0363`, which is what keeps a live ceiling from disappearing by accident. See [Delete a limit](/en/reference/products/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/products/tracer/list-limits), and [Retrieve a limit](/en/reference/products/tracer/retrieve-limit) for one by id.

***

## What goes wrong

***

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

  * **"The customer overspent and nothing stopped them."** Check `status` first. Tracer does not check a limit in `DRAFT` or `INACTIVE`. Then check that the limit's `asset` 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    | `asset` 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 `asset`                                                                                                                                           |
| `0442` | 409    | Another live limit already holds that name                                                                                                                                                   |

The complete list is in the [Tracer error list](/en/reference/products/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}` |
