# Billing and usage

> Prepaid credit in euro: balance, ledger, top-ups, auto-recharge, the payment portal, the billing preview and metered usage.

Rytiv bills from prepaid credit in euro. Usage is metered per machine and booked against the balance; at zero, machines are stopped (not deleted), nothing new starts and reads stay open. Top-ups and the auto-recharge rule are set in the console by an admin; the routes for them refuse a plain API key. See the guide [Credit and prices](https://docs.rytiv.com/billing.md).

## Endpoints

- [`GET /v1/billing/credits`](#get-v1billingcredits): The credit account: balance, whether it is blocked at zero, the warning line and the auto-recharge rule
- [`GET /v1/billing/ledger`](#get-v1billingledger): The credit ledger: top-ups, usage bookings, grants, refunds and adjustments, newest first
- [`POST /v1/billing/topups`](#post-v1billingtopups): Starts a top-up and answers the payment page to open. The card is typed there; no card data crosses this API
- [`PUT /v1/billing/auto-recharge`](#put-v1billingauto-recharge): Sets the auto-recharge rule: when the balance falls below `thresholdCents`, charge `amountCents`, never more than `capCents` in total
- [`POST /v1/billing/portal`](#post-v1billingportal): A link to the payment portal, where a saved card is changed
- [`GET /v1/billing/preview`](#get-v1billingpreview): What a period cost, line by line, from the metered usage
- [`GET /v1/usage`](#get-v1usage): The metered usage events of the organization

## GET /v1/billing/credits

The credit account: balance, whether it is blocked at zero, the warning line and the auto-recharge rule.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/billing/credits" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the credit account:

```json
{
  "orgId": "<orgId>",
  "currency": "EUR",
  "prepaid": true,
  "balanceMicros": "<balanceMicros>",
  "balanceCents": -9007199254740991,
  "blocked": true,
  "blockedAt": "2026-10-21T09:00:00Z",
  "warned": true,
  "warnAtPercent": 0,
  "minTopupCents": 1,
  "priced": [
    "<priced>"
  ],
  "autoRecharge": {
    "enabled": true,
    "thresholdCents": 0,
    "amountCents": 0,
    "capCents": 0,
    "pending": true,
    "failedAt": "2026-10-21T09:00:00Z",
    "hasPaymentMethod": true
  }
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `orgId` | string | always | The organization that owns it. |
| `currency` | `"EUR"` | always |  |
| `prepaid` | boolean | always | Whether the organization is on prepaid credit. |
| `balanceMicros` | string | always | The exact balance in micro-cents, as a decimal string. |
| `balanceCents` | integer | always | The balance in whole cents, rounded down. -9007199254740991 to 9007199254740991. |
| `blocked` | boolean | always | The balance reached zero: machines stopped, nothing new starts, reads stay open. |
| `blockedAt` | string (ISO 8601) or null | always | When that happened. |
| `warned` | boolean | always | The balance fell below the warning line. |
| `warnAtPercent` | integer | always | The warning line as a percentage of the reference top-up. 0 to 100. |
| `minTopupCents` | integer | always | The smallest top-up. at most 9007199254740991. |
| `priced` | array of string | always | The usage kinds that carry a price right now. |
| `autoRecharge` | object | always | The auto-recharge rule and its state. |
| `autoRecharge.enabled` | boolean | always | `false` creates the schedule paused. |
| `autoRecharge.thresholdCents` | integer | always | Charge when the balance falls below this. 0 to 9007199254740991. |
| `autoRecharge.amountCents` | integer | always | Whole cents. 0 to 9007199254740991. |
| `autoRecharge.capCents` | integer | always | Never charge more than this in total through the rule. 0 to 9007199254740991. |
| `autoRecharge.pending` | boolean | always |  |
| `autoRecharge.failedAt` | string (ISO 8601) or null | always |  |
| `autoRecharge.hasPaymentMethod` | boolean | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/billing/ledger

The credit ledger: top-ups, usage bookings, grants, refunds and adjustments, newest first.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Query parameters

| Name | Description |
| --- | --- |
| `limit` | How many entries, at most. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/billing/ledger" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the ledger:

```json
{
  "orgId": "<orgId>",
  "currency": "EUR",
  "limit": 1,
  "entries": [
    {
      "id": "<id>",
      "kind": "topup",
      "amountMicros": "<amountMicros>",
      "amountCents": -9007199254740991,
      "reference": "goodwill-2026-10",
      "windowFrom": "2026-10-21T09:00:00Z",
      "windowTo": "2026-10-21T09:00:00Z",
      "detail": "<detail>",
      "at": "2026-10-21T09:00:00Z"
    }
  ]
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `orgId` | string | always | The organization that owns it. |
| `currency` | `"EUR"` | always |  |
| `limit` | integer | always | at most 9007199254740991. |
| `entries` | array of object | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## POST /v1/billing/topups

Starts a top-up and answers the payment page to open. The card is typed there; no card data crosses this API.

**Auth:** API key with full access, and only through the console as an admin (a plain API key answers `403` with `billing-console-only`). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `CreditTopupRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amountCents` | integer | required | Whole cents. at most 9007199254740991. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/billing/topups" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amountCents":1}'
```

### Response

`201` with the top-up:

```json
{
  "topupId": "<topupId>",
  "amountCents": 1,
  "checkoutUrl": "<checkoutUrl>",
  "sessionId": "<sessionId>"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `topupId` | string | always |  |
| `amountCents` | integer | always | Whole cents. at most 9007199254740991. |
| `checkoutUrl` | string | always | The payment page to open. The card is typed there. |
| `sessionId` | string | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `billing-console-only` | A billing change is made in the console by an admin, not with an API key. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 409 | `not-prepaid` | The organization has no credit account; only org creation or an operator's grant starts prepaid credit. |

## PUT /v1/billing/auto-recharge

Sets the auto-recharge rule: when the balance falls below `thresholdCents`, charge `amountCents`, never more than `capCents` in total.

**Auth:** API key with full access, and only through the console as an admin (a plain API key answers `403` with `billing-console-only`). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `CreditAutoRechargeRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | required | `false` creates the schedule paused. |
| `thresholdCents` | integer | optional | Charge when the balance falls below this. 0 to 9007199254740991, default `0`. |
| `amountCents` | integer | optional | Whole cents. 0 to 9007199254740991, default `0`. |
| `capCents` | integer | optional | Never charge more than this in total through the rule. 0 to 9007199254740991, default `0`. |

### Example request

```bash
curl -X PUT "https://api.rytiv.app/v1/billing/auto-recharge" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":true}'
```

### Response

`200` with the credit account with the new rule:

```json
{
  "orgId": "<orgId>",
  "currency": "EUR",
  "prepaid": true,
  "balanceMicros": "<balanceMicros>",
  "balanceCents": -9007199254740991,
  "blocked": true,
  "blockedAt": "2026-10-21T09:00:00Z",
  "warned": true,
  "warnAtPercent": 0,
  "minTopupCents": 1,
  "priced": [
    "<priced>"
  ],
  "autoRecharge": {
    "enabled": true,
    "thresholdCents": 0,
    "amountCents": 0,
    "capCents": 0,
    "pending": true,
    "failedAt": "2026-10-21T09:00:00Z",
    "hasPaymentMethod": true
  }
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `orgId` | string | always | The organization that owns it. |
| `currency` | `"EUR"` | always |  |
| `prepaid` | boolean | always | Whether the organization is on prepaid credit. |
| `balanceMicros` | string | always | The exact balance in micro-cents, as a decimal string. |
| `balanceCents` | integer | always | The balance in whole cents, rounded down. -9007199254740991 to 9007199254740991. |
| `blocked` | boolean | always | The balance reached zero: machines stopped, nothing new starts, reads stay open. |
| `blockedAt` | string (ISO 8601) or null | always | When that happened. |
| `warned` | boolean | always | The balance fell below the warning line. |
| `warnAtPercent` | integer | always | The warning line as a percentage of the reference top-up. 0 to 100. |
| `minTopupCents` | integer | always | The smallest top-up. at most 9007199254740991. |
| `priced` | array of string | always | The usage kinds that carry a price right now. |
| `autoRecharge` | object | always | The auto-recharge rule and its state. |
| `autoRecharge.enabled` | boolean | always | `false` creates the schedule paused. |
| `autoRecharge.thresholdCents` | integer | always | Charge when the balance falls below this. 0 to 9007199254740991. |
| `autoRecharge.amountCents` | integer | always | Whole cents. 0 to 9007199254740991. |
| `autoRecharge.capCents` | integer | always | Never charge more than this in total through the rule. 0 to 9007199254740991. |
| `autoRecharge.pending` | boolean | always |  |
| `autoRecharge.failedAt` | string (ISO 8601) or null | always |  |
| `autoRecharge.hasPaymentMethod` | boolean | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `billing-console-only` | A billing change is made in the console by an admin, not with an API key. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 409 | `not-prepaid` | The organization has no credit account; only org creation or an operator's grant starts prepaid credit. |

## POST /v1/billing/portal

A link to the payment portal, where a saved card is changed.

**Auth:** API key with full access, and only through the console as an admin (a plain API key answers `403` with `billing-console-only`). Refused for every agent token (`agent-token-credential`).

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/billing/portal" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the link:

```json
{
  "url": "<url>"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `url` | string | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `billing-console-only` | A billing change is made in the console by an admin, not with an API key. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 409 | `no-customer` | No payment relationship yet; a first top-up creates it. |

## GET /v1/billing/preview

What a period cost, line by line, from the metered usage.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Query parameters

| Name | Description |
| --- | --- |
| `from` | Start of the period (ISO 8601). |
| `to` | End of the period (ISO 8601). |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/billing/preview" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the preview:

```json
{
  "orgId": "<orgId>",
  "currency": "EUR",
  "from": "2026-10-21T09:00:00Z",
  "to": "www.example.com",
  "lines": [
    {
      "kind": "machine_running_seconds",
      "unit": "<unit>",
      "quantity": 0,
      "priced": true
    }
  ],
  "pricesDecided": true
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `orgId` | string | always | The organization that owns it. |
| `currency` | `"EUR"` | always |  |
| `from` | string (ISO 8601) | always |  |
| `to` | string (ISO 8601) | always | Another hostname of the same app to redirect to, or `null` to clear the redirect. |
| `lines` | array of object | always | One line per usage kind: quantity, unit, and the amount where a price is set. |
| `total` | number | if set | The sum of the priced lines. at least 0. |
| `pricesDecided` | boolean | always | Whether this answer carries amounts. |
| `pricedFrom` | string (ISO 8601) | if set |  |
| `unpricedReason` | `no-price-list` \| `no-credit-account` \| `before-credit` | if set | If not, why: `no-price-list`, `no-credit-account` or `before-credit`. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/usage

The metered usage events of the organization.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Query parameters

| Name | Description |
| --- | --- |
| `from` | Start of the period (ISO 8601). |
| `to` | End of the period (ISO 8601). |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/usage" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the usage list:

```json
{
  "period": {
    "from": "2026-10-21T09:00:00Z",
    "to": "www.example.com"
  },
  "events": [
    {
      "id": "<id>",
      "orgId": "<orgId>",
      "kind": "machine_running_seconds",
      "quantity": 0,
      "at": "2026-10-21T09:00:00Z"
    }
  ],
  "truncated": true,
  "limit": 1
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `period` | object | always |  |
| `period.from` | string (ISO 8601) | always |  |
| `period.to` | string (ISO 8601) | always | Another hostname of the same app to redirect to, or `null` to clear the redirect. |
| `events` | array of object | always |  |
| `truncated` | boolean | always |  |
| `limit` | integer | always | at most 9007199254740991. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
