# Billing and usage API

> Balances, limits, metered usage, invoice preview, and billing management.

- [Get billing](#get-billing): `GET /v1/billing`
- [Change billing settings](#change-billing-settings): `PATCH /v1/billing`
- [Start a checkout](#start-a-checkout): `POST /v1/billing/checkout`
- [Open the payment portal](#open-the-payment-portal): `POST /v1/billing/portal`
- [Verify a card for the trial](#verify-a-card-for-the-trial): `POST /v1/billing/card-check`
- [Cancel the plan at period end](#cancel-the-plan-at-period-end): `POST /v1/billing/subscription/cancel`
- [Resume a cancelled plan](#resume-a-cancelled-plan): `POST /v1/billing/subscription/resume`
- [End the trial early](#end-the-trial-early): `POST /v1/billing/trial/end`
- [Get automatic refill](#get-automatic-refill): `GET /v1/billing/auto-refill`
- [Change automatic refill](#change-automatic-refill): `PUT /v1/billing/auto-refill`
- [Preview the invoice](#preview-the-invoice): `GET /v1/billing/invoice-preview`
- [Get limits](#get-limits): `GET /v1/limits`
- [Get organization usage](#get-organization-usage): `GET /v1/usage`
- [Get machine usage](#get-machine-usage): `GET /v1/machines/{machine_id}/usage`
- [Receive a Stripe event](#receive-a-stripe-event): `POST /v1/billing/stripe/webhook`
- [Grant credits or time (operators)](#grant-credits-or-time-operators): `POST /v1/operator/billing/grants`
- [List wallet movements](#list-wallet-movements): `GET /v1/billing/ledger`
- [Grant credits from VMYard (operators)](#grant-credits-from-vmyard-operators): `POST /v1/operator/billing/credits`

## Get billing

`GET /v1/billing`

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`BillingDto`](/docs/api/schemas#billingdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/billing" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "balances": {
    "credit_micros": 0,
    "plan_micros": 0,
    "spendable_micros": 0,
    "trial_micros": 0
  },
  "card_check": {
    "required": false,
    "verified_at": "string"
  },
  "credit_spent_this_month_micros": 0,
  "currency": "USD",
  "exhausted_since": "string",
  "limits": {
    "concurrent_machines": {
      "limit": 0,
      "used": 0
    },
    "starts": [
      {
        "limit": 0,
        "used": 0,
        "window": "hour"
      }
    ],
    "status": "trial",
    "tier": "pro"
  },
  "minimum_balance_micros": 0,
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "payment_customer_linked": false,
  "payments_available": false,
  "plan": {
    "active": false,
    "cancel_at_period_end": false,
    "included_micros": 0,
    "included_seconds": 0,
    "period_end": "string",
    "period_start": "string",
    "price_micros": 0,
    "renews": false,
    "tier": "pro"
  },
  "spend_cap_micros": 0,
  "status": "trial",
  "trial": {
    "active": false,
    "concurrent_machines": 0,
    "ended_at": "string",
    "max_hourly_micros": 0,
    "remaining_micros": 0,
    "remaining_seconds": 0,
    "total_micros": 0
  }
}
```

## Change billing settings

`PATCH /v1/billing`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Request body** (`application/json`, [`UpdateBillingRequest`](/docs/api/schemas#updatebillingrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `minimum_balance_micros` | `integer \| null` | No |  |
| `spend_cap_micros` | `integer \| null` | No | Monthly credit spend cap in micros, or `null` to remove it. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`BillingDto`](/docs/api/schemas#billingdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X PATCH "$VMYARD_API_URL/v1/billing" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "spend_cap_micros": 100000000,
  "minimum_balance_micros": 0
}'
```

```json title="Response 200"
{
  "balances": {
    "credit_micros": 0,
    "plan_micros": 0,
    "spendable_micros": 0,
    "trial_micros": 0
  },
  "card_check": {
    "required": false,
    "verified_at": "string"
  },
  "credit_spent_this_month_micros": 0,
  "currency": "USD",
  "exhausted_since": "string",
  "limits": {
    "concurrent_machines": {
      "limit": 0,
      "used": 0
    },
    "starts": [
      {
        "limit": 0,
        "used": 0,
        "window": "hour"
      }
    ],
    "status": "trial",
    "tier": "pro"
  },
  "minimum_balance_micros": 0,
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "payment_customer_linked": false,
  "payments_available": false,
  "plan": {
    "active": false,
    "cancel_at_period_end": false,
    "included_micros": 0,
    "included_seconds": 0,
    "period_end": "string",
    "period_start": "string",
    "price_micros": 0,
    "renews": false,
    "tier": "pro"
  },
  "spend_cap_micros": 0,
  "status": "trial",
  "trial": {
    "active": false,
    "concurrent_machines": 0,
    "ended_at": "string",
    "max_hourly_micros": 0,
    "remaining_micros": 0,
    "remaining_seconds": 0,
    "total_micros": 0
  }
}
```

## Start a checkout

`POST /v1/billing/checkout`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Request body** (`application/json`, [`CheckoutRequestDto`](/docs/api/schemas#checkoutrequestdto))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `credit_packs` | `integer \| null` | No | Buy this many $20 credit packs (1-50). |
| `end_trial` | `boolean` | No | With `plan`: forfeit the remaining trial once the plan's first payment is confirmed, so the plan's allowance is used right away. |
| `plan` | `string \| null` | No | Subscribe to `starter`, `pro`, `team`, or `scale`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `201` | — | [`CheckoutDto`](/docs/api/schemas#checkoutdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable`: payments are not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `502` | `payment_provider_error` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/checkout" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "credit_packs": 2
}'
```

```json title="Response 201"
{
  "checkout_url": "string",
  "session_id": "string"
}
```

## Open the payment portal

`POST /v1/billing/portal`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `201` | — | [`PortalDto`](/docs/api/schemas#portaldto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `billing_customer_missing`: no purchase has linked a payment customer yet | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable`: payments are not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `502` | `payment_provider_error` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/portal" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 201"
{
  "url": "https://hooks.example.com/vmyard"
}
```

## Verify a card for the trial

`POST /v1/billing/card-check`

Start a card check: a hosted setup page where a billing manager enters a
card, which the provider verifies (and screens for fraud) without
charging it. The signed `checkout.session.completed` webhook records the
verification; until then a trial cannot start machines when the service
requires a card (`402 card_verification_required`).

Opens the payment provider's page where a card is checked, not charged. When `card_check.required` is `true` on `GET /v1/billing`, a trial can't create, start, or restart machines (`402 card_verification_required`) until the check completes. See [Usage, billing, and limits](/docs/guides/billing#card-check).

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `201` | — | [`CardCheckSessionDto`](/docs/api/schemas#cardchecksessiondto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable`: payments are not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `502` | `payment_provider_error` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/card-check" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 201"
{
  "checkout_url": "string",
  "session_id": "string"
}
```

## Cancel the plan at period end

`POST /v1/billing/subscription/cancel`

The plan keeps its allowance until `plan.period_end`, then ends. See [Cancel a plan or end the trial](/docs/guides/plans-and-trial).

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | The plan stays active until `plan.period_end`, then ends | [`BillingDto`](/docs/api/schemas#billingdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `subscription_missing`: no renewing subscription is linked | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `502` | `payment_provider_error` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/subscription/cancel" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "balances": {
    "credit_micros": 0,
    "plan_micros": 0,
    "spendable_micros": 0,
    "trial_micros": 0
  },
  "card_check": {
    "required": false,
    "verified_at": "string"
  },
  "credit_spent_this_month_micros": 0,
  "currency": "USD",
  "exhausted_since": "string",
  "limits": {
    "concurrent_machines": {
      "limit": 0,
      "used": 0
    },
    "starts": [
      {
        "limit": 0,
        "used": 0,
        "window": "hour"
      }
    ],
    "status": "trial",
    "tier": "pro"
  },
  "minimum_balance_micros": 0,
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "payment_customer_linked": false,
  "payments_available": false,
  "plan": {
    "active": false,
    "cancel_at_period_end": false,
    "included_micros": 0,
    "included_seconds": 0,
    "period_end": "string",
    "period_start": "string",
    "price_micros": 0,
    "renews": false,
    "tier": "pro"
  },
  "spend_cap_micros": 0,
  "status": "trial",
  "trial": {
    "active": false,
    "concurrent_machines": 0,
    "ended_at": "string",
    "max_hourly_micros": 0,
    "remaining_micros": 0,
    "remaining_seconds": 0,
    "total_micros": 0
  }
}
```

## Resume a cancelled plan

`POST /v1/billing/subscription/resume`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | The plan renews again at `plan.period_end` | [`BillingDto`](/docs/api/schemas#billingdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `subscription_missing`: the subscription already ended | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `502` | `payment_provider_error` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/subscription/resume" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "balances": {
    "credit_micros": 0,
    "plan_micros": 0,
    "spendable_micros": 0,
    "trial_micros": 0
  },
  "card_check": {
    "required": false,
    "verified_at": "string"
  },
  "credit_spent_this_month_micros": 0,
  "currency": "USD",
  "exhausted_since": "string",
  "limits": {
    "concurrent_machines": {
      "limit": 0,
      "used": 0
    },
    "starts": [
      {
        "limit": 0,
        "used": 0,
        "window": "hour"
      }
    ],
    "status": "trial",
    "tier": "pro"
  },
  "minimum_balance_micros": 0,
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "payment_customer_linked": false,
  "payments_available": false,
  "plan": {
    "active": false,
    "cancel_at_period_end": false,
    "included_micros": 0,
    "included_seconds": 0,
    "period_end": "string",
    "period_start": "string",
    "price_micros": 0,
    "renews": false,
    "tier": "pro"
  },
  "spend_cap_micros": 0,
  "status": "trial",
  "trial": {
    "active": false,
    "concurrent_machines": 0,
    "ended_at": "string",
    "max_hourly_micros": 0,
    "remaining_micros": 0,
    "remaining_seconds": 0,
    "total_micros": 0
  }
}
```

## End the trial early

`POST /v1/billing/trial/end`

End the trial now: its remaining balance is forfeited (a
`trial_forfeit` ledger row) and cannot be restored. Trial rules (two
machines, small/default sizes) still apply until the first payment.
Ending an ended trial changes nothing.

Forfeits the remaining trial balance for good; trial rules apply until the first payment. See [Cancel a plan or end the trial](/docs/guides/plans-and-trial).

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`BillingDto`](/docs/api/schemas#billingdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/trial/end" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "balances": {
    "credit_micros": 0,
    "plan_micros": 0,
    "spendable_micros": 0,
    "trial_micros": 0
  },
  "card_check": {
    "required": false,
    "verified_at": "string"
  },
  "credit_spent_this_month_micros": 0,
  "currency": "USD",
  "exhausted_since": "string",
  "limits": {
    "concurrent_machines": {
      "limit": 0,
      "used": 0
    },
    "starts": [
      {
        "limit": 0,
        "used": 0,
        "window": "hour"
      }
    ],
    "status": "trial",
    "tier": "pro"
  },
  "minimum_balance_micros": 0,
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "payment_customer_linked": false,
  "payments_available": false,
  "plan": {
    "active": false,
    "cancel_at_period_end": false,
    "included_micros": 0,
    "included_seconds": 0,
    "period_end": "string",
    "period_start": "string",
    "price_micros": 0,
    "renews": false,
    "tier": "pro"
  },
  "spend_cap_micros": 0,
  "status": "trial",
  "trial": {
    "active": false,
    "concurrent_machines": 0,
    "ended_at": "string",
    "max_hourly_micros": 0,
    "remaining_micros": 0,
    "remaining_seconds": 0,
    "total_micros": 0
  }
}
```

## Get automatic refill

`GET /v1/billing/auto-refill`

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`AutoRefillDto`](/docs/api/schemas#autorefilldto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/billing/auto-refill" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "amount_micros": 0,
  "charged_this_month_micros": 0,
  "consecutive_failures": 0,
  "credit_packs": 0,
  "disabled_reason": "string",
  "enabled": false,
  "last_charge": {
    "amount_micros": 0,
    "created_at": "string",
    "credit_packs": 0,
    "failure_code": "string",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "status": "string"
  },
  "monthly_cap_micros": 0,
  "threshold_micros": 0
}
```

## Change automatic refill

`PUT /v1/billing/auto-refill`

Configure automatic refill. Enabling needs online payments, a linked
customer, and a default payment method at the provider; it also clears
the failure count of a refill that turned itself off.

All four fields are required. Enabling needs a linked customer and a default card at the payment provider. See [Automatic refill](/docs/guides/automatic-refill).

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Billing.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Request body** (`application/json`, [`UpdateAutoRefillRequest`](/docs/api/schemas#updateautorefillrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `credit_packs` | `integer` | Yes | 1-50 packs of $20. 1–50. |
| `enabled` | `boolean` | Yes |  |
| `monthly_cap_micros` | `integer` | Yes | At least one refill's amount. |
| `threshold_micros` | `integer` | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`AutoRefillDto`](/docs/api/schemas#autorefilldto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Refused for this caller: role, key preset, CSRF token, or organization (`forbidden`, `insufficient_scope`, `csrf_invalid`, `organization_mismatch`, `session_required`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `billing_customer_missing` or `payment_method_missing` (set a default card in the portal) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `502` | `payment_provider_error` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X PUT "$VMYARD_API_URL/v1/billing/auto-refill" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "credit_packs": 1,
  "enabled": false,
  "monthly_cap_micros": 0,
  "threshold_micros": 0
}'
```

```json title="Response 200"
{
  "amount_micros": 0,
  "charged_this_month_micros": 0,
  "consecutive_failures": 0,
  "credit_packs": 0,
  "disabled_reason": "string",
  "enabled": false,
  "last_charge": {
    "amount_micros": 0,
    "created_at": "string",
    "credit_packs": 0,
    "failure_code": "string",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "status": "string"
  },
  "monthly_cap_micros": 0,
  "threshold_micros": 0
}
```

## Preview the invoice

`GET /v1/billing/invoice-preview`

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`InvoicePreviewDto`](/docs/api/schemas#invoicepreviewdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/billing/invoice-preview" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "applied": {
    "credit_micros": 0,
    "plan_micros": 0,
    "trial_micros": 0
  },
  "credit_balance_micros": 0,
  "currency": "USD",
  "lines": [
    {
      "amount_micros": 0,
      "description": "default · machine time",
      "hourly_micros": 0,
      "multiplier": 0.5,
      "plan_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "plan_version": 0,
      "seconds": 0
    }
  ],
  "next_charge_micros": 0,
  "period_end": "string",
  "period_start": "string",
  "plan": {
    "active": false,
    "cancel_at_period_end": false,
    "included_micros": 0,
    "included_seconds": 0,
    "period_end": "string",
    "period_start": "string",
    "price_micros": 0,
    "renews": false,
    "tier": "pro"
  },
  "unbilled_micros": 0,
  "usage_micros": 0
}
```

## Get limits

`GET /v1/limits`

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`LimitsDto`](/docs/api/schemas#limitsdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/limits" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "concurrent_machines": {
    "limit": 0,
    "used": 0
  },
  "starts": [
    {
      "limit": 0,
      "used": 0,
      "window": "hour"
    }
  ],
  "status": "trial",
  "tier": "pro"
}
```

## Get organization usage

`GET /v1/usage`

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `since` | query | `string` | No | RFC 3339 start (inclusive). Defaults to the current plan period's start, or the UTC month's start without a plan; for one machine, to its first run. |
| `until` | query | `string` | No | RFC 3339 end (exclusive). Defaults to now. |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`UsageDto`](/docs/api/schemas#usagedto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/usage" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "cost_micros": 0,
  "machines": [
    {
      "cost_micros": 0,
      "hourly_micros": 0,
      "machine_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "multiplier": 0.5,
      "running": false,
      "seconds": 0
    }
  ],
  "seconds": 0,
  "since": "string",
  "until": "string"
}
```

## Get machine usage

`GET /v1/machines/{machine_id}/usage`

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `machine_id` | path | `string (uuid)` | Yes |  |
| `since` | query | `string` | No | RFC 3339 start (inclusive). Defaults to the current plan period's start, or the UTC month's start without a plan; for one machine, to its first run. |
| `until` | query | `string` | No | RFC 3339 end (exclusive). Defaults to now. |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`MachineUsageDto`](/docs/api/schemas#machineusagedto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/usage" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "cost_micros": 0,
  "hourly_micros": 0,
  "machine_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "multiplier": 0.5,
  "running": false,
  "seconds": 0
}
```

## Receive a Stripe event

`POST /v1/billing/stripe/webhook`

Stripe calls this with signed events. It needs no session or API key:
the signature over the exact body is the authentication.

- **Authentication:** none on this route.
- **Who:** Stripe (signed request).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Stripe-Signature` | header | `string` | Yes | t=<unix seconds>,v1=<hex HMAC-SHA256> |

**Request body** (`application/json`)

No fields.

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WebhookAckDto`](/docs/api/schemas#webhookackdto) |
| `400` | `invalid_signature` or `invalid_event` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `501` | `billing_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/billing/stripe/webhook" \
  -H "Stripe-Signature: $STRIPE_SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '"string"'
```

```json title="Response 200"
{
  "outcome": "applied",
  "received": false
}
```

## Grant credits or time (operators)

`POST /v1/operator/billing/grants`

Operators grant credits, trial time, or plan periods by hand (support,
invoices paid offline, or a deployment without online payments). The
caller presents the operator token configured for the service; members
of an organization, however privileged, cannot reach it.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`OperatorGrantRequest`](/docs/api/schemas#operatorgrantrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency_key` | `string` | Yes | Replaying the same key applies the grant once. |
| `kind` | [`GrantKindDto`](/docs/api/schemas#grantkinddto) | Yes |  |
| `organization_id` | `string (uuid)` | Yes |  |
| `reason` | `string` | Yes | Why the grant was made; recorded in the audit log. |
| `amount_micros` | `integer \| null` | No | Micros for `credits` and `trial`. |
| `period_days` | `integer \| null` | No | Period length for `plan_period`, starting now (1-366, default 30). |
| `tier` | `string \| null` | No | Tier for `plan_period`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`GrantResultDto`](/docs/api/schemas#grantresultdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator grants are not configured or the organization is unknown | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/billing/grants" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "organization_id": "2a7d4c1e-8b3f-4f6a-9c5d-0e1f2a3b4c5d",
  "kind": "credits",
  "amount_micros": 20000000,
  "reason": "Invoice 1042 paid by bank transfer",
  "idempotency_key": "grant-invoice-1042"
}'
```

```json title="Response 200"
{
  "outcome": "applied"
}
```

## List wallet movements

`GET /v1/billing/ledger`

The wallet's movements other than usage (grants, credits from VMYard,
purchases, expiries), newest first. Usage is reported by `/v1/usage`.

Grants, credits from VMYard, purchases, and expiries, newest first. Usage is in `GET /v1/usage`.

- **API key:** `Authorization: Bearer vmy_…` with preset `read_only`, `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Any member.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | `integer` | No | 1-100, default 50. |
| `before` | query | `integer` | No | Entries before this one (`next_before` of the previous page). |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`LedgerPage`](/docs/api/schemas#ledgerpage) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/billing/ledger" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "items": [
    {
      "amount_micros": 0,
      "at": "string",
      "bucket": "string",
      "id": 0,
      "kind": "trial_grant",
      "label": "k7f2mq3x9a1b",
      "plan_tier": "string"
    }
  ],
  "next_before": 0
}
```

## Grant credits from VMYard (operators)

`POST /v1/operator/billing/credits`

Grant credits from VMYard to an organization, or claw them back. They
are a gift, not a payment: the organization's status (and trial rules)
do not change. The organization sees each as "Credit from VMYard" in
`GET /v1/billing/ledger`.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`OperatorCreditRequest`](/docs/api/schemas#operatorcreditrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_micros` | `integer` | Yes | Positive micros (1 USD = 1000000). at least 1. |
| `idempotency_key` | `string` | Yes | Replaying the same key applies the change once. |
| `kind` | [`CreditKindDto`](/docs/api/schemas#creditkinddto) | Yes |  |
| `operator` | `string` | Yes | The individual operator acting, 1-200 characters. |
| `organization_id` | `string (uuid)` | Yes |  |
| `reason` | `string` | Yes | Why, 1-500 characters; kept on the ledger row and in the audit log, never shown to the organization. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`OperatorCreditDto`](/docs/api/schemas#operatorcreditdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured or the organization is unknown | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `insufficient_credit`: a claw-back larger than the credit balance | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/billing/credits" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "amount_micros": 1,
  "idempotency_key": "string",
  "kind": "grant",
  "operator": "string",
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "reason": "string"
}'
```

```json title="Response 200"
{
  "credit_balance_micros": 0,
  "outcome": "string"
}
```
