---
title: Conventions
description: Bulk requests, resource-prefixed IDs, and per-supplier minimum order quantities.
icon: book-open
sidebar:
  order: 3
---

## Bulk operations

Create and update endpoints accept arrays, so a single request covers up to
**500 items**. A row that fails is reported in the `errors` array rather than
failing the whole call.

## Paging

Every list endpoint returns the same envelope: the rows in `items`, a boolean
`isDone`, and a `cursor`.

```json
{ "items": [], "isDone": false, "cursor": "..." }
```

Ask for a page size with `limit`, between 1 and 100, defaulting to 25. To
continue, send the `cursor` you were given back as a query parameter. Stop when
`isDone` is `true` rather than when a page comes back short: a page can be
smaller than `limit` and still not be the last one, because filters apply as
the cursor walks.

## Dates and times

Every timestamp in and out of the API is **milliseconds since the Unix epoch**,
as a number. That covers event dates, hire windows and the `timestamp` on a
webhook delivery. The one exception is a price list's `date`, which is a
calendar day as a `YYYY-MM-DD` string, because a price list belongs to a date
rather than a moment.

## Asking for fewer fields

Every read endpoint takes a `fields` parameter: a comma-separated list of what
you want back.

```bash
curl "https://app.bloomcount.com/api/v1/products?fields=name,category" \
  -H "Authorization: Bearer bloomcount_..."
```

A dotted path reaches inside a collection, so `fields=variants.sku` keeps the
variants array with only each variant's SKU, and `fields=groups.items.quantity`
walks two levels into an event. Naming a collection on its own, as in
`fields=variants`, returns it whole.

Three rules worth knowing:

- **The id always comes back**, listed or not, so a trimmed object is still
  something you can fetch or update later.
- **A name that doesn't exist is an error**, not an empty object. The response
  is `422` with `unknown_fields:` and the names that didn't resolve, so a typo
  tells you rather than looking like missing data.
- **Omitting the parameter changes nothing.** The full object is still the
  default, so existing integrations are unaffected.

On a paginated endpoint the parameter shapes each item; `isDone` and `cursor`
are the envelope and always come back.

Some reads do less work rather than merely sending less: an event fetched
without `groups` never loads its lines, and without `imageUrls` never resolves
them; a price list fetched without `entries` skips the price table entirely; an
event whose selection names no money never loads the price lists behind it.

## Money

Event reads carry the figures the app itself works with: what each line costs,
what the customer pays for it, a subtotal per group, and the event's own
totals. Four things are worth knowing before you invoice from them.

**Cost and customer price are different numbers.** `costIncTax` is what the
stock costs with tax and no markup. `customerPriceIncTax` is that with the
event's per-unit uplift and the markup for the line's product type — and hire
lines are marked up on their own multiplier, set separately from flowers and
materials.

**Tax is in the name.** A field ending `ExclTax` has none applied, one ending
`IncTax` has the tax multiplier of the price list that line is priced from.
Nothing is ambiguous on purpose.

**Waste is a real cost, kept separate.** `costIncTax` covers only the units a
design consumes, but stock is bought in whole packs, so the leftovers are money
spent too. They are `wasteCostIncTax`, and what the florist actually pays out
is `costOfGoodsIncTax` — the two added together, already worked out for you.
Never sum all three.

**The figures are unrounded floats.** `28.799999999999997` rather than `28.80`,
because that is what the arithmetic produces. Round at the point you present or
bill.

An event's `totals` is the rollup the app maintains, so it excludes hidden
groups and includes labour, additional fees and travel. That last part is why
it is larger than the sum of the line prices. It rides along on the events list
too, at no extra cost, so a report over many events needs one call rather than
one per event.

```bash
curl "https://app.bloomcount.com/api/v1/events?fields=name,totals" \
  -H "Authorization: Bearer bloomcount_..."
```

## IDs

Resource IDs are resource-prefixed strings. The prefix says what the ID refers
to, so a variant ID can't be passed where a product ID is expected.

| Prop | Type | Default | Description |
| - | - | - | - |
| `prod_?` | `Product` | - | A catalogue product — the thing a variant belongs to. |
| `var_?` | `Product variant` | - | A specific variety of a product; what an event line and a price entry point at. |
| `sup_?` | `Supplier` | - | Who a variant can be bought from. |
| `pl_?` | `Price list` | - | A dated set of prices for one product type. |
| `pe_?` | `Price entry` | - | One variant's price on one price list, optionally per supplier. |
| `tax_?` | `Tax group` | - | A named tax rate applied to an event's totals. |
| `evt_?` | `Event` | - | A customer event, with its dates, venue and pricing. |
| `evtgrp_?` | `Event group` | - | A section within an event, such as a bouquet or a table. |
| `evti_?` | `Event item` | - | One line on an event: a variant, a supplier and a quantity. |
| `whsub_?` | `Webhook subscription` | - | A URL receiving event deliveries. |

Treat the part after the underscore as opaque. Passing the wrong type of ID
returns `422 wrong_id_type:expected_<table>_got_<table>`.

## Suppliers and minimum order quantities

Variants can be scoped to specific suppliers (`supplierIds`) and carry
per-supplier minimum order quantities (`supplierMinPacks`). When an event's
order sheet is generated, packs for a given variant and supplier are rounded up
to `minPacks`, so a supplier insisting on five packs per order is never
under-ordered.

Both fields reference suppliers by ID. List a tenant's suppliers with
`GET /api/v1/suppliers` (scope `suppliers:read`) and use the returned
`supplierId` on the products bulk upsert. The hidden Default supplier is not
returned there and cannot be assigned through the API.
