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

# Seat-based credits

How to grant credits that scale with the number of seats a customer bought, whether they are pooled across the customer or owned by the individual seat, and how to see whose usage reserved which part of a shared pool.

---

## Why this matters

A plan sold per seat often includes credits per seat: every editor comes with 500 AI credits, every workstation with 10 GB of processing. The grant is not a property of the customer, it is a property of the seat count, and that count changes whenever the customer adds or removes people.

Expressing that with a fixed grant means editing the credit quantity on the pricing every time the seat count moves, per customer, which is exactly the kind of manual step that produces a customer who was charged for twelve seats and given credits for eight.

A seat-scaled grant derives the quantity from the seat count the seat fee was already priced with, so the two can never disagree.

## How it works

A wallet-grant rule on a pricing item config carries a `source` field that declares what its quantity is derived from:

| `source`    | Quantity                                          | Where it is allowed                                                      |
| ----------- | ------------------------------------------------- | ------------------------------------------------------------------------ |
| `FIXED`     | Exactly what you configured                       | Any pricing item. This is the default, and what every existing rule does |
| `SEAT_POOL` | Configured quantity × the purchased seat count    | Only on a `PER_SEAT` pricing item                                        |
| `PER_SEAT`  | Configured quantity, prorated per seat assignment | Only on a `PER_SEAT` pricing item                                        |

`SEAT_POOL` grants the multiplied quantity into the customer's shared wallet as **one grant per rule per pricing period**, never one grant per seat. Where the pricing period is shorter than the billing period, one invoice carries a grant for each period it covers; see [when the grant is created](/platform-guides/wallets-credits/credits-in-pricing-plans#when-the-grant-is-created). Scaling multiplies the quantity on a single grant; it does not fan out into a row per person. Any seat can spend any credit in the resulting pool.

```mermaid
flowchart LR
    A["Per-seat pricing item<br />12 seats"] --> B["Wallet grant rule<br />500 credits, SEAT_POOL"]
    B --> C["One grant of 6,000 credits<br />into the customer's wallet"]
```

Credits owned by an individual seat, where an idle seat's credits cannot be spent by anyone else, are a different model with the same starting point. That is `PER_SEAT`, described in [Credits owned by one seat](#credits-owned-by-one-seat).

### Where the seat count comes from

This is how `SEAT_POOL` finds its multiplier. `PER_SEAT` uses no seat count at all: it grants against the seat assignments themselves, so the two sections below apply to a pooled grant only.

The seat count is resolved per pricing item, in this order:

1. **The charge data on the pricing item summary.** This is the authoritative source: it is the very seat count the seat fee was priced with for that sub-period, including any mid-period split.
2. **`seats_values` on the pricing plan schedule**, for that pricing item config, where the summary carries no charge data.
3. **`default_seats_value` on the pricing item config**, as the last fallback.

Because the first source is the priced seat count rather than a separately configured number, the credits a customer receives always match the seats they were billed for, including a period in which the count changed.

### Seats added mid-period

Adding seats part-way through a billing period pro-rates the seat fee. The credits follow: a top-up grant is issued for the added seats, using the same day-count ratio the fee used, so a customer who doubles their seats halfway through a month receives half a period's credits for the new ones.

Nothing is issued when seats are added exactly on cycle, because the period's main grant is already calculated with the new count, and nothing is issued when the seat count goes down. Credits already granted are not clawed back; the next period is simply granted at the lower count.

## Configure a pooled grant

Set `source` to `SEAT_POOL` on the wallet grant of a per-seat pricing item's config, on a pricing plan version or directly on a schedule. The rest of the rule, the wallet type, the quantity and the expiry policy, is unchanged.

**`Pricing item config granting 500 credits per seat per period`**

```json Pricing item config granting 500 credits per seat per period
{
  "wallet_grants": [
    {
      "wallet_type_id": "wtyp_...",
      "source": "SEAT_POOL",
      "credits_grant": {
        "credits": { "quantity": "500", "credit_type_id": "ctyp_..." }
      },
      "expiry_policy": {
        "expiry_period": { "type": "MONTH", "value": 1 }
      }
    }
  ]
}
```

On a per-seat item with twelve purchased seats, that rule grants 6,000 credits for the period.

Issued grants carry the same `source`, read-only, so a grant that was seat-scaled stays identifiable after the fact rather than looking like a fixed grant of an unexplained size.

Validation on `wallet_grants.source` rejects three things:

