# Shared budgets

> Set one limit that several agents draw on, and check purchases against it from the SDK.

- Human URL: https://conto.finance/docs/sdk/shared-budgets
- Raw Markdown: https://conto.finance/docs/sdk/shared-budgets.md
- Terminal view: https://conto.finance/ai/docs/sdk/shared-budgets

Documentation group: Spending controls

A shared budget is one limit that several agents draw on. The budget holder sets it in **Budgets** or
with `admin.spending`. Agents read it and check purchases against it with `conto.spending`. These
methods need SDK 0.8.0 or later.

```bash
npm install @conto_finance/sdk@^0.8.0
```

This is separate from a [budget request](https://conto.finance/docs/sdk/budget-requests), which one agent asks for and spends.

  A budget stops several payment paths for the agents it covers, even while it is paused. Read [what
  a budget stops](#what-a-budget-stops) and try a budget first on one agent with a Tempo Testnet wallet.

## Create a budget

Use an organization API key with `policies:write`. Amounts are exact decimal strings.

```typescript

const admin = new ContoAdmin({ orgApiKey: process.env.CONTO_ORG_API_KEY! });

const budget = await admin.spending.createBudget({
  name: 'Team tools',
  scope: 'AGENTS',
  participantIds: [agentA, agentB],
  amount: '500',
  currency: 'USDC',
  period: 'MONTHLY',
  periodStart: new Date().toISOString(),
  periodEnd: new Date(Date.now() + 365 * 86_400_000).toISOString(),
});
```

| Field            | Values                                                                 |
| ---------------- | ---------------------------------------------------------------------- |
| `scope`          | `WORKSPACE` for the whole organization, `AGENT` or `AGENTS` for named agents |
| `participantIds` | Exactly one agent for `AGENT`, one to 100 for `AGENTS`, none for `WORKSPACE`. They must be distinct agents in this organization |
| `period`         | `ONCE` for one limit across the whole date range. `MONTHLY` for a new full limit each UTC calendar month |
| `periodStart`, `periodEnd` | Required for both periods. UTC timestamps ending in `Z`. The end must be after the start |
| `requiresTask`   | Optional. `true` means a purchase must name an approved task           |

A budget created with `AGENT` or `AGENTS` reads back with `scope` set to `MEMBERS`, and `participantIds` still lists its agents.

A new budget counts from when it is saved. A payment or purchase already in progress for an agent it covers doesn't count against it. `POST /api/spending/budgets/impact` returns how many are in progress and their total before you save.

### More rules

SDK 0.8.0 and later type the fields above. These also work on `POST /api/spending/budgets` and `PATCH /api/spending/budgets/{id}`:

| Field | Values |
| ----- | ------ |
| `scope`, `members` | `MEMBERS` with up to 100 rows, each `{ agentId }` or `{ teamId }`, with an optional `limit` for that agent or team. A team covers each agent on it, and an agent that leaves the team stops being covered |
| `period` | Also `WEEKLY`, from Monday in UTC, and `QUARTERLY` and `YEARLY` on the UTC calendar |
| `periodStart`, `periodEnd` | Both optional here. The start defaults to now, and an end left out or `null` means the budget has no expiry |
| `perAgentLimit` | The most one agent can use in a period |
| `allowedMethods` | The payment methods the budget allows. It defaults to `MANAGED_WALLET`, `CONNECTED_CARD`, `STORE_SAVED_CARD`, `CHECKOUT` and `STRIPE_LINK`, the methods available on budgets today |
| `sourceMode`, `sources` | `ATTACHED` with up to 100 rows, each `{ walletId }` or `{ cardId }`, or the default `ANY` |
| `merchantMode`, `merchants` | `ALLOW` or `BLOCK` with up to 200 merchants, or the default `ANY`. A domain also matches its subdomains. Any other value, such as an address, must match exactly |
| `categoryMode`, `categoryIds` | `ALLOW` or `BLOCK` with up to 200 of your organization's own categories, or the default `ANY` |
| `ownerMembershipId`, `approverMembershipId` | Who owns the budget and who decides its task requests. Each must be an Owner, Admin or Manager. The owner defaults to the person who created the budget |

Category lists use what Conto observes about a purchase, such as the card network's merchant code, the merchant's domain and the payment method. They never use a label the agent supplies. With a category list set, a purchase Conto cannot place in a category is refused.

### Which budgets apply

Each budget that applies must pass, and each one is charged. The agent never names a budget. Two things decide which budgets apply: whether the budget covers the agent, and, with `sourceMode` set to `ATTACHED`, whether it lists the card or wallet paying.

A card or wallet that a budget lists spends only through a budget that lists it. An agent no such budget covers is refused with `BUDGET_SOURCE_NOT_ATTACHED`, on the [paths a budget stops](#what-a-budget-stops) as well. An agent whose budgets all list other cards and wallets gets the same refusal.

### Currency

A budget covers purchases in its own currency, so set it to what the wallets pay in: `USDC` on Base,
`USDC.e` on Tempo or `pathUSD` on Tempo Testnet. A `USD`, `USDC`, `USDC.e` or `USDT` budget counts those four and `pathUSD` at $1 each. Nothing converts, and other currencies stay separate. A `pathUSD` budget covers only `pathUSD`. The levels are workspace, members and the task a purchase names. A purchase counts against each budget that covers it at every level, and it must fit in each one. The exception is a `pathUSD` purchase: where a level has a `pathUSD` budget, only `pathUSD` budgets at that level count it, so test money stays off the dollar budgets beside it. Connected card draws, saved-card checkouts and checkout checkpoints are in `USD`, so a level with only `pathUSD` budgets refuses them.

A managed wallet payment or connected card draw is refused, whatever the budget's state, when a level
has budgets for that agent and none covers its currency. A budget's currency cannot be changed.

## Checkouts and Stripe Link

A checkout checkpoint, including `checkout.getLinkCard()`, and a guarded browser checkout (`runGuardedCheckout()`) hold spend against each budget that covers the agent. The hold is written when the checkout begins, after the agent's own limits pass and before Stripe Link is asked for a card. When the checkout is reported paid, the purchase settles on the budget. When it is cancelled or closed unpaid, the budget gets the amount back. A confirmed checkout with no outcome stays held until someone records what happened. A linked buyer's checkout gives an owner workspace's budget its amount back only as [Purchases and evidence](#purchases-and-evidence) describes.

A checkout paying with Stripe Link counts as `STRIPE_LINK`, and one paying any other way counts as `CHECKOUT`. Merchant and category lists use the checkout page's domain, not the merchant ID the agent sends.

The agent runs the checkout, so Conto declines approval at the limit. A payment the agent makes without asking Conto is not counted.

## What a budget stops

While a budget exists, it stops these paths for every agent it covers:

- x402 and MPP payments
- External wallet payments
- Agent-to-agent payments the covered agent makes. It can still receive them
- Payment grants
- Card checkout that Conto completes for an approved cart (`merchant:purchase`)
- Hosted Stripe Link
- Policy checks on customer-owned cards (`cards.approve()`)

A paused or expired budget still stops them. While paused and after its end date, a budget also refuses the managed wallet payments, connected card draws, saved-card checkouts, checkout checkpoints and guarded browser checkouts it covers, for those agents. `preview()` and `connectedCards.draw()` report `BUDGET_PAUSED` while it is paused and `BUDGET_EXPIRED` after its end date. A budget stops and refuses nothing until its start date.

Approving a [task](#tasks) creates a budget for that task. A task's budget stops nothing on other paths. It applies only to a managed wallet payment or connected card draw that names the task, and it refuses that payment or draw while it is paused.

Agents no budget covers are not affected, except a buyer linked to an owner workspace. The paths in the list above never open for a linked buyer. They fail with `reasonCode` `LINKED_BUDGET_REQUIRED` until a budget in that workspace covers it, and with `EXECUTION_PATH_NOT_CONNECTED` after that. A revoked link fails with `BUYER_LINK_REVOKED`. With no budget in that workspace, the buyer's managed wallet payments, connected card draws, saved-card checkouts and checkout checkpoints are refused too. See [linked buyers](https://conto.finance/docs/sdk/payments#shared-spending-controls).

## Change or end a budget

`updateBudget()` needs the budget's current `version`. It can change the amount, pause or resume the
budget with `isActive`, or move `periodEnd` to any time in the future, earlier or later. The API also
takes the fields in [more rules](#more-rules). Currency, period and start date cannot be changed. A
task's budget can only change its active state. Pausing stops new purchases on the budget and does
not lift what it stops on other payment paths.

A purchase already in progress keeps its hold when a change would now refuse it.

An organization API key with `policies:write`, an Owner or an Admin can make any change. A Manager who owns the budget can tighten it, and any other Manager's change fails with `reasonCode` `BUDGET_CHANGE_NOT_ALLOWED`. Raising or widening a budget needs an Owner or Admin. Removing a member, detaching a card or wallet, or starting a list of cards and wallets counts as widening, because each frees something from the limit. Organizations can require a [second approver](https://conto.finance/docs/guides/roles-permissions#require-a-second-approver-for-changes) for creating, widening and deleting budgets.

To end a budget, delete it in **Budgets** or call `deleteBudget()`. It then stops applying on every path, and its purchases stay in history. A linked buyer left with no budget in that workspace then fails with `reasonCode` `LINKED_BUDGET_REQUIRED`.

```typescript
const updated = await admin.spending.updateBudget(budget.id, {
  version: budget.version,
  amount: '750',
});

await admin.spending.deleteBudget(budget.id, { version: updated.version });
```

`deleteBudget()` fails with `reasonCode` `BUDGET_HAS_HOLDS` while a purchase holds spend against the
budget, and `TASK_BUDGET` for a task's budget, which ends by closing the task.

## Check a purchase

Use an agent SDK key. Nothing on `conto.spending` can raise a limit, approve a task, or release a
hold.

```typescript

const conto = new Conto({ apiKey: process.env.CONTO_API_KEY! });

const { budgets } = await conto.spending.budgets();

await conto.spending.preview({
  amount: '40',
  currency: 'USDC',
  sourceId: walletId,
  merchant: recipientAddress,
});
```

`preview()` needs a managed wallet linked to the agent, and that wallet's stablecoin as `currency`. It
holds nothing and approves nothing, so keep the request, approval and execute flow. A purchase the
budgets would refuse throws a `ContoError`.

`payments.request()` can return `APPROVED` for a purchase a budget refuses. `payments.execute()` then
throws status 409 and code `SHARED_BUDGET_BLOCKED`. With `autoExecute`, the request stays `APPROVED`
and nothing is thrown. To see why, call `preview()` with the same `taskRequestId` and `purchaseKey`.

`context.purchaseKey` identifies one purchase. When `context.invoice` has `vendorId` and `id`, Conto
builds the key from those two values and ignores `purchaseKey`. A key cannot be used again, so a second
payment of that invoice is refused whatever `purchaseKey` it names. `preview()` takes no invoice, so it
cannot show this refusal. Use a new key for a new attempt. The exception is a managed wallet payment
that Conto saw go nowhere: its transfer reverted onchain and the chain has finalized that revert, or
it stopped before Conto sent it and was then declined, cancelled or expired. It also can't have been
flagged for reconciliation. Then a new payment can use the key, and the earlier purchase still shows
it. Until a revert is final, a payment that uses the key is refused like any second payment. That
takes about a minute on Tempo, Polygon and Solana, about 20 minutes on Ethereum, and about 25 minutes
on Base and Arbitrum. If the chain can't show the revert final, the key stays taken. For a linked
buyer, the purchase in the owner workspace has to qualify too. A key can't start with `released:` or
`linked:`.

## Tasks

When a budget requires a task, the agent asks for one and the budget holder decides.

```typescript
const task = await conto.spending.requestTask({
  purpose: 'Renew design tool licenses',
  amount: '200',
  currency: 'USDC',
  expiresAt: new Date(Date.now() + 7 * 86_400_000).toISOString(),
});

// The budget holder, with an organization API key.
await admin.spending.decideTask(task.id, { decision: 'APPROVE' });
```

Once the task is approved, pass its id as `context.taskRequestId` on `payments.request()` from a
managed wallet, or as `taskRequestId` on [`connectedCards.draw()`](https://conto.finance/docs/sdk/connected-cards). A buyer
linked to an owner workspace cannot use a task. A purchase that names one is refused with
`LINKED_TASK_UNSUPPORTED`, so a linked buyer cannot spend from an owner budget that requires a task. A self-custody wallet cannot use a task. When a budget covers the agent, `payments.request()` throws status 409 with `reasonCode` `EXECUTION_PATH_NOT_CONNECTED`. When none does, a request that names a task ends in `DENIED`. `payments.approve()` refuses the task fields. A draw a budget refuses keeps its single-purchase authorization, unless another purchase takes the last of the budget at that same moment.

`requestTask()` also takes `permittedMerchants` and `permittedSourceIds`, up to 100 each. A purchase outside them fails with `MERCHANT_NOT_AUTHORIZED` or `SOURCE_NOT_AUTHORIZED`. `decideTask()` also takes `amount` to approve a different amount, and `participantIds` to let other agents draw on the task. The list must include the agent that asked, or the call fails with `INVALID_PARTICIPANTS`.

## Purchases and evidence

`purchases()` lists this agent's purchases against budgets in its own organization, not Commerce
`conto.purchases`. It is empty when no budget has applied to this agent's purchases. A buyer linked to an owner
workspace has no row for a purchase that only an owner budget covered. The owner reads those with
`admin.spending.purchases()`.

`attachEvidence()` takes a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the
Blob's `type`, for example `new Blob([bytes], { type: 'application/pdf' })`. A Blob with no type, or
with a type that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`. Conto
does not take the amount, currency or merchant from the file. The budget holder enters them with
`reviewEvidence()`.

`exportPurchases()` returns CSV bytes and fails with `reasonCode` `EXPORT_TOO_LARGE` above 10,000
purchases.

An uncertain outcome keeps the spend held until someone records what happened. An Owner can do this in **Budgets**. An organization API key with `spending:reconcile` can do it with `reconcile()`.

Recording the outcome of a connected card draw moves its purchase too. A charge settles it at the charged amount, and a draw that was not charged releases the hold. A store cancelling a reported order refunds the purchase, or releases it if it never settled. A settlement already recorded on the purchase refuses an outcome that contradicts it.

For a linked buyer, the purchase in the owner workspace waits for an Owner of that workspace, for card draws and checkouts alike. A buyer's record, or anyone else's, that nothing was charged marks it `UNKNOWN` and keeps the hold. A recorded charge settles it, and any difference from the hold stays held. Until an Owner records what happened in **Budgets**, the hold counts against the budget and stops `deleteBudget()` and reconnecting that buyer. Conto releases the hold itself only when it saw that nothing could be charged, such as the checkout provider refusing the card, or a checkout closing before Conto cleared it to pay and before Stripe Link was asked for a card.

A record that leaves the owner's budget short raises a `LINKED_BUDGET_NEEDS_OWNER` alert in the owner workspace, including a store cancelling a purchase the owner has as settled. Owners and the billing address get it by email, under the workspace's alert email settings. It closes when a later record leaves nothing held, such as an Owner recording the outcome in **Budgets**.

```typescript
await admin.spending.reconcile(purchase.id, {
  eventKey: 'order-1042-check',
  kind: 'RELEASED',
  evidenceRef: 'Merchant confirmed the order was not charged',
});
```

`kind` is `RELEASED`, `SETTLED`, `REFUNDED` or `UNKNOWN`. `SETTLED` needs `settledAmount` and `REFUNDED` needs `refundedAmount`, each the verified total so far. Set `final: true` on either one to release the rest of the hold. `RELEASED` always releases it. `evidenceRef` has at least 10 characters. `eventKey` has at least 8 characters, and sending it again with different values fails with `reasonCode` `EVIDENCE_CONFLICT`. A purchase with settled spend cannot be `RELEASED` and fails with `SETTLED_PURCHASE`, so record a refund instead.

## Errors

Most budget errors have `code` `POLICY_VIOLATION`, so read `reasonCode` on `ContoError` and
`ContoAdminError`. Some also have `nextActions`.

| `reasonCode`                         | Meaning                                                            |
| ------------------------------------ | ------------------------------------------------------------------ |
| `SHARED_BUDGET_EXCEEDED`             | The budget cannot cover the purchase. `nextActions` has `REQUEST_BUDGET_INCREASE`, which means ask the budget holder to raise the amount with `updateBudget()`. A budget request does not raise it |
| `TASK_REQUIRED`                      | A budget requires an approved task and the purchase named none     |
| `BUDGET_CURRENCY_UNSUPPORTED`        | A level has budgets for the agent and none covers the currency     |
| `EXECUTION_PATH_NOT_CONNECTED`       | A budget stops this payment path for the agent                     |
| `LINKED_BUDGET_REQUIRED`             | A linked buyer has no budget in the owner workspace                |
| `BUDGET_HAS_HOLDS`                   | A purchase holds spend against the budget, so it cannot be deleted |
| `TASK_BUDGET`                        | A task's budget ends by closing the task                           |
| `STALE_VERSION`                      | `version` is out of date. Read the budget and try again            |
| `BUDGET_PAUSED`                      | A budget that covers the purchase is paused                        |
| `BUDGET_EXPIRED`                     | The purchase is after a budget's end date                          |
| `MEMBER_LIMIT_EXCEEDED`              | The purchase is over what is left for this agent or team on the budget |
| `AGENT_LIMIT_EXCEEDED`               | The purchase is over the budget's limit per agent                  |
| `BUDGET_METHOD_NOT_ALLOWED`          | The budget does not allow this payment method                      |
| `BUDGET_SOURCE_NOT_ATTACHED`         | This card or wallet is not on a budget this agent can use          |
| `CATEGORY_NOT_AUTHORIZED`            | The budget does not allow this category                            |
| `CATEGORY_UNKNOWN`                   | The budget has a category list and the purchase has no category    |
| `BUDGET_CHANGE_NOT_ALLOWED`          | Only an Owner or Admin can make this change                        |
| `INVALID_BUDGET`                     | The budget's fields do not fit together. The message says what to fix |
| `TASK_AUTHORITY_UNAVAILABLE`         | The task is unapproved, expired, closed, or unavailable to this agent |
| `MERCHANT_NOT_AUTHORIZED`            | The budget or task does not allow this merchant                    |
| `SOURCE_NOT_AUTHORIZED`              | The task does not allow this wallet or card                        |
| `DUPLICATE_PURCHASE`                 | `context.purchaseKey` was already used                             |
| `SOURCE_UNAVAILABLE`                 | `preview()` got a `sourceId` that is not a managed wallet linked to the agent |
| `SOURCE_CURRENCY_MISMATCH`           | `preview()` got a `currency` that is not that wallet's stablecoin  |
| `BUYER_LINK_REVOKED`                 | A linked buyer's connection to the owner workspace was revoked     |
| `LINKED_TASK_UNSUPPORTED`            | A linked buyer cannot pay against a task                           |

`reasonCode` can have other values, so handle one you do not recognize.

These calls report a budget differently:

- A refused `checkout.begin()`, `getLinkCard()` or `replaceLinkCard()` closes the checkpoint, so a
  retry with the same `idempotencyKey` throws `CheckoutCheckpointClosedError`. Use a new key. SDK 0.8.2
  and later return each `reasonCode` above on these calls.
- `payments.execute()` and `payments.pay()` have `code` `SHARED_BUDGET_BLOCKED` and no `reasonCode`.
- `connectedCards.draw()` returns `DENIED` with a code in `reasonCodes`. See
  [connected cards](https://conto.finance/docs/sdk/connected-cards).

A `VALIDATION_ERROR` on a spending call usually lists the fields in `fieldErrors`. It does not when
the call got a field it does not accept, such as `periodStart` or `currency` on `updateBudget()`. A
`payments.request()` validation failure never has `fieldErrors`.

## Method reference

`tasks()` and `purchases()` page with `nextCursor`.

### `conto.spending`

Takes an agent SDK key.

| Method                                       | What it does                                                  | Scope               |
| -------------------------------------------- | ------------------------------------------------------------- | ------------------- |
| `budgets()`                                  | The budgets that cover the agent, with held, settled and available amounts | `policies:read`     |
| `preview(input)`                             | Check a purchase against shared budgets                       | `payments:request`  |
| `tasks(cursor?)`                             | List the agent's tasks                                        | `policies:read`     |
| `requestTask(input)`                         | Ask the budget holder to approve a task                       | `payments:request`  |
| `purchases(cursor?)`                         | List the agent's purchases against budgets                    | `transactions:read` |
| `purchase(purchaseId)`                       | Read one purchase with its events and evidence                | `transactions:read` |
| `exportPurchases()`                          | Download the agent's purchases as CSV bytes                   | `transactions:read` |
| `attachEvidence(purchaseId, file, filename)` | Attach a file to a purchase                                   | `payments:request`  |
| `downloadEvidence(purchaseId, evidenceId)`   | Download an attached file as bytes                            | `transactions:read` |

`budgets()` also returns `buyerConnection` (`{ revoked, participantId }`) when the agent is a shopping
buyer linked to an owner's workspace. Linking happens in the dashboard and the buyer's chat, not in
the SDK.

### `admin.spending`

Takes an organization API key. An admin SDK key cannot change budgets, decide tasks, or reconcile.

| Method                                       | What it does                                     | Scope                |
| -------------------------------------------- | ------------------------------------------------ | -------------------- |
| `budgets()`                                  | List the organization's budgets                  | `policies:read`      |
| `createBudget(input)`                        | Create a budget                                  | `policies:write`     |
| `updateBudget(budgetId, input)`              | Change the amount, active state or end date      | `policies:write`     |
| `deleteBudget(budgetId, { version })`        | End a budget                                     | `policies:write`     |
| `tasks(cursor?)`                             | List tasks                                       | `policies:read`      |
| `decideTask(taskId, input)`                  | Approve, reject or close a task                  | `policies:write`     |
| `purchases(cursor?)`                         | List purchases against budgets                   | `transactions:read`  |
| `purchase(purchaseId)`                       | Read one purchase with its events and evidence   | `transactions:read`  |
| `exportPurchases()`                          | Download purchases as CSV bytes                  | `transactions:read`  |
| `reconcile(purchaseId, input)`               | Record what happened to a purchase               | `spending:reconcile` |
| `accounting(purchaseId, input)`              | Set the department, project and accounting code  | `transactions:write` |
| `attachEvidence(purchaseId, file, filename)` | Attach a file to a purchase                      | `transactions:write` |
| `reviewEvidence(purchaseId, evidenceId, input)` | Record the amount, currency and merchant on a file | `transactions:write` |
| `downloadEvidence(purchaseId, evidenceId)`   | Download an attached file as bytes               | `transactions:read`  |

The **Standard** preset for organization API keys does not include `spending:reconcile`. Use the **Admin** preset, which only an Owner can create, or name the scope when you create the key with `POST /api/api-keys`.
