Domain Registrar / Per-SKU Catalog
The "one logical product, hundreds of priced variants" pattern: customers buy domain names, but each TLD has its own wholesale cost from the registry, its own retail price, its own renewal duration, and its own pricing curve. Modeling each TLD as a separate Kontier product would mean 1,500+ catalog entries; modeling them as keyed siblings on a single product — one product opted into a key set whose entries are the TLDs, plus one price per key — collapses the catalog to one row plus a price per key.
Real-world examples. Namecheap, GoDaddy, Cloudflare Registrar, Porkbun, Hover, Gandi, Squarespace Domains, Domain.com. Common shape: 1,500+ TLDs, prices ranging from $0.99 (.xyz on first year) to $5,000+ (premium .com / domain auctions), per-TLD renewal pricing, regional pricing differences (some TLDs have different wholesale costs for EU vs US registrants), bulk transfer flows.
The shape of the problem
Catalogs that grow with the underlying registry inventory create distinct billing challenges:
- Catalog explosion. Naively modeling 1,500 TLDs as 1,500 products means 1,500 plan attachments, 1,500 prices per currency, 1,500 places to update when wholesale costs shift. The catalog becomes its own maintenance burden.
- New TLDs land continuously. ICANN periodically delegates new TLDs (
.app,.dev,.ai). Your catalog has to absorb new SKUs without breaking ingest for in-flight registrations. - Unknown / mistyped SKUs. Upstream resellers sometimes send events for TLDs that aren't in your catalog (typos, deprecated TLDs, brand-new gTLDs). A keyed product's
price_keyis validated against its key set at ingest: an event whose key is not an active, non-excluded entry is rejected with aprice_keyvalidation error (HTTP 400), so a mistyped or unknown TLD never lands as an un-priced line — you add the missing key to the set before retrying. - Asymmetric renewal pricing. First-year
.xyzis $0.99; renewal is $9.99. The customer pays first-year price at registration, renewal price every year after. Separate price keys for registration and renewal handle this. - Bulk operations. Customers often register or renew 10+ domains in one transaction. The invoice should show one line per (TLD, action), not collapse into a single mush.
- Premium domains. Some specific names (single words, short strings) have wholesale prices set by the registry that override the standard TLD price. A one-off charge at the premium price handles these.
Kontier blueprint
| Concern | Kontier primitive |
|---|---|
| One product, many priced variants | Keyed product opted into a key set via key_set_id (TLDs are the set's keys) |
| Per-TLD price | One Price per (product, price_key=".tld") and currency |
| Per-TLD display name | Price.display_name (e.g., ".com domain") |
| Unknown SKU behavior | price_key validated against the key set at ingest; unknown keys are rejected (400) |
| Asymmetric first-year vs renewal | Different price keys per lifecycle event (e.g. .io-register / .io-renew) |
| Bulk operations | A batch of usage events in one call; keyed usage bills one invoice line per key |
| Premium domain pricing | A one-off entry with a stated unit price on a standalone invoice |
Build it
IDs in the examples are placeholders: use the id each create call returns.
1. Create the key set of TLDs
The TLDs you sell are the entries of an org-global key set. A key is 1-64 characters from a-z, A-Z, 0-9, ., _ and -, so .com is a valid key.
Code
When the registry delegates a new TLD, add it with PUT /v1/key-sets/{id}/entries, which upserts by key; retire a TLD by upserting it with "status": "retired".
2. Define the keyed product
A keyed product is one opted into the key set via key_set_id. The set's active entries are the valid price_key values; key_set_excluded_keys is a subtract-only list for keys you want to opt this product out of. Because the billed quantity comes from usage events, set quantity_source to METERED.
Code
A metered product needs a meter bound to it as its billable quantity:
Code
The product's price_key domain is the key set's active entries minus key_set_excluded_keys. An ingested usage event whose price_key is not an active, non-excluded entry is rejected with a price_key validation error (HTTP 400), and so is an event for this product that carries no price_key at all. An unknown or mistyped TLD bounces at ingest, and your catalog team adds the missing key to the set before retrying. There is no per-product "unknown SKU policy"; key-set membership is the gate.
3. Add prices for the most common TLDs
Each TLD is one Price on the product, keyed by price_key. The price carries the per-unit rate (list_price, a decimal string in major units) and its currency. Add one price per key and currency you bill in.
Code
For 1,500+ TLDs, this is a script run from your wholesale-cost ingest pipeline - one POST per TLD per currency, automated. Each price_key must already be an active entry of the product's key set.
The prices sit in the product's working draft until you publish it. Publishing is a change:
Code
To publish a wholesale-cost update at a set time instead, send "when": "at" with an at wall-clock time and a zone.
4. Give the customer a subscription
Usage bills only through a metered line on the customer's subscription, and a keyed product's line names one price_key: keyed usage bills one line per key. Create the subscription, author revision 1 with one line per TLD the customer registers, and start it:
Code
Code
Code
origin.kind: "product" sells the product directly, without a plan; version is the product version you published. A line without a price_key, or with a key the set does not offer, is refused with SUBSCRIPTION_LINE_PRICE_KEY.
When the customer first registers under a TLD the deal has no line for, the event is accepted but billed by nothing. GET /v1/subscriptions/usage-without-line lists such usage per subscription, product and key. Add the line before (or when) the customer starts using the TLD: cut the live revision now (POST /v1/subscriptions/{id}/revisions/cut), write the whole draft with a new last phase that lists the existing lines plus the new one (PUT /v1/subscriptions/{id}/revisions/draft), and publish (POST /v1/subscriptions/{id}/revisions/draft/publish). The SaaS per-seat blueprint shows the three calls in full.
5. Customer registers a domain
Code
The event flows to the .io price; the next invoice carries the .io line: "1 × $35.00". quantity is a decimal string, and idempotency_key dedupes replays of the same event.
6. Bulk renewal of 5 domains
Send the renewals as one batch ({ "events": [ … ] }, 1 to 1,000 events per call), one event per domain:
Code
Keyed usage bills one invoice line per price_key, so the five renewals appear as one .io line with quantity 5, at the price's display_name. Domains under different TLDs land on separate lines, one per key. The per-domain detail stays on the events: GET /v1/usage-events?subscription_id=… returns each event with its metadata. To bill registration and renewal on separate lines, give them separate keys (see Variations).
7. Premium domain
A premium .com priced at $5,000 by the registry is a one-off charge, not a catalog price. Bill it on a standalone draft invoice with a stated unit price:
Code
A stated unit_price wins over any catalog price. An entry without a product_id needs a title and is taxed under the invoice's own tax determination. Finalize the draft with POST /v1/invoices/{id}/finalize. Standard .com registrations on the subscription are unaffected.
Variations
- Asymmetric first-year vs renewal pricing. Give each lifecycle event its own key (e.g. add
.io-registerand.io-renewas separate key-set entries with their own prices and their own subscription lines), and send the matchingprice_keyon the registration vs renewal event. - Bulk discount over N domains. Create the product with
pricing_model: "STAIRCASE"and give the.comprice a tier ladder:[{ "up_to": "10", "unit_amount": "11.50" }, { "unit_amount": "9.50" }](decimal strings; the last tier omitsup_to). Customers renewing 11+ domains in a period get the lower rate on the tail. - Catch new TLDs with a set price. Create one price with
is_set_price: trueand noprice_keyon the product: the resolver falls back to it for any key in the set that has no exact-key price, so new TLDs added to the key set get a safe default rate while your catalog team backfills per-TLD prices. (Keys that are not in the set at all are still rejected at ingest, and each key still needs its subscription line.) - Per-region wholesale cost. The same TLD might have different prices in different countries. Add
country_codeto the price; a customer is priced at their billing-address country, and a price with nocountry_codematches any country. - Other catalog explosion patterns. This same blueprint covers: IP allocations (one IP allocation product, keyed by block size such as
ipv4-24), per-region cloud SKUs (one VM product, keyed by region and size such aseu-central-1_m5.large; see Cloud infrastructure), per-CA SSL certificates (one certificate product, keyed by issuing CA). Keys cannot contain/.
What you don't have to build
- 1,500 separate product records (one keyed product plus a key set replaces them)
- "Unknown SKU" validation (key-set membership is enforced at ingest; unknown keys are rejected with a 400)
- Detection of usage no line bills (
GET /v1/subscriptions/usage-without-line) - Premium-domain charges as a separate codepath (a standalone invoice with a stated unit price)
- Bulk operation aggregation and deduplication (batch ingest plus idempotency keys on the events)
Next steps
- Key Sets - the core feature this pattern showcases
- Prices - keyed prices, set prices and country-scoped prices
- Usage Metering - event ingest and idempotency
- Invoices - standalone drafts for one-off charges
- Custom Fields - storing domain-specific metadata on subscriptions