Conventions
Bulk requests, resource-prefixed IDs, and per-supplier minimum order quantities.
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.
{ "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.
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
422withunknown_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.
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.
prod_?Product
A catalogue product — the thing a variant belongs to.
Productvar_?Product variant
A specific variety of a product; what an event line and a price entry point at.
Product variantsup_?Supplier
Who a variant can be bought from.
Supplierpl_?Price list
A dated set of prices for one product type.
Price listpe_?Price entry
One variant's price on one price list, optionally per supplier.
Price entrytax_?Tax group
A named tax rate applied to an event's totals.
Tax groupevt_?Event
A customer event, with its dates, venue and pricing.
Eventevtgrp_?Event group
A section within an event, such as a bouquet or a table.
Event groupevti_?Event item
One line on an event: a variant, a supplier and a quantity.
Event itemwhsub_?Webhook subscription
A URL receiving event deliveries.
Webhook subscriptionTreat 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.