# API Credits



Credits meter public API and MCP consumption against the **workspace**, never against the individual credential or user. Every plan includes an allowance that refills on your billing anniversary.

The short version: a cheap read costs 1, a heavy read costs 5, a job dispatch or a very wide pull costs 25, and **an answer served from cache costs 0**.

## The four prices [#the-four-prices]

|  Price | Class                 | What it covers                                                                                   |
| -----: | --------------------- | ------------------------------------------------------------------------------------------------ |
|  **0** | Discovery             | Unauthenticated routes, and the small set of always-free routes below                            |
|  **1** | Read or write         | An ordinary read, or a configuration write                                                       |
|  **5** | Heavy read            | An analytical read over a time range, on the `heavy-query` [rate-limit bucket](/api/rate-limits) |
| **25** | Dispatch or wide pull | Any `run:` call, plus the routes that enqueue external collection or return up to 100,000 rows   |

Price follows the work, not the HTTP method. The authoritative number for any request is the `X-Credits-Cost` header on its response.

### Always free [#always-free]

These cost 0 and are exempt from the balance check, so a workspace that has run out of credits can still find out why:

```
GET /me
GET /workspaces
GET /domains
GET /credits
GET /credits/log
GET /credits/workspaces
GET /alerts/rules
GET /alerts/history
GET /alerts/metrics
GET /domains/reports/sections-catalog
GET /domains/reports/default-config
```

Charging for a catalog an agent has to read before it can build a valid request would be a tax on doing the right thing.

`GET /credits/workspaces`, `GET /alerts/rules` and `GET /alerts/history` are free for a related reason: each reads every workspace the credential can access in a single call, so no one workspace can honestly be billed for it. Their `workspace_id` and `domain_id` filters do not change that, because they are query parameters and the meter resolves the paying workspace from the path only. Even a narrowed call has no payer to charge.

Almost every `/notifications` route is free as well. Notifications belong to the person behind the credential rather than to a workspace, so no workspace owns the read and none can honestly be charged for it:

```
GET /notifications
GET /notifications/stats
GET /notifications/unread-count
GET /notifications/preferences
GET /notifications/domain-subscriptions
PUT /notifications/preferences
PUT /notifications/domain-subscriptions
POST /notifications/read
POST /notifications/{notification_id}/read
POST /notifications/{notification_id}/unread
DELETE /notifications/{notification_id}
```

The one exception is `GET /notifications/{notification_id}/trend`, which costs 1. It only answers for an alert-backed notification, which always carries a domain, so the charge has a workspace to land on, and it recomputes the series from live data rather than reading a stored row.

### The 25-credit routes [#the-25-credit-routes]

Beyond every `run:` endpoint, two groups carry the dispatch price:

**Wide pulls**, whose `limit` parameter admits up to 100,000 rows:

```
GET /domains/{domain_id}/gsc/performance
GET /domains/{domain_id}/analytics/ecommerce/products
```

**Dispatches**, which enqueue work into shared external collection pools:

```
POST /domains/{domain_id}/crux/urls/bulk
POST /domains/{domain_id}/synthetic/urls/bulk
POST /domains/{domain_id}/indexing/properties/{property_id}
```

Polling a report job's status is priced as an ordinary read, not as a dispatch. Watching a report should never cost more than starting one.

What makes a dispatch is the external quota it spends, not the fact that it starts background work. `POST /domains/{domain_id}/robots/sitemap-conflicts/recompute` also enqueues a job, but it recomputes from data we already hold and touches no third-party quota, so it costs the ordinary write price.

## A cached answer costs zero [#a-cached-answer-costs-zero]

Not a floor of one. A literal zero.

`X-Cache` on every response tells you which happened:

| Value         | Meaning                                                                                 |
| ------------- | --------------------------------------------------------------------------------------- |
| `HIT`         | Served from cache. This request cost 0.                                                 |
| `MISS`        | The work actually ran. Full price.                                                      |
| `UNAVAILABLE` | The cache could not be read. Priced as a hit, because a cache outage is not your fault. |

Two details worth knowing:

* **A single miss anywhere in a fanned-out request loses the discount for the whole request.** `GET /domains/{domain_id}/overview` covers eight modules; if one of them misses, the request is charged in full.
* **The five wide-pull routes above never get the discount.** They always charge 25, cache hit or not. A wide pull hands you your largest possible payload at full transfer cost, and a repeat request inside the cache window is exactly the shape that would otherwise be free.

## A rejected request is never charged [#a-rejected-request-is-never-charged]

The charge only lands on a `2xx`. A `402`, a `403`, or a `429` costs nothing, so an agent looping on its own lockout does not keep adding to the number in the error body it is reading.

## Which workspace is charged [#which-workspace-is-charged]

**A request is charged to the workspace whose data it read**, resolved from the `workspace_id` or the `domain_id` in the request path.