* **`SEAT_POOL` on anything but a `PER_SEAT` pricing item.** Seat counts are keyed by pricing item, not by plan, so there is no count to resolve for another model type. The error names the model type it found.
* **`SEAT_POOL` combined with `credits_grant.conversion`.** A conversion already derives the grant from the paid amount, which on a per-seat item is seats × unit price, so combining the two would apply the seat count twice.
* **`SEAT_POOL` combined with `amount_grant.percentage`**, for the same reason.

### Via Desk

The grant source is set on the wallet grant inside a pricing, next to the credit quantity. A seat-scaled grant shows the seat count it resolved against, so the pricing screen states what the customer will actually receive rather than the per-seat multiplier alone.

## Credits owned by one seat

`SEAT_POOL` answers "how many credits does this customer get". `PER_SEAT` answers "how many does this person get". Every [seat assignment](/platform-guides/customers-and-billing/customer/seat-assignments) active during the period receives a grant of its own. Only the child customer occupying that seat can spend it, so an idle seat's credits stay idle rather than being absorbed by a colleague.

```mermaid
flowchart LR
    A["Per-seat pricing item<br />3 seats assigned"] --> B["Wallet grant rule<br />500 credits, PER_SEAT"]
    B --> C["500 credits<br />held for seat A"]
    B --> D["500 credits<br />held for seat B"]
    B --> E["250 credits<br />seat C, assigned mid-period"]
```

A restricted grant is only eligible when the usage being priced belongs to the customer that holds it. Grants from a `FIXED` or `SEAT_POOL` rule carry no restriction and stay available to everyone. Usage that cannot be attributed to a seat draws on unrestricted grants only. A personal allowance and a shared pool can therefore live in the same wallet: a seat spends its own grant and the shared one, and nobody else can reach what it holds.

### Proration

The quantity is prorated by how much of the period the assignment covers, using the same ratio a mid-period seat fee uses. A seat assigned halfway through a monthly period is granted half the configured quantity. On a handover, the outgoing occupant keeps the part of the allowance their time covered, and the incoming one is granted the remainder.

### Over-granting

Credits follow the assignments, and assignments are not checked against the seats the customer paid for. A period with a handover, or with more children assigned than seats bought, therefore grants more than the paid seats account for. That excess is reported rather than withheld. Capping it would make one seat's allowance depend on what another seat did, which is the behaviour `PER_SEAT` exists to avoid.

The excess is judged at issuance and read back in three places:

| Where                               | Field                                                |
| ----------------------------------- | ---------------------------------------------------- |
| On the wallet grant                 | `over_granted`                                       |
| On the wallet balance status        | `over_granted`, for the wallet as a whole            |
| On each entry of `customer_details` | `over_granted`, beside that customer's own `balance` |

> **Warning**
>
> `over_granted` is a reporting figure, not a block. The credits are spendable. Watch it if seats are reassigned often: a seat type handed around within one period can grant a multiple of what was sold.

### Configure a seat-owned grant

Set `source` to `PER_SEAT` on the wallet grant of a per-seat pricing item's config:

**`Pricing item config granting 500 credits to each occupied seat`**

```json Pricing item config granting 500 credits to each occupied seat
{
  "wallet_grants": [
    {
      "wallet_type_id": "wtyp_5RmXe2QbtKfYuA9DcNH3P",
      "source": "PER_SEAT",
      "credits_grant": {
        "credits": { "quantity": "500", "credit_type_id": "ctyp_2PnBx7VdtRgLmE4WcQJ8K" }
      }
    }
  ]
}
```

Each grant it issues names the seat it belongs to:

**`A grant issued to a seat`**

```json A grant issued to a seat
{
  "object_type": "WALLET_GRANT",
  "id": "wgra_7KdRa4NvpWzYbT6FgXH2M",
  "source": "PER_SEAT",
  "seat": {
    "customer_id": "cust_5RmXe2QbtKfYuA9DcNH3P",
    "product_item_id": "proi_2PnBx7VdtRgLmE4WcQJ8K"
  },
  "grant_credits": { "quantity": "250", "credit_type_id": "ctyp_7KdRa4NvpWzYbT6FgXH2M" },
  "over_granted": { "credits": { "quantity": "50", "credit_type_id": "ctyp_7KdRa4NvpWzYbT6FgXH2M" } }
}
```

`seat.customer_id` is the child customer occupying the seat and `seat.product_item_id` is the per-seat product item that seat is held on, which is the seat type. Both are read-only, and neither is set on a grant from any other source.

Validation on `wallet_grants.source` adds two rules for `PER_SEAT`:

