> 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.

# Credit revenue & breakage

How cash received for prepaid credits becomes revenue as those credits are consumed, and how to account for credits that expire unused.

---

## Why this matters

Selling credits separates the cash from the service. A customer who buys 10,000 credits has paid you today for a service you have not delivered yet, so the payment is not revenue: it is a liability you settle by letting them consume credits. Getting this wrong overstates revenue in the month of the sale and understates it for every month after.

Some of those credits are never consumed. The portion that expires unused is **breakage**, and it becomes revenue at the moment the obligation to deliver disappears.

## How it works

A credit sale moves through three states:

```mermaid
stateDiagram-v2
    [*] --> Deferred: top-up invoiced and paid
    Deferred --> Recognised: credits consumed
    Deferred --> Breakage: credits expire unused
    Recognised --> [*]
    Breakage --> [*]
```

| Stage      | Trigger                                                                              | Accounting                                                                      |
| ---------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| Deferred   | The top-up invoice is issued. `paid_amount` on the wallet grant is the cash received | Credit deferred revenue, debit cash or receivable                               |
| Recognised | The customer consumes credits from the grant                                         | Debit deferred revenue, credit revenue, in proportion to the credits consumed   |
| Breakage   | The grant reaches `expire_at` with credits remaining                                 | Debit deferred revenue, credit revenue, for the value of the unconsumed credits |

The value of a consumed credit is the grant's `paid_amount` divided by its granted credits. A 10,000-credit grant bought for \$1,000 recognises \$0.10 of revenue per credit consumed. Free and promotional grants carry a `paid_amount` of zero and therefore recognise no revenue when consumed; they reduce the credits available to charge against, not the deferred balance.

> **Note**
>
> Consumption is priced in credits, not currency, so a credit-priced usage line shows as "Not billed" on the invoice. The revenue for that consumption comes from releasing the deferred balance on the wallet grant, not from an invoice line.

## Getting the numbers

Solvimon does not expose a deferred-credit balance or a breakage figure as its own object. You assemble both from two sources:

* **The revenue recognition report** for recognised and deferred revenue per month. See [Revenue Recognition](/platform-guides/analytics-reporting/revenue-recognition).
* **Wallet grants and balances** for the credits side: `GET /v{version}/wallet-grants` gives you `grant_credits`, `paid_amount`, `start_at` and `expire_at` per grant, and the wallet balance endpoint gives you what remains.

### Request