A route that spans every workspace the credential can reach names none of them in its path, so there is no payer to resolve. Those routes are free rather than charged to whichever workspace happened to come first, which is why the account-wide listings under [Always free](#always-free) cost 0 even when you filter them.

For an API key this changes nothing. A key belongs to one workspace, which is always both the reader and the payer.

For an OAuth or MCP connection that covers several workspaces it matters. Such a connection spends from several separate allowances, one per workspace, and **one can run out while the others are untouched**. A `402` on one domain therefore tells you nothing about the next.

`X-Credits-Remaining` on a billable response always reports the balance of the workspace that response was charged to, so it is the number to trust when a connection spans more than one.

## Plan allowances [#plan-allowances]

| Plan     | Credits per billing cycle |
| -------- | ------------------------: |
| Free     |                       500 |
| Starter  |                     2,500 |
| Lite     |                    25,000 |
| Standard |                   100,000 |
| Advanced |                   250,000 |

Unused credits do **not** roll over.

The allowance refills on your workspace's billing anniversary. Free workspaces have no subscription, so the anchor is the signup anniversary instead.

Changing your plan mid-cycle does not restart the cycle. An open cycle keeps its dates, and the new anniversary takes effect at the next one.

## Reading your balance [#reading-your-balance]

```bash
curl -sS https://app.vitalsentinel.com/api/public/v1/credits \
  -H "Authorization: Bearer $VITALSENTINEL_API_KEY"
```

```json
{
  "workspace_id": "e0d4a2c8-1b77-4f39-9c52-6a8d3e0f4b21",
  "granted": 100000,
  "used": 34211,
  "remaining": 65789,
  "cycle_start": "2026-07-14",
  "cycle_end": "2026-08-13",
  "enabled": true,
  "member": { "limit": null, "used": 8420, "remaining": null }
}
```

| Field       | Meaning                                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
| `granted`   | Your plan's allowance for one cycle                                                                                 |
| `remaining` | `granted` minus `used`, floored at 0. A concurrent burst can overshoot slightly, reported as 0 rather than negative |
| `cycle_end` | Last day of the cycle, inclusive. The allowance refills the following day                                           |
| `enabled`   | Whether charging is being enforced. `false` means the numbers are tracked but nothing is refused                    |
| `member`    | Your own cap, if the workspace owner set one                                                                        |

This endpoint needs no scope and costs nothing. A credential holding only `read:rum` can still read the balance that just cut it off.

It reports the connection's default workspace, which for an API key is the only one. Pass `workspace_id` to ask about another workspace the credential can reach:

```bash
curl -sS "https://app.vitalsentinel.com/api/public/v1/credits?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $VITALSENTINEL_API_KEY"
```

Naming a workspace the credential cannot reach returns a `404`, like any other unreachable resource. When a call is refused, ask about the workspace that owns the domain you were reading rather than the default one: that is the allowance that ran out.

### The spending log [#the-spending-log]

`GET /credits/log` returns this cycle's spending request by request, newest first, cursor-paginated.

Rows are the requests **this workspace paid for**. A connection covering several workspaces has a separate log for each, so pass `workspace_id` to read another, or omit it for the connection's default workspace.

It lists **every member's** requests, which is workspace-wide visibility rather than a view of your own spend, so it requires the **admin** or **owner** role. An OAuth credential additionally needs the `read:credits` scope. A member gets a `403` with `insufficient-role`, and that `403` still costs nothing.

Rows charged 0 are included deliberately: a 0 on a route that normally costs 5 means a cache hit, and a 0 on a `4xx` means a rejected request was never billed.

The log inherits the access log's 90-day retention. If you need older spend data, export it before then.

The same cycle's spend is in the dashboard under **Settings** → **API Credits**, in the **Spending log** card, owner and admin only. That card and the workspace's **Data Access Log** answer different questions and neither replaces the other: the Spending log is keyed on who paid, and the Data Access Log is keyed on whose data was read, over 90 days and with a CSV export.

## Per-member caps [#per-member-caps]

A workspace **owner** can cap how much of the shared allowance any one member may spend, in the dashboard under **API Credits** on the workspace. A cap belongs to one workspace, so a member who works across several has a separate cap, and a separate share of the allowance, in each.

| `limit` value | Effect                                                                                              |
| ------------- | --------------------------------------------------------------------------------------------------- |
| `null`        | Uncapped. This is the default for every member.                                                     |
| A number      | That member may spend up to this many credits per cycle                                             |
| `0`           | That member is blocked from the API and MCP entirely, while the rest of the workspace keeps working |

<Callout type="warning">
  **A cap is not a grant.** The workspace balance is always the outer bound. Capping a member at 20,000 in a workspace with 500 credits left buys them 500, not 20,000.
</Callout>

When a cap is what stopped you, the `402` names `member_credits` rather than `credits`. Check the workspace balance first when debugging a `402`: it binds in both directions.

## Running out [#running-out]

A `402` with `quota-exhausted`. Retrying later today does not help. Retrying next cycle does.

```json
{
  "type": "https://docs.vitalsentinel.com/api/errors/quota-exhausted",
  "title": "Quota exhausted",
  "status": 402,
  "detail": "This workspace has used its API credit allowance for the current cycle.",
  "used": 100000,
  "limit": 100000,
  "resets_on": "2026-08-14"
}
```

The always-free routes keep working, so your client can still read `GET /credits` and report the reset date to whoever needs to act.

## Spending less [#spending-less]

1. **Use `GET /domains/{domain_id}/overview`.** Eight modules for one heavy read beats eight separate reads.
2. **Do not poll faster than the data changes.** Most module caches refresh hourly. Polling every minute costs sixty times as much for the same answer, unless you are getting cache hits, in which case it costs nothing but still burns your [daily ceiling](/api/rate-limits).
3. **Narrow your windows.** A wide pull is 25 credits whether you needed 200 rows or 100,000.
4. **Watch `X-Credits-Cost` in development.** It is the only number that cannot be wrong, and it tells you immediately which of your calls is the expensive one.
5. **Cache report runs on your side.** A `run:` call is 25 credits every time, and the result rarely changes between two calls minutes apart.