* **A seat-owned grant has to be a fixed credits grant.** An amount grant cannot be attributed to a single seat. `PER_SEAT` therefore requires `credits_grant.credits`, and is otherwise rejected with `PER_SEAT requires a fixed credits_grant.credits; a seat-owned amount grant is not supported`. As with `SEAT_POOL`, the pricing item has to be priced `PER_SEAT`.
* **One rule per wallet type.** A wallet type granted from `PER_SEAT` carries no second grant rule on the same wallet type in that config. Two rules resolve to the same grant, so the second would be dropped silently at issuance. The config is rejected instead.

## See whose usage reserved the balance

A pool spent by several children reports one reserved figure, which answers "how much is committed" but not "by whom". Pass `include_customer_details` on a balance call to get the reserved balance split per customer:

\<Endpoin### Request

POST [https://test.api.solvimon.com/v\{version}/wallets/\{resourceIdOrReference}/balance](https://test.api.solvimon.com/v\{version}/wallets/\{resourceIdOrReference}/balance)

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```curl Wallets_postWalletsByResourceIdOrReferenceBalance_example
curl -X POST https://test.api.solvimon.com/v1/wallets//balance \
     -H "X-API-KEY: <apiKey>" \
     -H "Content-Type: application/json" \
     -d '{
  "balance_at": "2026-10-09T12:00:00Z"
}'
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```python Wallets_postWalletsByResourceIdOrReferenceBalance_example
import requests

url = "https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance"

payload = { "balance_at": "2026-10-09T12:00:00Z" }
headers = {
    "X-API-KEY": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```javascript Wallets_postWalletsByResourceIdOrReferenceBalance_example
const url = 'https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance';
const options = {
  method: 'POST',
  headers: {'X-API-KEY': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"balance_at":"2026-10-09T12:00:00Z"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```go Wallets_postWalletsByResourceIdOrReferenceBalance_example
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance"

	payload := strings.NewReader("{\n  \"balance_at\": \"2026-10-09T12:00:00Z\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-KEY", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```ruby Wallets_postWalletsByResourceIdOrReferenceBalance_example
require 'uri'
require 'net/http'

url = URI("https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"balance_at\": \"2026-10-09T12:00:00Z\"\n}"

response = http.request(request)
puts response.read_body
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```java Wallets_postWalletsByResourceIdOrReferenceBalance_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance")
  .header("X-API-KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"balance_at\": \"2026-10-09T12:00:00Z\"\n}")
  .asString();
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```php Wallets_postWalletsByResourceIdOrReferenceBalance_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance', [
  'body' => '{
  "balance_at": "2026-10-09T12:00:00Z"
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

```csharp Wallets_postWalletsByResourceIdOrReferenceBalance_example
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"balance_at\": \"2026-10-09T12:00:00Z\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`Wallets_postWalletsByResourceIdOrReferenceBalance_example`**

````swift Wallets_postWalletsByResourceIdOrReferenceBalance_example
import Foundation

let headers = [
  "X-API-KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = ["balance_at": "2026-10-09T12:00:00Z"] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/wallets/:resourceIdOrReference/balance")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: \{ (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```ame flag works on the customer-level call, `POST /v1/customers/{id}/wallets/balance`, which returns the breakdown for each of that customer's wallets. It defaults to `false`, so existing integrations are unaffected.

The split is read from what was recorded on the credit invoice lines when the invoice was built, so it reflects usage that has actually been drafted against the wallet, not a live estimate.

## Edge cases

- **The breakdown only covers credit wallets.** A wallet denominated in an amount rather than a credit type returns an empty `customer_details`, because the attribution is recorded per credit type on the invoice line.
- **Only reserved usage is attributed.** A reservation exists while the invoice that consumes the credits is still being built. Once that invoice goes `FINAL` the credits are spent rather than reserved, and the invoice drops out of the breakdown. The breakdown answers "who is about to spend this", not "who has spent this".
- **A grant of `0` is rejected.** A wallet grant rule with a zero quantity fails validation at configuration time, rather than failing later while the grant is being issued.
- **`default_seats_value` has to be a number.** A seats value is a count, so configuring it as an amount is rejected with `default_seats_value: seats value must be a number`. A configured count of zero bills no seat line at all.
- **An unresolvable seat count grants nothing.** If the summary has no charge data, the schedule has no `seats_values` for the item and the config has no `default_seats_value`, there is no count to multiply by and no grant is issued. Set a `default_seats_value` on the pricing item config if you want a floor.
- **For a pooled grant, seat assignments are not the seat count.** The count that scales a `SEAT_POOL` grant is what the customer paid for, `seats_values`. Which children occupy those seats is recorded separately as [seat assignments](/platform-guides/customers-and-billing/customer/seat-assignments), and over-assignment does not increase the credits granted. A `PER_SEAT` grant is the opposite: it is driven by the assignments, and over-assignment does increase what is granted.
````