# Products & Pricing


Manage the product catalogue over the LedgerBee public API (base URL
[`https://api.ledgerbee.com/api/v1`](https://api.ledgerbee.com/api/v1)). A
product price is what an invoice, quote, or order-confirmation line references
through its `priceId` to bill a real catalogue product. Authenticate with the
`x-api-key` header; the key carries the tenant, and every `:id` path segment is
a UUID.

The catalogue is three resources, all under one scope pair:

- `products-read` — list and get products, prices, and product groups.
- `products-write` — create, update, and delete products, prices, and product
  groups, and run price lifecycle actions.

A product belongs to a product group, and a product has one or more prices.
Create them in that order: group, then product, then price.

## Product groups

A product group maps a product's sales to ledger accounts per VAT zone
(domestic, EU, abroad, domestic-without-VAT) and is required on every product.

| Operation | Endpoint | Scope |
| --- | --- | --- |
| List | `GET /v1/product-groups` | `products-read` |
| Get | `GET /v1/product-groups/{id}` | `products-read` |
| Create | `POST /v1/product-groups` | `products-write` |
| Update | `PATCH /v1/product-groups/{id}` | `products-write` |
| Delete | `DELETE /v1/product-groups/{id}` | `products-write` |

All four account fields are required: `domesticAccountId`, `euAccountId`,
`abroadAccountId`, and `domesticWithoutVatAccountId`. The domestic, EU, and
abroad accounts must each reference a ledger account carrying a VAT code valid
for sales; the domestic-without-VAT account has no VAT requirement. Ledger
accounts and VAT codes are available over the public API. See the
[create operation](/api/product-groups#create-a-product-group).

```bash
curl -X POST 'https://api.ledgerbee.com/api/v1/product-groups' \
  -H 'x-api-key: <your-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "number": "PG001",
    "name": "Services",
    "domesticAccountId": "0197a943-2325-7829-b835-b6c71a293010",
    "euAccountId": "0197a943-2325-7829-b835-b6c71a293011",
    "abroadAccountId": "0197a943-2325-7829-b835-b6c71a293012",
    "domesticWithoutVatAccountId": "0197a943-2325-7829-b835-b6c71a293013"
  }'
```

## Products

A product is a catalogue item belonging to a product group. `GET /v1/products/{id}`
returns the product with its `prices` array, so the get response is where you read
the `priceId` for a billing-document line. The list (`GET /v1/products`) is a
lean index and omits the `prices` array; get a single product to read its prices.

| Operation | Endpoint | Scope |
| --- | --- | --- |
| List (no prices) | `GET /v1/products` | `products-read` |
| Get (with prices) | `GET /v1/products/{id}` | `products-read` |
| Create | `POST /v1/products` | `products-write` |
| Update | `PATCH /v1/products/{id}` | `products-write` |
| Delete | `DELETE /v1/products/{id}` | `products-write` |

`productGroupId` is required and must reference an existing group. `productNumber`
is unique per company. An optional `initialPrice` creates, activates, and
sets-default a first price in the same call. See the
[create operation](/api/products#create-a-product).

```bash
curl -X POST 'https://api.ledgerbee.com/api/v1/products' \
  -H 'x-api-key: <your-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "productNumber": "P001",
    "name": "Web hosting",
    "unit": "pcs",
    "productGroupId": "0197a943-2325-7829-b835-b6c71a293020"
  }'
```

## Product prices

A price belongs to a product. Its `id` is the `priceId` used on invoice, quote,
and order-confirmation lines.

| Operation | Endpoint | Scope |
| --- | --- | --- |
| Create | `POST /v1/product-prices` | `products-write` |
| Get | `GET /v1/product-prices/{id}` | `products-read` |
| Update | `PATCH /v1/product-prices/{id}` | `products-write` |
| Delete | `DELETE /v1/product-prices/{id}` | `products-write` |
| Activate | `POST /v1/product-prices/{id}/activate` | `products-write` |
| Disable | `POST /v1/product-prices/{id}/disable` | `products-write` |
| Archive | `POST /v1/product-prices/{id}/archive` | `products-write` |
| Set default | `POST /v1/product-prices/{id}/set-default` | `products-write` |

A price is created as a draft; activate it before using it on a document. The
default price of a product cannot be disabled, archived, or deleted. A price
already used on a subscription or invoice is locked and only a limited field set
can change. See the
[create operation](/api/product-prices#create-a-product-price).

```bash
curl -X POST 'https://api.ledgerbee.com/api/v1/product-prices' \
  -H 'x-api-key: <your-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "productId": "0197a943-2325-7829-b835-b6c71a293030",
    "billingPeriodType": "one_time",
    "pricingModel": "flat_rate",
    "unitAmount": "199.00",
    "currency": "DKK"
  }'
```

### Pricing models

`billingPeriodType` and `pricingModel` together describe how a price bills.

| Field | Value | Meaning |
| --- | --- | --- |
| `billingPeriodType` | `one_time` | A one-off charge. No billing interval. |
| `billingPeriodType` | `recurring` | Bills each interval. Requires `billingInterval` and a Subscription license. |
| `billingPeriodType` | `usage` | Metered. Requires `meterId`, a billing interval, and both the Subscription and Metered Products licenses. |
| `pricingModel` | `flat_rate` | One `unitAmount`. No tiers. |
| `pricingModel` | `package` | Exactly one tier. |
| `pricingModel` | `volume` / `graduated` | One or more tiers; the last tier must be open-ended (omit its `toQuantity`). |

A `recurring` or `usage` price requires the matching license. A request without
it is rejected with `LICENSE.REQUIRED`. Meters are available over the public API.

## Errors

| Condition | Error code | Fix |
| --- | --- | --- |
| Group number already used | `PRODUCT_GROUP_NUMBER_EXISTS` | Use a unique `number`. |
| Account missing / wrong type | `PRODUCT_GROUP_INVALID_ACCOUNT_ID` | Reference an existing sales account. |
| Sales account has no VAT code | `PRODUCT_GROUP_ACCOUNT_MUST_HAVE_VAT_CODE` | Pick a domestic/EU/abroad account with a VAT code. |
| Product number already used | `PRODUCT_NUMBER_EXISTS` | Use a unique `productNumber`. |
| Product group not found | `PRODUCT_GROUP_NOT_FOUND` | Create the group first, then reference its id. |
| Recurring/usage price without the license | `LICENSE.REQUIRED` | The tenant needs the Subscription (and, for usage, Metered Products) license. |
