# Spend

> Usage, spend, forecasts, caps, rate cards, billing and invoices.

Usage, spend, forecasts, caps, rate cards, billing and invoices. All amounts are GBP.

| Operation | Method and path |
| --- | --- |
| [Current public prices](#current-public-prices) | `GET /v1/rate-card` |
| [Workspace spend this month, forecast and per-project breakdown](#workspace-spend-this-month-forecast-and-per-project-breakdown) | `GET /v1/workspaces/{workspace}/spend` |
| [Workspace usage timeseries](#workspace-usage-timeseries) | `GET /v1/workspaces/{workspace}/usage/timeseries` |
| [List spend caps](#list-spend-caps) | `GET /v1/workspaces/{workspace}/spend-caps` |
| [Create a spend cap](#create-a-spend-cap) | `POST /v1/workspaces/{workspace}/spend-caps` |
| [Get a spend cap](#get-a-spend-cap) | `GET /v1/workspaces/{workspace}/spend-caps/{cap_id}` |
| [Change a spend cap](#change-a-spend-cap) | `PATCH /v1/workspaces/{workspace}/spend-caps/{cap_id}` |
| [Remove a spend cap](#remove-a-spend-cap) | `DELETE /v1/workspaces/{workspace}/spend-caps/{cap_id}` |
| [Plan, payment method and balance](#plan-payment-method-and-balance) | `GET /v1/workspaces/{workspace}/billing` |
| [Change plan](#change-plan) | `POST /v1/workspaces/{workspace}/billing/plan` |
| [Open the payment portal](#open-the-payment-portal) | `POST /v1/workspaces/{workspace}/billing/portal` |
| [List invoices](#list-invoices) | `GET /v1/workspaces/{workspace}/invoices` |
| [Get an invoice](#get-an-invoice) | `GET /v1/workspaces/{workspace}/invoices/{invoice_id}` |
| [Project spend with per-service breakdown and "why" notes](#project-spend-with-per-service-breakdown-and-why-notes) | `GET /v1/projects/{project}/spend` |
| [Project usage timeseries](#project-usage-timeseries) | `GET /v1/projects/{project}/usage/timeseries` |
| [Raw usage rows with cost](#raw-usage-rows-with-cost) | `GET /v1/projects/{project}/usage` |

## Current public prices

```http
GET /v1/rate-card
```

- **Operation:** `getRateCard`
- **Scopes:** none (public)

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `at` | query | string (date-time) | Rate card in force at this time. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [RateCard](/docs/api/objects/rate-card/) |

**Errors:** `400`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/rate-card \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "id": "rc_01jbx2m9k7q4r8t5v6w3y0z1rc",
  "version": "2026-10",
  "effective_from": "2026-10-01T00:00:00Z",
  "currency": "GBP",
  "items": [
    {
      "metric": "vcpu_seconds",
      "unit": "vCPU-month",
      "unit_price": {
        "amount": "10.00",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Billed per second on actual use"
    },
    {
      "metric": "memory_gb_seconds",
      "unit": "GB-month",
      "unit_price": {
        "amount": "5.00",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Working set, billed per second"
    },
    {
      "metric": "volume_mb",
      "unit": "GB-month",
      "unit_price": {
        "amount": "0.10",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Single-server NVMe, backed up"
    },
    {
      "metric": "volume_ha_mb",
      "unit": "GB-month",
      "unit_price": {
        "amount": "0.20",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Replicated volume"
    },
    {
      "metric": "db_storage_mb",
      "unit": "GB-month",
      "unit_price": {
        "amount": "0.15",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Includes point-in-time backups"
    },
    {
      "metric": "egress_bytes",
      "unit": "GB",
      "unit_price": {
        "amount": "0.02",
        "currency": "GBP"
      },
      "free_quantity": "100",
      "description": "First 100 GB a month free"
    },
    {
      "metric": "static_ip_seconds",
      "unit": "IP-month",
      "unit_price": {
        "amount": "5.00",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Static egress IP"
    },
    {
      "metric": "build_seconds",
      "unit": "build-minute",
      "unit_price": {
        "amount": "0.00",
        "currency": "GBP"
      },
      "free_quantity": null,
      "description": "Builds are free"
    }
  ]
}
```

## Workspace spend this month, forecast and per-project breakdown

```http
GET /v1/workspaces/{workspace}/spend
```

- **Operation:** `getWorkspaceSpend`
- **Scopes:** `spend:read`
- **CLI:** `guv spend`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `month` | query | string | YYYY-MM; defaults to the current month. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [SpendSummary](/docs/api/objects/spend-summary/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/spend \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "scope": {
    "type": "project",
    "id": "prj_01jbx2m9k7q4r8t5v6w3y0z1pr",
    "name": "primecarers"
  },
  "headline": "This month: £7.42 of your £10 cap. Forecast £9.10.",
  "period": {
    "start": "2026-10-01T00:00:00Z",
    "end": "2026-11-01T00:00:00Z"
  },
  "month_to_date": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "forecast_month_end": {
    "amount": "9.10",
    "currency": "GBP",
    "display": "£9.10"
  },
  "caps": [
    {
      "cap_id": "cap_01jbx2m9k7q4r8t5v6w3y0z1cp",
      "scope": "project",
      "amount": {
        "amount": "10.00",
        "currency": "GBP",
        "display": "£10.00"
      },
      "used_percent": 74.2,
      "forecast_percent": 91,
      "state": "warning",
      "on_cap": "sleep_non_production"
    }
  ],
  "by_resource": [
    {
      "metric": "vcpu_seconds",
      "quantity": "0.31",
      "unit": "vCPU-month",
      "cost": {
        "amount": "3.10",
        "currency": "GBP",
        "display": "£3.10"
      }
    },
    {
      "metric": "memory_gb_seconds",
      "quantity": "0.86",
      "unit": "GB-month",
      "cost": {
        "amount": "4.32",
        "currency": "GBP",
        "display": "£4.32"
      }
    }
  ],
  "breakdown": [
    {
      "type": "service",
      "id": "svc_01jbx2m9k7q4r8t5v6w3y0z1sv",
      "name": "web",
      "environment": "production",
      "cost": {
        "amount": "5.10",
        "currency": "GBP",
        "display": "£5.10"
      },
      "share_percent": 68.7,
      "why": "web used 0.2 vCPU and 610 MB on average"
    }
  ],
  "updated_at": "2026-10-23T14:05:00Z"
}
```

## Workspace usage timeseries

```http
GET /v1/workspaces/{workspace}/usage/timeseries
```

- **Operation:** `getWorkspaceUsageTimeseries`
- **Scopes:** `spend:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `from` | query | string (date-time) |  |
| `to` | query | string (date-time) |  |
| `range` | query | `7d`, `month`, `last_month`, `90d` |  |
| `step` | query | `5m`, `1h`, `1d` |  |
| `group_by` | query | `resource`, `service`, `environment`, `project` |  |
| `resource` | query | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | Usage timeseries. | [UsageTimeseries](/docs/api/objects/usage-timeseries/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/usage/timeseries \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "scope": {
    "type": "project",
    "id": "prj_example",
    "name": "Example"
  },
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-11-01T00:00:00Z",
  "step": "1d",
  "step_seconds": 86400,
  "timezone": "Europe/London",
  "range": "month",
  "group_by": "resource",
  "resources": [
    "cpu"
  ],
  "buckets": [],
  "days": [],
  "forecast_from": null,
  "series": [],
  "totals": [],
  "total": {
    "amount": "0.000000",
    "currency": "GBP",
    "display": "£0.00"
  },
  "forecast_total": null,
  "month_to_date": {
    "amount": "0.000000",
    "currency": "GBP",
    "display": "£0.00"
  },
  "forecast_month_end": {
    "amount": "0.000000",
    "currency": "GBP",
    "display": "£0.00"
  },
  "cap": null,
  "busiest": null,
  "simulated": false,
  "updated_at": "2026-10-23T12:00:00Z"
}
```

## List spend caps

```http
GET /v1/workspaces/{workspace}/spend-caps
```

- **Operation:** `listSpendCaps`
- **Scopes:** `spend:read`
- **Paginated:** `?limit=` and `?cursor=`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | A page of SpendCap objects. | [SpendCapList](/docs/api/objects/spend-cap-list/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/spend-caps \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "data": [
    {
      "id": "cap_01jbx2m9k7q4r8t5v6w3y0z1pr",
      "scope": {
        "type": "workspace",
        "project_id": "string",
        "environment_id": "string"
      },
      "amount": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      },
      "on_cap": "sleep_non_production",
      "warn_at": [
        50,
        80
      ],
      "state": "ok",
      "month_to_date": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      },
      "forecast": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      },
      "source": "default",
      "created_at": "2026-10-22T13:05:00Z",
      "updated_at": "2026-10-22T13:05:00Z"
    }
  ],
  "next_cursor": "string",
  "has_more": false
}
```

## Create a spend cap

```http
POST /v1/workspaces/{workspace}/spend-caps
```

- **Operation:** `createSpendCap`
- **Scopes:** `spend:write`
- **Idempotency-Key:** required; supports `?dry_run=true`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Request body** (`application/json`, required): [SpendCapCreate](/docs/api/objects/spend-cap-create/)

| Field | Type | Description |
| --- | --- | --- |
| `scope` * | object |  |
| `amount` * | [Money](/docs/api/objects/money/) |  |
| `on_cap` | [OnCapAction](/docs/api/objects/on-cap-action/) |  |
| `warn_at` | array of integer |  |
| `acknowledge_no_pause` | boolean | Required true when `on_cap` is `notify_only`. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | Dry run. Nothing was changed; this is what would happen. | [Plan](/docs/api/objects/plan/) |
| `201` Created | Created. | [SpendCap](/docs/api/objects/spend-cap/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/spend-caps \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "scope": {
    "type": "workspace",
    "project": "string",
    "environment": "string"
  },
  "amount": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "on_cap": "sleep_non_production",
  "warn_at": [
    1
  ],
  "acknowledge_no_pause": false
}'
```

**Example response**

```json
{
  "id": "cap_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "scope": {
    "type": "workspace",
    "project_id": "string",
    "environment_id": "string"
  },
  "amount": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "on_cap": "sleep_non_production",
  "warn_at": [
    50,
    80
  ],
  "state": "ok",
  "month_to_date": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "forecast": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "source": "default",
  "created_at": "2026-10-22T13:05:00Z",
  "updated_at": "2026-10-22T13:05:00Z"
}
```

## Get a spend cap

```http
GET /v1/workspaces/{workspace}/spend-caps/{cap_id}
```

- **Operation:** `getSpendCap`
- **Scopes:** `spend:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `cap_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [SpendCap](/docs/api/objects/spend-cap/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/spend-caps/cap_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "id": "cap_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "scope": {
    "type": "workspace",
    "project_id": "string",
    "environment_id": "string"
  },
  "amount": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "on_cap": "sleep_non_production",
  "warn_at": [
    50,
    80
  ],
  "state": "ok",
  "month_to_date": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "forecast": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "source": "default",
  "created_at": "2026-10-22T13:05:00Z",
  "updated_at": "2026-10-22T13:05:00Z"
}
```

## Change a spend cap

```http
PATCH /v1/workspaces/{workspace}/spend-caps/{cap_id}
```

Raising the amount always needs a human; agent tokens get `approval_required`.

- **Operation:** `updateSpendCap`
- **Scopes:** `spend:write`
- **Approval:** Agent tokens get `approval_required`; a human approves with a passkey and Guv runs the stored request.
- **Idempotency-Key:** required; supports `?dry_run=true`
- **Concurrency:** send the `ETag` you read as `If-Match`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `cap_id` * | path | string |  |

**Request body** (`application/json`, required): [SpendCapUpdate](/docs/api/objects/spend-cap-update/)

| Field | Type | Description |
| --- | --- | --- |
| `amount` | [Money](/docs/api/objects/money/) |  |
| `on_cap` | [OnCapAction](/docs/api/objects/on-cap-action/) |  |
| `warn_at` | array of integer |  |
| `acknowledge_no_pause` | boolean |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. With `dry_run=true`: a Plan (application/vnd.guv.plan+json) and nothing changes. | [SpendCap](/docs/api/objects/spend-cap/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `412`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X PATCH https://api.guv.dev/v1/workspaces/acme/spend-caps/cap_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "on_cap": "sleep_non_production",
  "warn_at": [
    1
  ],
  "acknowledge_no_pause": false
}'
```

**Example response**

```json
{
  "id": "cap_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "scope": {
    "type": "workspace",
    "project_id": "string",
    "environment_id": "string"
  },
  "amount": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "on_cap": "sleep_non_production",
  "warn_at": [
    50,
    80
  ],
  "state": "ok",
  "month_to_date": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "forecast": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "source": "default",
  "created_at": "2026-10-22T13:05:00Z",
  "updated_at": "2026-10-22T13:05:00Z"
}
```

## Remove a spend cap

```http
DELETE /v1/workspaces/{workspace}/spend-caps/{cap_id}
```

- **Operation:** `deleteSpendCap`
- **Scopes:** `spend:write`
- **Approval:** Agent tokens get `approval_required`; a human approves with a passkey and Guv runs the stored request.
- **Idempotency-Key:** required; supports `?dry_run=true`
- **Concurrency:** send the `ETag` you read as `If-Match`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `cap_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | Dry run. Nothing was changed; this is what would happen. | [Plan](/docs/api/objects/plan/) |
| `204` No Content | Done. No body. | none |

**Errors:** `400`, `401`, `403`, `404`, `409`, `412`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X DELETE https://api.guv.dev/v1/workspaces/acme/spend-caps/cap_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

## Plan, payment method and balance

```http
GET /v1/workspaces/{workspace}/billing
```

- **Operation:** `getBilling`
- **Scopes:** `billing:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [Billing](/docs/api/objects/billing/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/billing \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "workspace_id": "string",
  "plan": "trial",
  "plan_fee": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "included_usage": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "payment_method": {
    "brand": "string",
    "last4": "string",
    "exp_month": 1,
    "exp_year": 1
  },
  "credit_balance": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "next_invoice_estimate": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "vat_number": "string",
  "status": "ok"
}
```

## Change plan

```http
POST /v1/workspaces/{workspace}/billing/plan
```

- **Operation:** `changePlan`
- **Scopes:** `billing:write`
- **Approval:** Agent tokens get `approval_required`; a human approves with a passkey and Guv runs the stored request.
- **Idempotency-Key:** required; supports `?dry_run=true`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Request body** (`application/json`, required): [PlanChangeRequest](/docs/api/objects/plan-change-request/)

| Field | Type | Description |
| --- | --- | --- |
| `plan` * | `hobby`, `pro`, `business` |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. With `dry_run=true`: a Plan (application/vnd.guv.plan+json) and nothing changes. | [Billing](/docs/api/objects/billing/) |

**Errors:** `400`, `401`, `402`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/billing/plan \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "plan": "hobby"
}'
```

**Example response**

```json
{
  "workspace_id": "string",
  "plan": "trial",
  "plan_fee": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "included_usage": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "payment_method": {
    "brand": "string",
    "last4": "string",
    "exp_month": 1,
    "exp_year": 1
  },
  "credit_balance": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "next_invoice_estimate": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "vat_number": "string",
  "status": "ok"
}
```

## Open the payment portal

```http
POST /v1/workspaces/{workspace}/billing/portal
```

- **Operation:** `createBillingPortalSession`
- **Scopes:** `billing:write`
- **Idempotency-Key:** required

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `201` Created | Created. | [BillingPortalSession](/docs/api/objects/billing-portal-session/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/billing/portal \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

**Example response**

```json
{
  "url": "https://guv.dev/",
  "expires_at": "2026-10-22T13:05:00Z"
}
```

## List invoices

```http
GET /v1/workspaces/{workspace}/invoices
```

- **Operation:** `listInvoices`
- **Scopes:** `billing:read`
- **Paginated:** `?limit=` and `?cursor=`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | A page of Invoice objects. | [InvoiceList](/docs/api/objects/invoice-list/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/invoices \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "data": [
    {
      "id": "inv_01jbx2m9k7q4r8t5v6w3y0z1pr",
      "number": "GUV-2026-000123",
      "status": "draft",
      "period_start": "2026-10-22T13:05:00Z",
      "period_end": "2026-10-22T13:05:00Z",
      "subtotal": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      },
      "vat": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      },
      "total": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      },
      "lines": [
        {
          "description": "string",
          "quantity": "string",
          "unit": "string",
          "amount": {
            "amount": "7.42",
            "currency": "GBP",
            "display": "£7.42"
          }
        }
      ],
      "hosted_url": "https://guv.dev/",
      "pdf_url": "https://guv.dev/",
      "issued_at": "2026-10-22T13:05:00Z",
      "paid_at": "2026-10-22T13:05:00Z"
    }
  ],
  "next_cursor": "string",
  "has_more": false
}
```

## Get an invoice

```http
GET /v1/workspaces/{workspace}/invoices/{invoice_id}
```

- **Operation:** `getInvoice`
- **Scopes:** `billing:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `invoice_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [Invoice](/docs/api/objects/invoice/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/invoices/inv_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "id": "inv_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "number": "GUV-2026-000123",
  "status": "draft",
  "period_start": "2026-10-22T13:05:00Z",
  "period_end": "2026-10-22T13:05:00Z",
  "subtotal": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "vat": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "total": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "lines": [
    {
      "description": "string",
      "quantity": "string",
      "unit": "string",
      "amount": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      }
    }
  ],
  "hosted_url": "https://guv.dev/",
  "pdf_url": "https://guv.dev/",
  "issued_at": "2026-10-22T13:05:00Z",
  "paid_at": "2026-10-22T13:05:00Z"
}
```

## Project spend with per-service breakdown and "why" notes

```http
GET /v1/projects/{project}/spend
```

- **Operation:** `getProjectSpend`
- **Scopes:** `spend:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `project` * | path | string | Project id (`prj_…`) or slug (resolved in the `Guv-Workspace` workspace, or the caller's default). |
| `month` | query | string |  |
| `environment` | query | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [SpendSummary](/docs/api/objects/spend-summary/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/projects/shopfront/spend \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "scope": {
    "type": "project",
    "id": "prj_01jbx2m9k7q4r8t5v6w3y0z1pr",
    "name": "primecarers"
  },
  "headline": "This month: £7.42 of your £10 cap. Forecast £9.10.",
  "period": {
    "start": "2026-10-01T00:00:00Z",
    "end": "2026-11-01T00:00:00Z"
  },
  "month_to_date": {
    "amount": "7.42",
    "currency": "GBP",
    "display": "£7.42"
  },
  "forecast_month_end": {
    "amount": "9.10",
    "currency": "GBP",
    "display": "£9.10"
  },
  "caps": [
    {
      "cap_id": "cap_01jbx2m9k7q4r8t5v6w3y0z1cp",
      "scope": "project",
      "amount": {
        "amount": "10.00",
        "currency": "GBP",
        "display": "£10.00"
      },
      "used_percent": 74.2,
      "forecast_percent": 91,
      "state": "warning",
      "on_cap": "sleep_non_production"
    }
  ],
  "by_resource": [
    {
      "metric": "vcpu_seconds",
      "quantity": "0.31",
      "unit": "vCPU-month",
      "cost": {
        "amount": "3.10",
        "currency": "GBP",
        "display": "£3.10"
      }
    },
    {
      "metric": "memory_gb_seconds",
      "quantity": "0.86",
      "unit": "GB-month",
      "cost": {
        "amount": "4.32",
        "currency": "GBP",
        "display": "£4.32"
      }
    }
  ],
  "breakdown": [
    {
      "type": "service",
      "id": "svc_01jbx2m9k7q4r8t5v6w3y0z1sv",
      "name": "web",
      "environment": "production",
      "cost": {
        "amount": "5.10",
        "currency": "GBP",
        "display": "£5.10"
      },
      "share_percent": 68.7,
      "why": "web used 0.2 vCPU and 610 MB on average"
    }
  ],
  "updated_at": "2026-10-23T14:05:00Z"
}
```

## Project usage timeseries

```http
GET /v1/projects/{project}/usage/timeseries
```

- **Operation:** `getProjectUsageTimeseries`
- **Scopes:** `spend:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `project` * | path | string | Project id (`prj_…`) or slug (resolved in the `Guv-Workspace` workspace, or the caller's default). |
| `from` | query | string (date-time) |  |
| `to` | query | string (date-time) |  |
| `range` | query | `7d`, `month`, `last_month`, `90d` |  |
| `step` | query | `5m`, `1h`, `1d` |  |
| `group_by` | query | `resource`, `service`, `environment`, `project` |  |
| `environment` | query | string |  |
| `resource` | query | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | Usage timeseries. | [UsageTimeseries](/docs/api/objects/usage-timeseries/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/projects/shopfront/usage/timeseries \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "scope": {
    "type": "project",
    "id": "prj_example",
    "name": "Example"
  },
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-11-01T00:00:00Z",
  "step": "1d",
  "step_seconds": 86400,
  "timezone": "Europe/London",
  "range": "month",
  "group_by": "resource",
  "resources": [
    "cpu"
  ],
  "buckets": [],
  "days": [],
  "forecast_from": null,
  "series": [],
  "totals": [],
  "total": {
    "amount": "0.000000",
    "currency": "GBP",
    "display": "£0.00"
  },
  "forecast_total": null,
  "month_to_date": {
    "amount": "0.000000",
    "currency": "GBP",
    "display": "£0.00"
  },
  "forecast_month_end": {
    "amount": "0.000000",
    "currency": "GBP",
    "display": "£0.00"
  },
  "cap": null,
  "busiest": null,
  "simulated": false,
  "updated_at": "2026-10-23T12:00:00Z"
}
```

## Raw usage rows with cost

```http
GET /v1/projects/{project}/usage
```

- **Operation:** `getProjectUsage`
- **Scopes:** `spend:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `project` * | path | string | Project id (`prj_…`) or slug (resolved in the `Guv-Workspace` workspace, or the caller's default). |
| `from` * | query | string (date-time) |  |
| `to` * | query | string (date-time) |  |
| `granularity` | query | `hour`, `day` |  |
| `environment` | query | string |  |
| `service` | query | string |  |
| `metric` | query | [UsageMetric](/docs/api/objects/usage-metric/) |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [UsageReport](/docs/api/objects/usage-report/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/projects/shopfront/usage \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "from": "2026-10-22T13:05:00Z",
  "to": "2026-10-22T13:05:00Z",
  "granularity": "hour",
  "rows": [
    {
      "start": "2026-10-22T13:05:00Z",
      "environment_id": "string",
      "service_id": "string",
      "subject": {
        "type": "service",
        "id": "svc_01jbx2m9k7q4r8t5v6w3y0z1sv",
        "name": "web",
        "url": "/v1/projects/primecarers/services/web"
      },
      "metric": "vcpu_seconds",
      "quantity": "string",
      "unit": "string",
      "cost": {
        "amount": "7.42",
        "currency": "GBP",
        "display": "£7.42"
      }
    }
  ],
  "next_cursor": "string"
}
```
