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

# Seller limits and the buyer rate card

> Set limits on what your agent may offer a buyer with save_playbook, and read the terms that apply to one buyer with the rate_card include.

A limit says when an offer to a buyer needs your approval first, or is never
made at all. You write limits on your playbook with `save_playbook`. The agent
that answers your buyers' proposal requests, hosted by Apostra or your own,
reads the terms that apply to one buyer, limits included, from that buyer's
rate card.

> [!NOTE]
>
> Limits and the rate card are in beta for Seller Accounts on the beta profile.
> Elsewhere `limits` and `include: ["rate_card"]` are refused with
> `FEATURE_NOT_ENABLED`. In this beta your limits are something your agent
> reads; Apostra does not yet hold back an offer that breaks one by itself.

## Who can set limits

Only a person sets limits directly: you, signed in, or through your own API
key or connected chat. A call from an agent acting on its own, such as a hosted
agent's scheduled run, or made with an organization API key, is refused with
`ACCESS_DENIED`. So is a limit change read from a Library file (a
`save_playbook` call that carries `materialCandidate`). Changes like these will wait for your approval once that
approval path is available; until then, make the change yourself.

## Write a limit

Each limit is a rule. A rule says which buyers, which products and which kind
of execution it covers, when it holds, and what happens then.

| Field | What it holds |
| - | - |
| `id` | Omit it to add a rule. Send the id from `get(playbook)` to replace that rule. |
| `houseDomain` | A buyer house such as `nike.com`, or `null` for every buyer. |
| `scope` | `brand` or `operator`: which side of the buyer `houseDomain` names. `null` when `houseDomain` is `null`. |
| `productIds` | The wholesale product ids the rule covers, or `null` for every product. |
| `execution` | `standard` or `custom`, or `null` for either. |
| `outcome` | `needs_approval`: you approve the offer before it is made. `never`: the offer is not made. |
| `when` | The conditions. The rule holds only when every condition you name holds. Name at least one. |

`houseDomain` and `scope` go together. Send both as `null` for a rule that
covers every buyer, or set both for one house. One set without the other is
refused with `VALIDATION_FAILED`. A house rule covers the house's brands or
operators as house discounts do: a `brand` rule on `nike.com` also covers a
Converse buy, because converse.com belongs to the Nike house.

The conditions in `when` are:

| Condition | Holds when |
| - | - |
| `lineItemBudgetBelow` | The line item's budget is below this amount. |
| `discountPercentAbove` | The discount off list price is above this percent. |
| `priceBelow` | The offered price is below this amount. |

`lineItemBudgetBelow` and `priceBelow` are amounts with their own currency,
such as `{ "amount": 30000, "currency": "USD" }`. The playbook and the rate
card return each amount in the currency you saved it in; in this beta Apostra
converts and compares nothing. When Apostra enforces limits, it will convert
an offer priced in another currency at the platform's exchange rate for that
day before comparing it, so a rule never lapses because an offer is priced in
another currency. It will also treat a condition as holding when no rate is
available, or when the offer states no budget or price.

### Example: no custom execution under \$30,000

```json theme={null}
{
  "limits": {
    "rules": [
      {
        "houseDomain": null,
        "scope": null,
        "productIds": null,
        "execution": "custom",
        "outcome": "never",
        "when": {
          "lineItemBudgetBelow": { "amount": 30000, "currency": "USD" }
        }
      }
    ]
  }
}
```

### Example: Nike-house discounts above 15% need your approval

```json theme={null}
{
  "limits": {
    "rules": [
      {
        "houseDomain": "nike.com",
        "scope": "brand",
        "productIds": null,
        "execution": null,
        "outcome": "needs_approval",
        "when": { "discountPercentAbove": 15 }
      }
    ]
  }
}
```

### Change or remove a rule

`limits` changes only the rules you name. A rule with an `id` replaces that
rule in full, so send every field, not only the one you are changing. Rules you
leave out stay as they are. To delete rules, list their ids in `remove`:

```json theme={null}
{ "limits": { "rules": [], "remove": ["42"] } }
```

All the rules and removals in one call are saved together: if any id is not a
rule of your account, nothing changes and the call says which id. One id may
appear only once across `rules` and `remove`.

An account holds at most 200 limit rules. A call that would leave more is
refused, and nothing in it is saved; remove rules you no longer need first.

## When several rules match

The strictest rule wins. `never` beats `needs_approval`, and `needs_approval`
beats an offer no rule stops. A rule matches an offer when it covers the buyer,
the product and the execution, and every condition in its `when` holds.

## Read your limits

`get(kind: "playbook")` returns your rules under `limits.rules`, in the same
shape `save_playbook` takes, plus three fields the server sets: `id`, `version`
(which goes up by one each time the rule changes) and `updatedAt`. To change a
rule, read it, drop `version` and `updatedAt`, change what you need, and save
it back with its `id`.

## Read one buyer's rate card

`get(kind: "account_relationship", id, include: ["rate_card"])` adds `rateCard`
to the relationship: the terms that apply to that buyer.

```ts theme={null}
rateCard: {
  version: string,
  currency: string | null, // your playbook's pricing currency
  lines: Array<{
    id: string,
    version: string,
    kind: "price" | "floor" | "discount" | "needs_approval" | "never",
    productIds: string[] | null, // null: every product
    value: object,               // depends on kind, see below
    source: object,              // where the line comes from
  }>,
}
```

| `kind` | Comes from | `value` |
| - | - | - |
| `price` | A playbook pricing fact with a target price | `appliesWhen` and `pricing` (`pricing_model`, `currency`, `target_price`, and `ceiling_price` when set) |
| `floor` | A playbook pricing fact with a floor price | `appliesWhen`, `pricing` (`pricing_model`, `currency`, `floor_price`) and the fact's `strength` |
| `discount` | The discount a quote gives this buyer: the larger of the house discount that matches the buyer's brand or operator and the `discountPercent` in your [buyer instructions](https://docs.apostra.com/v3/seller-buyer-instructions.md) for the buyer's operator and brand | `discountPercent` |
| `needs_approval`, `never` | Each limit that covers this buyer | The rule's `houseDomain`, `scope`, `execution` and `when` |

Floors come from your pricing facts. A floor you set for one buyer in your
buyer instructions is not part of the rate card yet.

Each line's `version` is derived from what the line says, and the card's
`version` from its lines. The same terms always read with the same versions,
and any change to a line changes both.

If the buyer's corporate hierarchy cannot be read, the rate card is not
returned, because a rule or discount keyed to the buyer's house could be
missing. Retry the read.

## Related

* [Seller buyer instructions](https://docs.apostra.com/v3/seller-buyer-instructions.md)
* [Listing, Playbook and Business Rules](https://docs.apostra.com/v2/setup/seller-pages.md)
* [Tool reference](https://docs.apostra.com/v3/tool-reference.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.