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

# Spend, balance and errors

> Check the balance, report what a Rask API key spent, and handle errors by code: insufficient balance, changed price, spend caps and failed jobs.

## Check the balance

[getBalance](https://docs.api.rask.ai/operations/getBalance.md) reads the payer's pools. They belong to the workspace owner, and all workspaces of one owner share them.

```typescript TypeScript theme={null}
const { data: balance } = await rask.account.getBalance();
```

```python Python theme={null}
balance = client.account.get_balance()
```

```go Go theme={null}
balance, err := client.Account.GetBalance(ctx)
if err != nil {
	return err
}
```

```java Java theme={null}
Balance balance = rask.account().getBalance().data();
```

```php PHP theme={null}
$balance = $rask->account->getBalance();
```

```bash cURL theme={null}
curl https://api.rask.ai/v3/balance \
  -H "Authorization: Bearer $RASK_API_KEY"
```

| Field | Meaning |
| - | - |
| `minutes.remaining_minutes` | The main minute pool |
| `minutes.expiring` | The pool's minutes grouped by expiry, soonest first: `minutes`, `expires_at` (null when they do not expire by date), `expires_with_subscription` and `source` |
| `lipsync_free_minutes.remaining_minutes` | Free lip-sync minutes |
| `projects.unlimited`, `projects.remaining_projects` | Project credits; `remaining_projects` is null while unlimited |

For the price of one action, call its estimate rather than doing the sum: it reports `eligible`, `total_minutes`, `balance_after` and the key's `spend_limit`. Top-ups and plan changes happen only in
the web app, at the `manage_url` from [getPlan](https://docs.api.rask.ai/operations/getPlan.md).

## See what was spent

[getUsage](https://docs.api.rask.ai/operations/getUsage.md) groups charges and refunds by up to two of `day`, `month`, `feature`, `project`, `token` and `member`. `period` picks the `current` (default) or `previous` billing period (without a subscription: the last 30 days, or the 30 days
before); `from` and `to` pick a span of up to 400 days instead.

```typescript TypeScript theme={null}
const { data: usageBucketList } = await rask.usage.getUsage({
  query: { group_by: ['day', 'feature'], period: 'current' },
});
```

```python Python theme={null}
usage_bucket_list = client.usage.get_usage(group_by=["day", "feature"], period="current")
```

```go Go theme={null}
usageBucketList, err := client.Usage.GetUsage(ctx, &api.GetUsageParams{
	GroupBy: &[]api.UsageGroupBy{api.UsageGroupByDay, api.UsageGroupByFeature},
	Period:  rask.Ptr(api.UsagePeriodCurrent),
})
if err != nil {
	return err
}
```

```java Java theme={null}
UsageBucketList usageBucketList = rask.usage().getUsage(GetUsageParams.builder()
        .groupBy(List.of(UsageGroupBy.DAY, UsageGroupBy.FEATURE))
        .period(UsagePeriod.CURRENT)
        .build()).data();
```

```php PHP theme={null}
$usageBucketList = $rask->usage->getUsage(['group_by' => ['day', 'feature'], 'period' => 'current']);
```

```bash cURL theme={null}
curl "https://api.rask.ai/v3/usage?group_by=day,feature&period=current" \
  -H "Authorization: Bearer $RASK_API_KEY"
```

Each bucket has `charged_minutes`, `refunded_minutes`, `net_minutes` and `operations` (or the `_projects` fields). A refund counts in the bucket of the charge it reverses.

[listUsageEvents](https://docs.api.rask.ai/operations/listUsageEvents.md) lists single charges, newest first: one row per charge with its refund, its `feature`, `project_id`, `job_id` and, for v3 jobs, its `credential_id`, `member_user_id` and `channel`. It takes the
same filters and period as getUsage. Use it to reconcile a job.

```typescript TypeScript theme={null}
const { data: usageEventList } = await rask.usage.listUsageEvents({ query: { project_id: projectId } });
```

```python Python theme={null}
usage_event_list = client.usage.list_usage_events(project_id=project_id)
```

```go Go theme={null}
usageEventList, err := client.Usage.ListUsageEvents(ctx, &api.ListUsageEventsParams{ProjectId: rask.Ptr(projectID)})
if err != nil {
	return err
}
```

```java Java theme={null}
UsageEventList usageEventList = rask.usage().listUsageEvents(ListUsageEventsParams.builder()
        .projectId(projectId)
        .build()).data();
```

```php PHP theme={null}
$usageEventList = $rask->usage->listUsageEvents(['project_id' => $projectId]);
```

```bash cURL theme={null}
curl "https://api.rask.ai/v3/usage/events?project_id=$PROJECT_ID" \
  -H "Authorization: Bearer $RASK_API_KEY"
```

Key, member and channel come from v3 jobs. Charges without one, from the web app or API v2, report channel `unknown` and no key or member. Usage across the payer's other workspaces is only in the web app.

To see one key's month: for the calling key, `auth.spent_minutes_this_month` and `auth.spend_limit_minutes_per_month` in [getMe](https://docs.api.rask.ai/operations/getMe.md); for any key,
`credential_id` in the usage filters, with `from` at the start of the UTC month. The cap counts the UTC month and includes work
that is ordered but not charged yet (a pending auto-dub, the next stages of a localization run), so the two can differ.

## Read the cost of a job

`Job.cost` shows what a job holds. It is null on a job that never costs anything, such as an `export`.

| Field | Meaning |
| - | - |
| `credit_type` | `minutes`, `lipsync_free_minutes` or `projects` |
| `minutes` or `projects` | The amount; 0 when free. On a localization run, what its stages charged and did not refund so far |
| `charged` | True once the charge is committed |
| `refunded`, `refunded_at` | Read anew on every read of the job, so a late refund on a finished job shows here |

The rules are in [Prices and refunds](https://docs.api.rask.ai/v3/concepts/pricing.md).

## Handle errors

Every error is `application/problem+json`. Branch on `code`, follow `recommendation`, and retry as `retry` says. [Errors](https://docs.api.rask.ai/errors.md) lists every code.

| `retry` | What to do |
| - | - |
| `same_key` | Resend the same request with the same `Idempotency-Key`, after `Retry-After` when given |
| `new_key` | A failed job: start again with a new key |
| `none` | Do not retry unchanged |

| `recommendation` | What has to happen first |
| - | - |
| `fix_request` | Change the request or its inputs |
| `wait` | Work in progress blocks the request: wait for it to end, then retry |
| `top_up` | The workspace owner adds minutes or changes the plan in the web app |
| `retry_later` | A temporary failure: retry after a pause |
| `contact_support` | Rask has to act: contact support with the `instance` value |

### The codes a spending client meets

| Code | Status | Meaning | What to do |
| - | - | - | - |
| [`insufficient_balance`](https://docs.api.rask.ai/errors/insufficient_balance) | 402 | The pool in `credit_type` is short: `required_minutes` against `balance_minutes` (`required_projects` against `balance_projects` for project credits) | Stop and tell the user. The owner tops up in the web app |
| [`cost_changed`](https://docs.api.rask.ai/errors/cost_changed) | 409 | `current_cost_minutes` is above your `expected_cost_minutes`. Nothing was charged | Estimate again, confirm the new price, resend with a new cap |
| [`token_spend_limit_exceeded`](https://docs.api.rask.ai/errors/token_spend_limit_exceeded) | 403 | The request would pass the key's monthly cap in `credit_type` | Wait until `resets_at`, or raise the cap in Settings -> API keys |
| [`too_many_concurrent_jobs`](https://docs.api.rask.ai/errors/too_many_concurrent_jobs) | 429 | Too many active jobs of `job_type` started through the API: the payer's, or across Rask (`policy`, `limit`) | Wait `Retry-After`, then retry with the same key |
| [`spending_paused`](https://docs.api.rask.ai/errors/spending_paused) | 503 | Spending is paused for now; reads still work | Retry after `Retry-After` with the same key |
| [`idempotency_key_reused`](https://docs.api.rask.ai/errors/idempotency_key_reused) | 422 | The key was used with a different request | Use a new key |

### Failures inside a job

A job that fails answers `200` to `getJob`, with `status: failed` and a problem of the same shape in `error`. `instance` is then `/v3/jobs/{id}`, and `retry` is `new_key` or `none`, so a new attempt needs a new key. Dub and generation failures
also carry `cause_code`, and its class sets `recommendation`:

| `cause_code` | `recommendation` |
| - | - |
| `segment_changed` | `retry_later` |
| `insufficient_speech` | `fix_request` |
| `voice_configuration` | `fix_request` |
| `missing_translation` | `fix_request` |
| `tts_provider_failed` | `retry_later` |
| `merge_failed` | `retry_later` |
| `unknown` | `retry_later`, or `contact_support` when the project's previous job of that type also failed with `unknown` |

## The whole program

```python spend_report.py theme={null}
"""Print the balance and this billing period's usage, then dub the project and act on a refusal: python spend_report.py PROJECT_ID

The dub spends at most the minutes its estimate showed on the first run.

Its keys are saved in spend-report-PROJECT_ID.json: run it again within 24 hours, and no paid work starts twice. Delete the file to
start anew.
"""
import sys

from rask_client import Progress, RaskProblem, call, wait_for_job

project_id = sys.argv[1]
progress = Progress(f"spend-report-{project_id}.json")

balance = call("GET", "/balance")
print("Minutes left:", balance["minutes"]["remaining_minutes"])
for grant in balance["minutes"]["expiring"]:
    ends = grant["expires_at"] or ("with the subscription" if grant["expires_with_subscription"] else "never")
    print(" ", grant["minutes"], "from", grant["source"], "expire:", ends)

for bucket in call("GET", "/usage", params={"group_by": "feature", "period": "current"})["data"]:
    if bucket["credit_type"] != "projects":
        print(bucket["key"]["feature"], "net minutes:", bucket["net_minutes"])

# The price of the first run caps every run: a second run sends the same request, so a dub that started is replayed.
max_minutes = progress.once(
    "max_minutes", lambda: call("POST", f"/projects/{project_id}/dub/estimate", {})["total_minutes"]
)
try:
    job = call(
        "POST",
        f"/projects/{project_id}/dub",
        {"expected_cost_minutes": max_minutes},
        key=progress.key("dub"),
    )
    job = wait_for_job(job["id"], check=False)
    if job["status"] == "failed":
        print("Failed:", job["error"]["code"], job["error"].get("cause_code"), job["error"]["recommendation"])
    print("Refunded:", job["cost"]["refunded"])
except RaskProblem as refused:
    problem = refused.problem
    if problem["code"] == "insufficient_balance":
        print(f"Short of minutes: {problem['required_minutes']} needed, {problem['balance_minutes']} left.")
        print("The workspace owner tops up at", call("GET", "/plan")["manage_url"])
    elif problem["code"] == "cost_changed":
        print(f"The price is now {problem['current_cost_minutes']} minutes.")
        print("To accept it, delete", progress.path, "and run again.")
    elif problem["code"] == "token_spend_limit_exceeded":
        print("This key's monthly cap is reached until", problem["resets_at"])
    else:
        print(problem["code"], problem["hint"], "Quote", problem["instance"], "to support if it persists.")
```


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