Rebuilding a product catalog usually starts in spreadsheets: a column for products, another for entitlements, and a separate tab that engineering hopes stays in sync. We went through the same pain and ended up moving the entire workflow into the UsageBox API so pricing changes ship like regular application code.
The baseline structure is simple - projects group environments or customer-facing surfaces, products describe what you sell, and product_items capture the individual billable components that plans attach to. Because every resource lives in Firestore through the UsageBox service, the catalog stays versioned, queryable, and ready for automation.
An API-First Catalog
Everything we do in the dashboard is mirrored by REST endpoints. A typical workflow starts with POST /api/v1/projects to define the container for a catalog variant. Projects carry the account_id, display name, and status flag so the same customer can live in production, staging, and a beta sandbox without rewriting rules.
Within a project we call POST /api/v1/products. Products pick up the project_id automatically, along with metadata like description strings that show up in invoices. The GET /api/v1/products/:id response folds in related plans so you can hand a single payload to your billing portal without fanning out to multiple services.
UsageBox creates the bridge between catalog objects and ingestion by way of product_items. These are the things you meter against - API calls, seats, model training hours. When we hit POST /api/v1/product_items the service generates an ingest_key; the same key appears in product_items inside the GET /api/v1/products/:id/overview payload. That key is what ingestion events reference so billing can attribute usage back to the right catalog entry.
How the Pieces Connect
- Project: top-level scope used to isolate catalogs (think "Production" vs "Early Access"). The API enforces account ownership and keeps a status flag so you can archive experiments without deleting history.
- Product: business object the customer buys. Each product stores its canonical project, description text, and the list of associated plans.
- Product Item: fine-grained component tied either to a meter or a manual charge. Items inherit the project from the parent product, and UsageBox issues an
ingest_keyso telemetry can point at them. - Plan: pricing wrapper that joins meters, items, and charge tiers. Plans reference
product_itemsby ID, so moving a plan between products is as simple as patching the link.
This structure keeps billing consistent even when product and finance are iterating quickly. When we need a new add-on, we add a product_item, attach it to a plan, and expose the new SKU in customer portals without refactoring ingestion.
Walking Through a Real Update
Say we are launching a "Usage Insights" add-on for an existing analytics product. The script we run behind the dashboard does three things:
- Create the add-on with
POST /api/v1/product_itemsso we get an ingest key likeitem_usage_insights. - Attach the item to the plan via
POST /api/v1/plans/:planId/charges, referencing the newproduct_item_idand either selecting a meter or setting a flat tier. - Kick off a backfill that tags historical usage events with the new
product_itemso reports and invoices stay aligned.
Every step is visible through GET /api/v1/products/:id/overview, which returns the project, product, items, and plans in one payload. Support uses the same endpoint to confirm what a customer should see when they open the UsageBox portal.
Keeping Catalog Changes Safe
We treat catalog updates like development work: stage first, promote when ready. Projects give us that guardrail. When we draft a new product we create it inside a "Preview" project, sync test accounts to the plan, and watch usage flow through before flipping the project status to active.
Because UsageBox stores timestamps on every catalog document, we can diff releases and answer finance's favorite question: "what changed between the quarter-close snapshot and now?" If something goes sideways, we toggle the product or item status to inactive and UsageBox immediately hides it from plan builders and invoices.
Once the catalog is clean, pricing work feels less like a quarterly reset and more like day-to-day product iteration. See how catalog updates feed pricing experiments. The same API paths power both stories.
Tips From Rolling It Out
Import existing SKUs via the API instead of manually retyping them. The scripts we wrote loop through our legacy spreadsheet and call POST /api/v1/product_items, storing the API response as the new source of truth. We also keep a short checklist for QA: confirm the project status, double-check each product_item ingest key, and ensure the plan charges all reference active meters.
None of this stops at billing. Because catalog data is tidy, we expose it to downstream systems - quoting, CRM, usage reporting - through the same endpoints. It keeps everyone speaking the same language when a customer asks, "What exactly am I paying for?"