Skip to content

Offers & Pricing

The offers catalog lets an organisation define what it sells — products, services, and subscriptions, each with a price and a billing model. It is the data foundation for invoicing automation at the end of the Sales pipeline, and it powers a public pricing page without any hand-rolled HTML pricing logic: a site fetches the org’s active offers through an API-key-gated read API.

The domain lives in @abeauvois/platform-billing (Offer / IOfferRepository / OfferService), co-located with the billing module that will own invoicing but decoupled from the credit ledger.

Each Offer is owner- and org-scoped and carries:

FieldNotes
offerTypeproduct | service | subscription
billingModelone_off | recurring | custom_quote
priceAmountCentsPrice in minor units (cents). null for custom_quote.
currencyISO 4217, e.g. EUR (default).
recurrenceIntervalmonth | year — required when billingModel is recurring.
featuresBullet points for a pricing card.
slugURL-safe, unique per owner — the public read key. Derived from name if omitted.
statusdraft | active | archived — only active offers are served publicly.

Invariants are enforced server-side: a custom_quote offer never has a price; a recurring offer must name an interval.

Manage offers from the dashboard at /offers, or via the SDK with an authenticated session:

import { PlatformApiClient } from '@abeauvois/platform-sdk';
const client = new PlatformApiClient({ baseUrl, sessionToken });
const studio = await client.offers.create({
name: 'viite studio',
offerType: 'product',
billingModel: 'one_off',
priceAmountCents: 9900, // 99.00 EUR
features: ['Brand kit', 'Landing page'],
});
// Publish it to the public pricing API.
await client.offers.update(studio.id, { status: 'active' });
const all = await client.offers.list({ status: 'active' });

Endpoints: GET/POST /api/offers, GET/PATCH/DELETE /api/offers/:id. Reads are scoped to the caller — another user’s offer reads as 404. Creating an offer requires an active organization.

Public: rendering a pricing page (API-key gated)

Section titled “Public: rendering a pricing page (API-key gated)”

A consumer website (e.g. viite.ai) renders its pricing page from the owner’s active offers. Mint an API key for the owner account and pass it as a bearer token — no user session required:

const site = new PlatformApiClient({ baseUrl, getToken: () => SITE_API_KEY });
const offers = await site.offers.listPublic(); // GET /api/offers/public/offers
const studio = await site.offers.getPublicBySlug('viite-studio');

The public response is a projected DTO — slug, name, description, offerType, billingModel, priceAmountCents, currency, recurrenceInterval, features, sortOrder. Internal fields (userId, orgId, status, ids) never leak. The key resolves to its owner, so a site only ever sees that owner’s active offers.

viite.ai dogfoods this end-to-end. Its corporate-website funnel no longer hardcodes prices — it reads the catalog from /api/offers/public/offers at build time (SSG) and bakes the result into the page, using the owner’s API key (kept off the browser, never PUBLIC_-prefixed). The catalog is managed in the dashboard /offers; editing an offer there changes the funnel’s pricing on the next site rebuild. The funnel keeps a built-in fallback so a degraded build still renders sensible numbers. Slugs follow a per-consumer convention (e.g. expertise-sprint, module-core, module-core-setup, managed-pro).