Seat-based credits

How to grant credits that scale with the number of seats a customer bought, and how to see which customer’s 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:

sourceQuantityWhere it is allowed
FIXEDExactly what you configuredAny pricing item. This is the default, and what every existing rule does
SEAT_POOLConfigured quantity × the purchased seat countOnly on a PER_SEAT pricing item
PER_SEATDeclared, not implementedRejected with PER_SEAT is not supported yet

SEAT_POOL grants the multiplied quantity into the customer’s shared wallet as one grant per rule per billing period, never one grant per seat. 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.

Credits owned by an individual seat, where an idle seat’s credits cannot be spent by anyone else, are a separate model. That is what PER_SEAT is reserved for; until it ships, a seat-scaled grant is always a shared pool.

Where the seat count comes from

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 seat-scaled 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
1{
2 "wallet_grants": [
3 {
4 "wallet_type_id": "wtyp_...",
5 "source": "SEAT_POOL",
6 "credits_grant": {
7 "credits": { "quantity": "500", "credit_type_id": "crty_..." }
8 },
9 "expiry_policy": {
10 "expiry_period": { "type": "MONTH", "value": 1 }
11 }
12 }
13 ]
14}

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.

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:

Wallet balance with the reserved balance split per customer
$curl -X POST https://test.api.solvimon.com/v1/wallets/wall_TwDeeN0tYSY3F7BkeN1v/balance \
> -H "X-API-KEY: <apiKey>" \
> -H "Content-Type: application/json" \
> -d '{"include_customer_details": true}'
Response
1{
2 "balance": { "credits": { "quantity": "6000", "credit_type_id": "crty_..." } },
3 "reserved_balance": { "credits": { "quantity": "1750", "credit_type_id": "crty_..." } },
4 "open_balance": { "credits": { "quantity": "4250", "credit_type_id": "crty_..." } },
5 "balance_at": "2026-09-07T10:00:00Z",
6 "customer_details": [
7 {
8 "customer_id": "cust_MwDR9l0vsgwcV6CRqw14",
9 "reserved_balance": { "credits": { "quantity": "1200", "credit_type_id": "crty_..." } }
10 },
11 {
12 "customer_id": "cust_qwDRHT0vsJoJuFCkPm1A",
13 "reserved_balance": { "credits": { "quantity": "550", "credit_type_id": "crty_..." } }
14 }
15 ]
16}

The same 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.
  • 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.
  • Seat assignments are not the seat count. The count that scales the grant is what the customer paid for, seats_values. Which children occupy those seats is recorded separately as seat assignments, and over-assignment does not increase the credits granted.