GET [https://test.api.solvimon.com/v\{version}/wallet-grants](https://test.api.solvimon.com/v\{version}/wallet-grants)

```curl
curl https://test.api.solvimon.com/v1/wallet-grants \
     -H "X-API-KEY: <apiKey>" \
     -H "Content-Type: application/json"
```

```python
import requests

url = "https://test.api.solvimon.com/v1/wallet-grants"

payload = {}
headers = {
    "X-API-KEY": "<apiKey>",
    "Content-Type": "application/json"
}

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

print(response.json())
```

```javascript
const url = 'https://test.api.solvimon.com/v1/wallet-grants';
const options = {
  method: 'GET',
  headers: {'X-API-KEY': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{}'
};

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

```go
package main

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

func main() {

	url := "https://test.api.solvimon.com/v1/wallet-grants"

	payload := strings.NewReader("{}")

	req, _ := http.NewRequest("GET", 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))

}
```

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

url = URI("https://test.api.solvimon.com/v1/wallet-grants")

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

request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{}"

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

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

HttpResponse<String> response = Unirest.get("https://test.api.solvimon.com/v1/wallet-grants")
  .header("X-API-KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

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

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://test.api.solvimon.com/v1/wallet-grants', [
  'body' => '{}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-KEY' => '<apiKey>',
  ],
]);

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

```csharp
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/wallet-grants");
var request = new RestRequest(Method.GET);
request.AddHeader("X-API-KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "X-API-KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/wallet-grants")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
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()
```

### Response (200)

```json
{
  "data": [
    {
      "object_type": "wallet_grant",
      "id": "wg_8f3b2c1a-4d5e-11ec-81d3-0242ac130003",
      "customer_id": "cust_123e4567-e89b-12d3-a456-426614174000",
      "billing_entity_id": "be_987f6543-21ab-4cde-9f87-123456789abc",
      "wallet_id": "wallet_4567abcd-89ef-0123-4567-89abcdef0123",
      "type": "DEFAULT",
      "source": "FIXED",
      "seat": {
        "customer_id": "cust_123e4567-e89b-12d3-a456-426614174000",
        "product_item_id": "proditem_abc123ef-4567-89ab-cdef-0123456789ab"
      },
      "over_granted": {
        "credits": {
          "quantity": "15.5",
          "credit_type_id": "credittype_00112233-4455-6677-8899-aabbccddeeff",
          "credit_type": {
            "object_type": "credit_type",
            "id": "credittype_00112233-4455-6677-8899-aabbccddeeff",
            "reference": "CREDIT_STANDARD",
            "status": "ACTIVE",
            "name": "Standard Credit",
            "description": "Standard credits for usage",
            "unit_name": {
              "singular": "credit",
              "plural": "credits"
            },
            "created_at": "2023-11-01T10:00:00Z",
            "updated_at": "2024-04-01T12:00:00Z"
          }
        },
        "amount": {
          "quantity": "0.00",
          "currency": "USD"
        }
      },
      "description": "Monthly default wallet grant",
      "grant_amount": {
        "quantity": "100.00",
        "currency": "USD"
      },
      "grant_credits": {
        "quantity": "100",
        "credit_type_id": "credittype_00112233-4455-6677-8899-aabbccddeeff",
        "credit_type": {
          "object_type": "credit_type",
          "id": "credittype_00112233-4455-6677-8899-aabbccddeeff",
          "reference": "CREDIT_STANDARD",
          "status": "ACTIVE",
          "name": "Standard Credit",
          "description": "Standard credits for usage",
          "unit_name": {
            "singular": "credit",
            "plural": "credits"
          },
          "created_at": "2023-11-01T10:00:00Z",
          "updated_at": "2024-04-01T12:00:00Z"
        }
      },
      "paid_amount": {
        "quantity": "100.00",
        "currency": "USD"
      },
      "start_at": "2024-01-15T09:30:00Z",
      "expire_at": "2024-02-15T09:30:00Z",
      "created_at": "2024-01-10T08:00:00Z",
      "reverted_at": null,
      "revert_reason": null
    }
  ],
  "links": {
    "first": "https://api.solvimon.com/v1/wallet-grants?limit=25&page=1",
    "previous": null,
    "current": "https://api.solvimon.com/v1/wallet-grants?limit=25&page=1",
    "next": "https://api.solvimon.com/v1/wallet-grants?limit=25&page=2"
  },
  "limit": 25,
  "page": 1,
  "total_number_of_pages": 4
}
```

The deferred balance on a grant at any date is:

**`Deferred balance on a grant`**

```text Deferred balance on a grant
deferred = paid_amount x (credits remaining / grant_credits)
```

Because credits are consumed [earliest expiry first](/platform-guides/wallets-credits/credit-consumption-and-expiry), the credits remaining on a specific grant are not simply the wallet balance. Pull the grant list and the wallet balance for the same `balance_at` date and allocate in expiry order.

## Recognising breakage

Breakage recognition is driven by the grant's `expire_at`:

1. On the first day after `expire_at`, read the grant's remaining credits.
2. Value them at the grant's per-credit rate.
3. Post that amount out of deferred revenue and into revenue, dated to the expiry period.

Run this as part of month-end close alongside the recognition report, so the two together give a complete deferred revenue balance: invoiced-but-unearned subscription revenue from the report, plus unconsumed prepaid credits from the wallet grants.

> **Warning**
>
> Under IFRS 15 and ASC 606, breakage may be recognisable earlier than expiry when you can reliably estimate the proportion of credits that will never be consumed. Solvimon does not calculate an expected-breakage estimate; if your policy recognises breakage over the life of the grant rather than at expiry, that estimate is made in your ledger, from the grant data above.

## Edge cases

* **Grants that are extended** rather than allowed to expire keep their deferred balance. Nothing is recognised until credits are consumed or the new expiry passes.
* **Rollover top-ups** carry the balance into the next period, so no breakage arises at the period boundary. See the rollover section in [Credit consumption & expiry](/platform-guides/wallets-credits/credit-consumption-and-expiry).
* **Refunding an unused top-up** removes the liability rather than recognising it. Issue a credit note against the top-up invoice.

---